# -*- coding: utf-8 -*-
"""Build ATP four-way comparison report (self-contained HTML, PDF via Chrome)."""
import json, math, os, sys, html as H
from datetime import datetime

BASE = os.path.expanduser('~/Desktop/Hermes输出-工作类')
DATA = os.path.join(BASE, '数据/atp_fourway_comparison_20261106.json')
OUT_HTML = os.path.join(BASE, '产品与市场/调研报告/ATP荧光检测四方差_易瑞_天隆_宝锐_克拉玛尔_20261106_V01.html')
sys.path.insert(0, os.path.join(BASE, '脚本'))
import brand_kit as BK

D = json.load(open(DATA, encoding='utf-8'))
NOW = '2026-11-06'

def e(s):
    return H.escape(str(s), quote=True)

def ref(*ids):
    return ''.join(f'<sup class="ref">{i}</sup>' for i in ids)

def pct(v):
    return f'{v:.1f}%' if isinstance(v, float) else f'{v}%'

def fmt_ratio(v):
    if v >= 100:
        return f'{v:,.0f}×'
    if v >= 10:
        return f'{v:.1f}×'
    return f'{v:.2f}×'

# ---------- SVG: LOD ladder ----------
def svg_lod():
    W, Hh = 1080, 350
    left, right, top, bottom = 78, 40, 52, 96
    plotW = W - left - right
    minv, maxv = 0.07, 2500.0
    def x(v):
        return left + (math.log10(v) - math.log10(minv)) / (math.log10(maxv) - math.log10(minv)) * plotW
    axis_y = 228
    def ygrid(v):
        return axis_y
    ticks = [0.1, 1, 10, 100, 1000]
    out = [f'<svg class="fig" viewBox="0 0 {W} {Hh}" role="img" aria-label="ATP 检测限口径阶梯图">']
    out.append('<rect x="0" y="0" width="1080" height="350" fill="#ffffff"/>')
    # band 0.1-1 fmol
    x1, x2 = x(0.1), x(1.0)
    out.append(f'<rect x="{x1:.1f}" y="{top}" width="{max(2,x2-x1):.1f}" height="{axis_y-top+18}" fill="#eef4ff" opacity="0.9"/>')
    out.append(f'<text x="{(x1+x2)/2:.1f}" y="{top-14}" text-anchor="middle" font-size="11" fill="#1a56db">标称 / 系统 LOD 区间</text>')
    # axis
    out.append(f'<line x1="{left}" y1="{axis_y}" x2="{W-right}" y2="{axis_y}" stroke="#94a3b8" stroke-width="1.5"/>')
    for t in ticks:
        xx = x(t)
        out.append(f'<line x1="{xx:.1f}" y1="{axis_y}" x2="{xx:.1f}" y2="{axis_y+8}" stroke="#94a3b8" stroke-width="1"/>')
        lab = {0.1:'0.1',1:'1',10:'10',100:'100',1000:'1,000'}[t]
        out.append(f'<text x="{xx:.1f}" y="{axis_y+28}" text-anchor="middle" font-size="11" fill="#475569">{lab}</text>')
    out.append(f'<text x="{W-right}" y="{axis_y+52}" text-anchor="end" font-size="10.5" fill="#64748b">ATP（fmol / assay，对数刻度）</text>')
    # gap bracket 0.9 -> 100
    xb, xd = x(0.9), x(100)
    by = axis_y + 66
    out.append(f'<line x1="{xb:.1f}" y1="{by}" x2="{xd:.1f}" y2="{by}" stroke="#b91c1c" stroke-width="1" stroke-dasharray="4 3"/>')
    out.append(f'<line x1="{xb:.1f}" y1="{by-5}" x2="{xb:.1f}" y2="{by+5}" stroke="#b91c1c" stroke-width="1"/>')
    out.append(f'<line x1="{xd:.1f}" y1="{by-5}" x2="{xd:.1f}" y2="{by+5}" stroke="#b91c1c" stroke-width="1"/>')
    out.append(f'<text x="{(xb+xd)/2:.1f}" y="{by-7}" text-anchor="middle" font-size="11" fill="#b91c1c">宝锐 0.1 pmol 比 AOAC 系统 LOD 高约 111×</text>')
    # markers
    items = [
        ('A', 0.1, '#b91c1c', '天隆营销 10⁻¹⁶', 'up', -8),
        ('B', 0.9, '#1a56db', 'AOAC 系统 LOD', 'down', 34),
        ('C', 1.0, '#b45309', '天隆正文 10⁻¹⁵', 'up', -26),
        ('D', 100.0, '#047857', '宝锐 0.1 pmol', 'down', 26),
    ]
    for letter, v, col, label, side, dx in items:
        xx = x(v)
        out.append(f'<line x1="{xx:.1f}" y1="{top+16}" x2="{xx:.1f}" y2="{axis_y}" stroke="{col}" stroke-width="2"/>')
        out.append(f'<circle cx="{xx:.1f}" cy="{axis_y}" r="5.5" fill="{col}" stroke="#fff" stroke-width="1.5"/>')
        if side == 'up':
            ly = top - 2
            out.append(f'<line x1="{xx:.1f}" y1="{top+16}" x2="{xx+dx:.1f}" y2="{ly+8}" stroke="{col}" stroke-width="1" opacity="0.65"/>')
            out.append(f'<text x="{xx+dx:.1f}" y="{ly}" text-anchor="middle" font-size="12" font-weight="bold" fill="{col}">{letter}</text>')
        else:
            ly = axis_y - 44
            out.append(f'<line x1="{xx:.1f}" y1="{axis_y-8}" x2="{xx+dx:.1f}" y2="{ly+8}" stroke="{col}" stroke-width="1" opacity="0.65"/>')
            out.append(f'<text x="{xx+dx:.1f}" y="{ly}" text-anchor="middle" font-size="12" font-weight="bold" fill="{col}">{letter}</text>')
    # legend
    lx, ly = 70, 322
    for i, (letter, v, col, label, side, dx) in enumerate(items):
        cx = lx + i * 250
        out.append(f'<circle cx="{cx}" cy="{ly-4}" r="4.5" fill="{col}"/>')
        out.append(f'<text x="{cx+9}" y="{ly}" font-size="10.5" fill="#334155">{letter}  {label}（{v:g} fmol）</text>')
    out.append('</svg>')
    return ''.join(out)

# ---------- SVG: precision ----------
def svg_precision():
    W, Hh = 1080, 390
    left, right, top, bottom = 78, 34, 44, 78
    plotW, plotH = W-left-right, Hh-top-bottom
    levels = D['precision']['levels_fmol']
    dev = D['precision']['developer_rsd']
    ind = D['precision']['independent_rsd']
    ymax = 80
    def y(v): return top + plotH * (1 - v/ymax)
    groupW = plotW / len(levels)
    out = [f'<svg class="fig" viewBox="0 0 {W} {Hh}" role="img" aria-label="天隆 AOAC 纯分析物精密度图">']
    out.append('<rect x="0" y="0" width="1080" height="390" fill="#ffffff"/>')
    for g in [0,20,40,60,80]:
        yy = y(g)
        out.append(f'<line x1="{left}" y1="{yy:.1f}" x2="{W-right}" y2="{yy:.1f}" stroke="#e2e8f0" stroke-width="1"/>')
        out.append(f'<text x="{left-12}" y="{yy+4:.1f}" text-anchor="end" font-size="10.5" fill="#64748b">{g}%</text>')
    # highlight 100 fmol group
    gi = levels.index(100)
    gx = left + gi * groupW
    out.append(f'<rect x="{gx:.1f}" y="{top-8}" width="{groupW:.1f}" height="{plotH+12}" fill="#eff6ff" opacity="0.95"/>')
    out.append(f'<text x="{gx+groupW/2:.1f}" y="{top-16}" text-anchor="middle" font-size="10.5" fill="#1a56db">0.1 pmol = 100 fmol</text>')
    # bars
    bw = 28
    for i, lv in enumerate(levels):
        cx = left + i*groupW + groupW/2
        for vals, off, col, lab in [(dev,-bw/1.7,'#1a56db','开发者'), (ind,bw/1.7,'#f59e0b','独立实验室')]:
            v = vals[i]
            yy = y(v)
            xx = cx + off - bw/2
            out.append(f'<rect x="{xx:.1f}" y="{yy:.1f}" width="{bw}" height="{y(0)-yy:.1f}" fill="{col}" rx="2"/>')
            out.append(f'<text x="{xx+bw/2:.1f}" y="{yy-5:.1f}" text-anchor="middle" font-size="9.5" fill="#334155">{v:.1f}</text>')
        out.append(f'<text x="{cx:.1f}" y="{Hh-40}" text-anchor="middle" font-size="10.5" fill="#334155">{lv:,}</text>')
    out.append(f'<text x="{W-right}" y="{Hh-14}" text-anchor="end" font-size="10.5" fill="#64748b">ATP（fmol / assay），n=10</text>')
    out.append(f'<text x="{left}" y="{Hh-14}" font-size="10.5" fill="#64748b">RSD（相对标准偏差）</text>')
    # legend
    out.append(f'<rect x="{W-300}" y="14" width="12" height="12" fill="#1a56db"/><text x="{W-283}" y="24" font-size="10.5" fill="#334155">开发者</text>')
    out.append(f'<rect x="{W-200}" y="14" width="12" height="12" fill="#f59e0b"/><text x="{W-183}" y="24" font-size="10.5" fill="#334155">独立实验室</text>')
    out.append('</svg>')
    return ''.join(out)

# ---------- SVG: stability ----------
def svg_stability():
    W, Hh = 1080, 360
    left, right, top, bottom = 78, 38, 46, 78
    plotW, plotH = W-left-right, Hh-top-bottom
    temps = D['stability']['temps']
    br = D['stability']['baorui']
    kl = D['stability']['klamar']
    target = D['stability']['target']
    ymax = 100
    def y(v): return top + plotH*(1-v/ymax)
    groupW = plotW/len(temps)
    out = [f'<svg class="fig" viewBox="0 0 {W} {Hh}" role="img" aria-label="宝锐与克拉玛尔 7 天稳定性对比图">']
    out.append('<rect x="0" y="0" width="1080" height="360" fill="#ffffff"/>')
    for g in [0,20,40,60,80,100]:
        yy=y(g)
        out.append(f'<line x1="{left}" y1="{yy:.1f}" x2="{W-right}" y2="{yy:.1f}" stroke="#e2e8f0" stroke-width="1"/>')
        out.append(f'<text x="{left-12}" y="{yy+4:.1f}" text-anchor="end" font-size="10.5" fill="#64748b">{g}%</text>')
    # target line
    ty = y(target)
    out.append(f'<line x1="{left}" y1="{ty:.1f}" x2="{W-right}" y2="{ty:.1f}" stroke="#b91c1c" stroke-width="1.6" stroke-dasharray="7 4"/>')
    out.append(f'<line x1="{W-370}" y1="24" x2="{W-350}" y2="24" stroke="#b91c1c" stroke-width="1.6" stroke-dasharray="7 4"/>')
    out.append(f'<text x="{W-343}" y="28" font-size="10.5" fill="#b91c1c">虚线：易瑞目标 ≥85%</text>')
    bw=40
    for i,t in enumerate(temps):
        cx=left+i*groupW+groupW/2
        for vals, off, col, lab in [(br,-bw/1.7,'#1a56db','宝锐'),(kl,bw/1.7,'#94a3b8','克拉玛尔')]:
            v=vals[i]; yy=y(v); xx=cx+off-bw/2
            out.append(f'<rect x="{xx:.1f}" y="{yy:.1f}" width="{bw}" height="{y(0)-yy:.1f}" fill="{col}" rx="2"/>')
            out.append(f'<text x="{xx+bw/2:.1f}" y="{yy-6:.1f}" text-anchor="middle" font-size="11" font-weight="bold" fill="{col if col!="#94a3b8" else "#475569"}">{v}%</text>')
        out.append(f'<text x="{cx:.1f}" y="{Hh-40}" text-anchor="middle" font-size="11" fill="#334155">{t}</text>')
    out.append(f'<text x="{W-right}" y="{Hh-14}" text-anchor="end" font-size="10.5" fill="#64748b">37℃ 7 天；0 天为 100%；单次实验，2T/点</text>')
    out.append(f'<rect x="{W-260}" y="14" width="12" height="12" fill="#1a56db"/><text x="{W-243}" y="24" font-size="10.5" fill="#334155">宝锐 E800</text>')
    out.append(f'<rect x="{W-150}" y="14" width="12" height="12" fill="#94a3b8"/><text x="{W-133}" y="24" font-size="10.5" fill="#334155">克拉玛尔</text>')
    out.append('</svg>')
    return ''.join(out)

# ---------- Tables ----------
comparison = [
    ('角色', ['荧光素酶原料供应商', '生化试剂目录供应商', 'ATP 仪器 + Quickswab 拭子厂家', '食品快检上市公司；天隆 Biolum 海外代理'], ['原料层', '原料层', '系统层', '渠道/客户端']),
    ('可比较产品', ['E800-020714，1 mg/mL，甘油液体/冻干粉', '13081130021 萤火虫荧光素酶，冻干粉', 'Biolum / Biolum Plus / Biolum Pro；Quickswab A010–A036', 'ATP 荧光检测试剂（餐具/环境洁净度）；仪器型号未公开'], ['酶', '酶', '仪器+耗材', '试剂+渠道']),
    ('检测限 LOD', ['0.1 pmol ATP/管（3σ）', '未披露', '营销 1×10⁻¹⁶ mol；官网正文 1×10⁻¹⁵ mol；AOAC 系统 0.9 fmol', '未披露'], ['◐ 单体系', '○ 未披露', '● AOAC 可核', '○ 未披露']),
    ('精密度', ['未披露 CV', '未披露', '100 fmol 处 RSD 5.65%/10.24%；1 fmol 处 21.52%/75.18%', '未披露'], ['○ 待补', '○ 未披露', '● AOAC 可核', '○ 未披露']),
    ('线性/量程', ['0.1–200 pmol，R²≥0.999', '未披露', '0–999,999 RLU；Pro R²≥0.99', '未披露'], ['◐ 宣称', '○ 未披露', '● 公开', '○ 未披露']),
    ('稳定性', ['4℃ 90%、25℃ 77%、37℃ 74%（7 天）；客户实测 73%', '无公开；易瑞体系 37℃ 7 天 40%', '拭子/试剂稳定性未公开', '要求 7 天下降≤15%、11 天≤25%'], ['◐ 单次', '◐ 客户实测', '○ 未披露', '● 客户要求']),
    ('仪器/读出', ['无自有仪器；96 孔板/手持式', '无仪器', 'Biolum 280 g/3.5 寸/10 秒；Pro 420 g/5 寸/10 秒/云端', '未公开；天隆海外代理'], ['—', '—', '● 公开', '◐ 部分']),
    ('检测体系', ['D-Luciferin + ATP/Mg²⁺；客户 buffer 可定制', '仅冻干酶粉；需客户自配', '一体化液态稳定拭子；试剂开放但建议配套拭子', '客户自有 buffer；16 µL/test；200 nmol 标液'], ['◐ 可定制', '○ 无应用', '● 公开', '◐ 客户口径']),
    ('认证/质量', ['ATP 系统认证未披露；COA/批间待补', '未披露 COA/批间', 'AOAC 022401；NMPA/FDA/CE 宣称', '上市公司；食品快检体系'], ['○ 待补', '○ 未披露', '● 可核', '● 公开']),
    ('价格（元/mg）', ['16.0 / 14.4 / 12.96（100 mg/500 mg/1 g）', '2,708（1 mg）/1,084.6（5 mg）/16.96（100 mg 内部报价）', '整机 1.8–3.0 万元/台；拭子/试剂未公开', '易瑞首单 100 元/mg（小批量项目价）'], ['● 目录', '◐ 价差大', '◐ 整机', '◐ 项目价']),
    ('数据完整度', ['2/5', '2/5', '4/5', '1/5'], ['◐', '◐', '●', '○']),
]

def comparison_table():
    head = '<tr><th style="width:14%">维度</th><th style="width:21.5%">宝锐</th><th style="width:21.5%">克拉玛尔</th><th style="width:21.5%">天隆</th><th style="width:21.5%">易瑞</th></tr>'
    rows = []
    for dim, vals, tags in comparison:
        cells = []
        for v, t in zip(vals, tags):
            cls = 'tag-blue'
            if t.startswith('○'): cls = 'tag-gray'
            elif t.startswith('◐'): cls = 'tag-amber'
            elif t.startswith('●'): cls = 'tag-green'
            cells.append(f'<td>{e(v)}<div class="mini {cls}">{e(t)}</div></td>')
        rows.append(f'<tr><td class="dim">{e(dim)}</td>{"".join(cells)}</tr>')
    return f'<table class="matrix"><caption>表 1  四者横向比较总表（按“原料—试剂/拭子—仪器—渠道”分层；● 公开可核 / ◐ 宣称或单次实验 / ○ 未披露）</caption><thead>{head}</thead><tbody>{"".join(rows)}</tbody></table>'

def lod_table():
    rows = []
    for i, x in enumerate(D['lod']):
        ratio = 100.0 / x['fmol']
        rows.append(f'<tr><td>{e(x["label"])}</td><td class="num">{e(x["mol"])}</td><td class="num">{x["pmol"]:.4f}</td><td class="num">{x["fmol"]:g}</td><td class="num">{fmt_ratio(ratio)}</td><td>{e(x["note"])}</td></tr>')
    return f'''<table><caption>表 2  检测限的四套口径：先换算，再比较</caption>
<thead><tr><th style="width:27%">口径</th><th style="width:11%">mol</th><th style="width:11%">pmol</th><th style="width:11%">fmol</th><th style="width:13%">宝锐 0.1 pmol 的倍数</th><th>说明</th></tr></thead>
<tbody>{''.join(rows)}</tbody></table>'''

def precision_table():
    rows = []
    for i, lv in enumerate(D['precision']['levels_fmol']):
        dev = D['precision']['developer_rsd'][i]
        ind = D['precision']['independent_rsd'][i]
        note = '低端，检测到但定量不稳' if lv <= 5 else ('0.1 pmol；可定量区间' if lv == 100 else '')
        rows.append(f'<tr><td class="num">{lv:,}</td><td class="num">{dev:.2f}%</td><td class="num">{ind:.2f}%</td><td>{note}</td></tr>')
    return f'''<table><caption>表 3  天隆 AOAC 022401 纯分析物精密度（n=10）</caption>
<thead><tr><th style="width:20%">ATP（fmol/assay）</th><th style="width:20%">开发者 RSD</th><th style="width:20%">独立实验室 RSD</th><th>判读</th></tr></thead>
<tbody>{''.join(rows)}</tbody></table>'''

def stability_table():
    rows = []
    for i, t in enumerate(D['stability']['temps']):
        br = D['stability']['baorui'][i]; kl = D['stability']['klamar'][i]
        rows.append(f'<tr><td>{e(t)}</td><td class="num">{br}%</td><td class="num">{kl}%</td><td class="num">{br-85:+d} pp</td><td class="num">{kl-85:+d} pp</td></tr>')
    return f'''<table><caption>表 4  7 天加速稳定性：宝锐 vs 克拉玛尔（0 天=100%）</caption>
<thead><tr><th style="width:14%">温度</th><th style="width:21%">宝锐 E800</th><th style="width:21%">克拉玛尔</th><th style="width:22%">宝锐距 85% 目标</th><th style="width:22%">克拉玛尔距 85% 目标</th></tr></thead>
<tbody>{''.join(rows)}</tbody></table>'''

def price_table():
    rows = []
    for p in D['prices']:
        rows.append(f'<tr><td>{e(p["name"])}</td><td class="num">{p["price_per_mg"]:,.1f}</td><td class="num">{p["total"]:,.1f}</td><td>{e(p["note"])}</td></tr>')
    return f'''<table><caption>表 5  价格口径：目录价、批量价、项目价不能混用</caption>
<thead><tr><th style="width:30%">产品/规格</th><th style="width:16%">元/mg</th><th style="width:16%">元/包装</th><th>口径</th></tr></thead>
<tbody>{''.join(rows)}</tbody></table>'''

def case_timeline():
    return ''.join(f'<div class="tl-item"><div class="tl-date">{e(x["date"])}</div><div class="tl-body">{e(x["event"])}</div></div>' for x in D['case']['timeline'])

def source_list():
    items = []
    for s in D['sources']:
        extra = ''
        if s.get('pmid'): extra += f' ｜ PMID {e(s["pmid"])}'
        if s.get('doi'): extra += f' ｜ DOI {e(s["doi"])}'
        items.append(f'<li id="s{s["id"]}">{e(s["name"])}：{e(s["detail"])}{extra}<div class="src-url">{e(s["url"])}</div></li>')
    return f'<ol class="sources">{"".join(items)}</ol>'

def action_table():
    rows = [
        ('D0–D14', '签三方 NDA；拿到 Quickswab、Biolum 仪器、ATP 标液；书面确认天隆 LOD 口径（10⁻¹⁶ / 10⁻¹⁵ / AOAC 0.9 fmol 哪个是合同规格）', '刘新元', '王新旺、彭洪富、李红东', 'NDA、测试方法、样品清单'),
        ('D15–D30', '同体系 ATP 标准曲线：0、0.9、1、5、25、100、500、1,500 fmol；空白 n≥10；3σ/10σ', '王新旺', '研发、质量', 'LOD/LOQ 报告（mol/test 与 pM 双单位）'),
        ('D15–D45', '37℃ 7 天/11 天、25℃、4℃ 稳定性；客户 buffer 与 Quickswab 提取液', '研发', '质量、王新旺', '稳定性报告；7 天下降≤15% 判定'),
        ('D31–D45', 'Quickswab 兼容性：回收率、基质效应、批间 CV；与 Klamar/现用酶平行', '王新旺', '天隆试剂研发', '回收率 80–120%；CV≤15%'),
        ('D46–D60', '配方优化：酶量、buffer、luciferin、液体/冻干；20 mg/瓶冻干', '研发', '王新旺、质量', 'LOD≤1 fmol；7 天下降≤15%'),
        ('D46–D60', '易瑞数据授权：三方 NDA 或匿名化案例；确认易瑞 ATP 仪器型号与 buffer', '陈溪子', '刘新元、易瑞', '书面授权/匿名案例'),
        ('D61–D75', '小批量样品 20 mg/100 mg；批特异 COA、批间数据、出口资料', '生产/质量', '彭洪富', 'COA、批间 CV、样品交付'),
        ('D61–D90', '商务方案：双源、年度 3–5 g 参照、价格 12.96–16 元/mg、供货协议', '刘新元', '彭洪富、李红东', '试点订单/年度框架'),
        ('D90', '决策评审：继续 LOD 对标，或转“成本/稳定性第二来源”', '刘新元', '全体', 'Go/No-Go 纪要'),
    ]
    body = ''.join(f'<tr><td class="nowrap">{e(a)}</td><td>{e(b)}</td><td class="nowrap">{e(c)}</td><td>{e(d)}</td><td>{e(f)}</td></tr>' for a,b,c,d,f in rows)
    return f'''<table><caption>表 6  90 天行动清单（主责唯一；验收口径写死）</caption>
<thead><tr><th style="width:9%">时间</th><th style="width:43%">动作</th><th style="width:9%">主责</th><th style="width:16%">协同</th><th>验收/输出</th></tr></thead>
<tbody>{body}</tbody></table>'''

def risk_table():
    rows = [
        ('检测限差距', '宝锐 0.1 pmol = 100 fmol；AOAC 系统 LOD 0.9 fmol，差 111×；对营销 10⁻¹⁶ 差 1,000×', '先做同体系复测；未达标不对外写“对标 10⁻¹⁶”；把目标写成 ≤1 fmol'),
        ('稳定性差距', '37℃ 7 天下降 26–27%，高于易瑞≤15%；11 天数据未出', '配方优化；若 7 天做不到，先谈 25℃/运输窗口，不承诺 37℃ 7 天'),
        ('易瑞数据边界', '易瑞数据含客户 buffer/项目信息；不能直接给天隆', '三方 NDA 或匿名化；只给结论，不给原始配方/价格'),
        ('天隆既有体系', 'AOAC 022401 已锁定 0.9 fmol 系统；换酶要重新验证', '不要求改仪器；做 Quickswab 酶的 second source，并行验证'),
        ('竞品价格不透明', 'Klamar 公开 1 mg 2,708 元、5 mg 5,423 元；内部 100 mg 16.96 元/mg；易瑞记录 100 元/mg', '先拿天隆实际采购价/年用量；不要用易瑞项目价推天隆'),
        ('自产 vs OEM 未知', '天隆有拭子专利，可能自产，也可能由 OEM 供货', '先确认 Quickswab 生产主体；若 OEM，同步开发 OEM'),
        ('宝锐数据缺口', '0.1 pmol 缺体积、空白 n、CV、加标回收；COA/批间待补', '补原始曲线与批间数据；没有原始数据不签“符合”'),
    ]
    body = ''.join(f'<tr><td>{e(a)}</td><td>{e(b)}</td><td>{e(c)}</td></tr>' for a,b,c in rows)
    return f'''<table><caption>表 7  主要风险与应对（风险必须能对应一个动作）</caption>
<thead><tr><th style="width:16%">风险</th><th style="width:42%">事实/依据</th><th>应对</th></tr></thead>
<tbody>{body}</tbody></table>'''

def strategy_cards():
    cards = [
        ('A', '技术对标', '目标：天隆 Quickswab 体系 ≤1×10⁻¹⁶ mol', '前提：同体系 LOD ≤0.9–1 fmol、CV≤10–15%、7 天下降≤15%。当前数据不支持；只在拿到数据后启动。', '高风险 · 不作为首选'),
        ('B', '第二来源', '目标：Quickswab 酶的 second source，系统 LOD 对齐 0.9 fmol', '用 2.7× 信号降酶量、73–74% 稳定性、液体/冻干剂型、12.96–16 元/mg 和国内供应切入；先小批量，后双源。', '推荐 · 主路径'),
        ('C', '渠道带动', '目标：借易瑞海外渠道形成天隆体系内的 field data', '易瑞已是天隆 Biolum 海外独家代理；若易瑞 ATP 试剂配套天隆仪器，E800 可在海外场景先跑；需三方 NDA，防渠道冲突。', '推荐 · 并行'),
        ('D', '整机/品牌', '目标：做 ATP 仪器或自有品牌', '不是宝锐当前能力；仪器+软件+注册+渠道都要重建，投入产出不匹配。', '不做'),
    ]
    return ''.join(f'''<div class="strategy {('primary' if k=='B' else '')}"><div class="strategy-top"><span class="strategy-k">{k}</span><span class="strategy-t">{e(t)}</span></div><div class="strategy-g">{e(g)}</div><p>{e(d)}</p><div class="strategy-b">{e(b)}</div></div>''' for k,t,g,d,b in cards)

# ---------- CSS ----------
CSS = r'''
:root{--navy:#0b1f3a;--blue:#1a56db;--sky:#0ea5e9;--bg:#f4f7fb;--card:#fff;--ink:#0f172a;--muted:#5d6b80;--line:#dbe4f0;--red:#b91c1c;--amber:#b45309;--green:#047857}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--bg);color:var(--ink);font-family:"Times New Roman","Songti SC","STSong","SimSun","宋体",serif;font-size:11pt;line-height:1.64}
.wrap{max-width:1180px;margin:0 auto;padding:0 24px 56px}
.hero{background:linear-gradient(135deg,#081a33 0%,#12356b 52%,#1a56db 100%);color:#fff;border-radius:0 0 18px 18px;padding:54px 48px 40px;position:relative;overflow:hidden}
.hero:before{content:"";position:absolute;width:420px;height:420px;border-radius:50%;right:-120px;top:-180px;background:rgba(14,165,233,.22)}
.hero:after{content:"";position:absolute;width:260px;height:260px;border-radius:50%;left:-80px;bottom:-160px;background:rgba(255,255,255,.08)}
.hero .eyebrow{font-size:9.5pt;letter-spacing:.18em;color:#9cc4ff;text-transform:uppercase;position:relative;z-index:1}
.hero h1{font-size:32pt;line-height:1.18;margin:12px 0 10px;font-weight:700;position:relative;z-index:1;max-width:930px}
.hero .sub{font-size:13.5pt;line-height:1.5;color:#dbeafe;max-width:860px;position:relative;z-index:1}
.hero .meta{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:26px;position:relative;z-index:1}
.hero .meta div{border-top:1px solid rgba(255,255,255,.28);padding-top:8px;font-size:9.5pt;color:#c7dbff}
.hero .meta b{display:block;color:#fff;font-size:10.5pt;margin-top:2px}
.hero .stamp{display:inline-block;margin-top:18px;border:1px solid rgba(255,255,255,.45);border-radius:999px;padding:4px 12px;font-size:9pt;color:#e0efff;position:relative;z-index:1}
.toc{display:flex;flex-wrap:wrap;gap:8px;margin:22px 0 4px;padding:12px 0;border-bottom:1px solid var(--line)}
.toc a{color:#244b82;text-decoration:none;font-size:9.5pt;border:1px solid #cfe0f4;background:#fff;border-radius:999px;padding:4px 10px}
.toc a:hover{background:#eaf2ff}
.kpis{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:12px;margin:22px 0 8px}
.kpi{background:#fff;border:1px solid var(--line);border-radius:12px;padding:14px 16px;box-shadow:0 1px 3px rgba(16,42,86,.06);min-width:0}
.kpi .n{font-size:22pt;line-height:1.1;color:var(--navy);font-weight:700}
.kpi .n small{font-size:10pt;font-weight:400;color:var(--muted)}
.kpi .l{font-size:9pt;color:var(--muted);margin-top:6px}
.kpi.red .n{color:var(--red)} .kpi.green .n{color:var(--green)} .kpi.amber .n{color:var(--amber)}
h2{font-size:19pt;line-height:1.3;border-left:6px solid var(--blue);padding-left:12px;margin:40px 0 14px;color:var(--navy)}
h3{font-size:13.5pt;color:var(--navy);margin:24px 0 8px}
h4{font-size:11.5pt;color:var(--navy);margin:16px 0 6px}
p{margin:8px 0}
.lead{font-size:12pt;color:#23344d}
.small{font-size:9.4pt;color:var(--muted);line-height:1.55}
.callout{border-left:5px solid var(--blue);background:#eef4ff;padding:14px 18px;border-radius:0 10px 10px 0;margin:14px 0}
.callout.warn{border-left-color:var(--red);background:#fff1f1}
.callout.amber{border-left-color:var(--amber);background:#fff8eb}
.callout.green{border-left-color:var(--green);background:#effaf5}
.callout b{color:var(--navy)}
.card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:18px 20px;box-shadow:0 1px 3px rgba(16,42,86,.06);margin:12px 0}
.grid2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
.grid3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}
.grid4{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}
.mini{display:inline-block;font-size:8pt;border-radius:999px;padding:1px 6px;margin-top:4px;letter-spacing:.02em}
.tag-blue{background:#e8f0ff;color:#174ea6}
.tag-green{background:#e8f7ef;color:#05603a}
.tag-amber{background:#fff4df;color:#8a4b00}
.tag-gray{background:#f0f3f7;color:#526070}
.tag-red{background:#fdebec;color:#a61b1b}
table{width:100%;border-collapse:collapse;background:#fff;table-layout:fixed;font-size:9.2pt;line-height:1.45;margin:10px 0 6px}
caption{caption-side:top;text-align:left;font-size:9pt;color:var(--muted);padding:4px 0 6px}
th{background:#0f2b52;color:#fff;padding:8px 7px;border:1px solid #274a7a;text-align:left;vertical-align:middle;font-weight:700}
td{padding:7px;border:1px solid var(--line);vertical-align:top;word-break:break-word}
tbody tr:nth-child(even) td{background:#f8fbff}
td.dim{font-weight:700;color:var(--navy);background:#f2f6fc}
td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.nowrap{white-space:nowrap}
.matrix td{font-size:8.8pt}
.matrix td.dim{font-size:9pt}
.fig{display:block;width:100%;height:auto;background:#fff;border:1px solid var(--line);border-radius:10px;margin:8px 0 4px}
.stack{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin:12px 0}
.layer{background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px;position:relative;min-width:0}
.layer:after{content:"→";position:absolute;right:-13px;top:50%;transform:translateY(-50%);color:#9db4d0;font-size:14pt}
.layer:last-child:after{display:none}
.layer .k{font-size:8.5pt;letter-spacing:.08em;color:var(--muted);text-transform:uppercase}
.layer .t{font-size:12pt;font-weight:700;color:var(--navy);margin:4px 0}
.layer .who{font-size:9.4pt;color:#334155;line-height:1.5}
.layer .lock{font-size:8.7pt;color:var(--muted);margin-top:6px}
.tl{position:relative;margin:12px 0 0 0;padding-left:26px}
.tl:before{content:"";position:absolute;left:8px;top:6px;bottom:6px;width:2px;background:#cfe0f4}
.tl-item{position:relative;margin:0 0 16px 0}
.tl-item:before{content:"";position:absolute;left:-22px;top:5px;width:10px;height:10px;border-radius:50%;background:var(--blue);border:2px solid #fff;box-shadow:0 0 0 2px #cfe0f4}
.tl-date{font-weight:700;color:var(--navy);font-size:10pt}
.tl-body{font-size:9.8pt;color:#334155;margin-top:2px}
.strategy{background:#fff;border:1px solid var(--line);border-radius:12px;padding:16px;min-width:0}
.strategy.primary{border:2px solid var(--blue);box-shadow:0 4px 16px rgba(26,86,219,.12)}
.strategy-top{display:flex;align-items:center;gap:8px}
.strategy-k{display:inline-flex;width:24px;height:24px;border-radius:50%;background:var(--navy);color:#fff;align-items:center;justify-content:center;font-size:10pt;font-weight:700;flex:none}
.strategy.primary .strategy-k{background:var(--blue)}
.strategy-t{font-size:12.5pt;font-weight:700;color:var(--navy)}
.strategy-g{font-size:9.2pt;color:var(--muted);margin:8px 0 6px}
.strategy p{font-size:9.4pt;color:#334155;margin:6px 0}
.strategy-b{font-size:9pt;font-weight:700;color:var(--blue);margin-top:8px}
.quad{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:12px 0}
.q{background:#fff;border:1px solid var(--line);border-radius:10px;padding:14px;min-width:0}
.q .qh{font-size:9pt;color:var(--muted);letter-spacing:.06em}
.q .qt{font-size:12pt;font-weight:700;color:var(--navy);margin:4px 0}
.q .qb{font-size:9.4pt;color:#334155}
.q.pri{border:2px solid var(--blue);background:#f7faff}
.priority-strip{display:flex;flex-wrap:wrap;align-items:center;gap:10px;background:#0f2b52;color:#fff;border-radius:10px;padding:12px 16px;margin:10px 0 4px;font-size:9.6pt}
.priority-strip b{color:#9cc4ff;letter-spacing:.08em;margin-right:6px}
.priority-strip span{background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.22);border-radius:999px;padding:3px 9px}
.priority-strip .arrow{background:none;border:0;padding:0;color:#9cc4ff}
.sources{font-size:8.5pt;color:#334155;line-height:1.55;padding-left:22px}
.sources li{margin:0 0 7px 0}
.src-url{font-size:7.8pt;color:#64748b;word-break:break-all;margin-top:1px}
.ref{font-size:7pt;color:var(--blue);vertical-align:super;margin-left:1px}
.footer-note{border-top:1px solid var(--line);margin-top:30px;padding-top:12px;font-size:8.8pt;color:var(--muted)}
.page-break{page-break-before:always}
.keep{break-inside:avoid;page-break-inside:avoid}
ul.tight{margin:6px 0 6px 20px;padding:0}
ul.tight li{margin:3px 0}
@media (max-width:980px){.kpis{grid-template-columns:repeat(2,minmax(0,1fr))}.grid3,.grid4,.stack{grid-template-columns:repeat(2,minmax(0,1fr))}.layer:after{display:none}.hero h1{font-size:25pt}}
@media (max-width:640px){.wrap{padding:0 12px 40px}.hero{padding:32px 20px 28px}.hero h1{font-size:21pt}.hero .sub{font-size:11.5pt}.hero .meta{grid-template-columns:1fr 1fr}.kpis{grid-template-columns:1fr}.grid2,.grid3,.grid4,.stack,.quad{grid-template-columns:1fr}.toc{gap:6px}.toc a{font-size:8.5pt}table{font-size:8.2pt}.matrix td{font-size:8pt}}
@media print{
  @page{size:A4;margin:12mm 9mm}
  body{background:#fff;font-size:9.2pt;line-height:1.5}
  .wrap{max-width:none;padding:0}
  .hero{border-radius:0;padding:24px 20px 20px}
  .hero h1{font-size:23pt}
  .hero .sub{font-size:10.5pt}
  .hero .meta{grid-template-columns:repeat(4,1fr)}
  .kpis{grid-template-columns:repeat(5,minmax(0,1fr));gap:6px}
  .kpi{padding:8px}
  .kpi .n{font-size:15pt}
  .kpi .l{font-size:7.4pt}
  .grid3{grid-template-columns:repeat(3,minmax(0,1fr))}
  .grid4{grid-template-columns:repeat(4,minmax(0,1fr))}
  .toc,.no-print{display:none}
  h2{font-size:15pt;margin:18px 0 8px;page-break-after:avoid}
  h3{font-size:11.5pt;page-break-after:avoid}
  .card,.callout,.kpi,.strategy,.q,.layer{break-inside:avoid;box-shadow:none}
  table{font-size:7.6pt;page-break-inside:auto}
  th,td{padding:4px 4px}
  tr{page-break-inside:avoid}
  .page-break{page-break-before:always}
  .sources{font-size:7pt}
}
'''

# ---------- HTML ----------
HTML = r'''<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ATP 荧光检测四方差：易瑞、天隆、宝锐、克拉玛尔</title>
<style>@@CSS@@</style>
</head>
<body>
<main class="wrap">

<section class="hero">
  <div class="eyebrow">BIORI BIOTECH · 营销中心 · 内部研究稿</div>
  <h1>ATP 荧光检测四方差：<br>易瑞、天隆、宝锐、克拉玛尔</h1>
  <div class="sub">精密度、检测限、仪器与检测体系横向比较，以及宝锐在易瑞项目之后切入天隆 ATP 需求的路径</div>
  <div class="meta">
    <div>报告日期<b>2026-11-06</b></div>
    <div>版本<b>V01（研究稿）</b></div>
    <div>覆盖产品<b>荧光素酶 / ATP 快检</b></div>
    <div>数据边界<b>公开资料 + 内部客户记录</b></div>
  </div>
  <div class="stamp">内部资料 · 含客户数据 · 禁止外发</div>
</section>

<nav class="toc">
  <a href="#s0">结论</a>
  <a href="#s1">角色边界</a>
  <a href="#s2">横向总表</a>
  <a href="#s3">检测限</a>
  <a href="#s4">精密度</a>
  <a href="#s5">稳定性</a>
  <a href="#s6">易瑞案例</a>
  <a href="#s7">天隆机会</a>
  <a href="#s8">撬动策略</a>
  <a href="#s9">90 天</a>
  <a href="#s10">风险与来源</a>
</nav>

<section id="s0">
<h2>一、结论：先分清层级，再谈对标</h2>
<div class="kpis">
  <div class="kpi red"><div class="n">0.9 <small>fmol</small></div><div class="l">天隆 Biolum + ATP 拭子的 AOAC 系统 LOD</div></div>
  <div class="kpi amber"><div class="n">0.1 <small>pmol</small></div><div class="l">宝锐 E800 的 LOD（3σ，96 孔板）</div></div>
  <div class="kpi red"><div class="n">111×</div><div class="l">宝锐 0.1 pmol 相对 AOAC 系统 LOD 的差距</div></div>
  <div class="kpi green"><div class="n">73–74%</div><div class="l">宝锐 37℃ 7 天活力保留；克拉玛尔 40%</div></div>
  <div class="kpi"><div class="n">3–5 <small>g/年</small></div><div class="l">易瑞 ATP 项目酶用量（客户口径）</div></div>
</div>

<div class="callout warn">
<b>第一判断：现有公开数据不能证明宝锐荧光素酶“对标”天隆 Biolum 的标称检测限。</b> 宝锐 E800 的 LOD 是 0.1 pmol ATP/管（3σ，96 孔板）；天隆的 AOAC 022401 系统 LOD 是 0.9 fmol，宝锐比它高约 111 倍。若拿营销页的 1×10⁻¹⁶ mol 比，差距是 1,000 倍。两者测试体系不同，不能直接拿“酶 LOD”压“仪器标称”。
</div>

<div class="grid2">
  <div class="card">
    <h3>易瑞的胜利是稳定性 + 价格，不是 LOD</h3>
    <p>易瑞项目里，E800 在 37℃ 7 天保留 73–74%，克拉玛尔 40%；E800 同酶量信号约 2.7 倍；报价从 200、150 降到 100 元/mg 后成交。客户买的是“比克拉玛尔稳、价格拉平”，不是“LOD 更低”。</p>
  </div>
  <div class="card">
    <h3>天隆项目要打“第二来源”</h3>
    <p>天隆已有 AOAC 系统，换酶要重新验证。宝锐的机会是给 Quickswab 做<strong>酶 + 配方 + 供应的第二来源</strong>：先同体系验证，再小批量，再双源；用易瑞的 ATP 试剂经验做配方证明，用天隆的 AOAC 方法做准入。</p>
  </div>
</div>
</section>

<section id="s1" class="page-break">
<h2>二、角色边界：四个名字，四层生意</h2>
<p class="lead">先把“谁卖什么、谁卡什么”说清。四者不是同层竞品；真正的竞争发生在“酶原料”和“Quickswab 试剂”两个不同的采购口。</p>
<div class="stack">
  <div class="layer"><div class="k">渠道 / 终端</div><div class="t">易瑞</div><div class="who">食品快检上市公司；天隆 Biolum 海外独家代理（除中国等少数国家）；ATP 试剂项目客户</div><div class="lock">卡：海外渠道、客户场景、双品牌</div></div>
  <div class="layer"><div class="k">仪器 / 读出</div><div class="t">天隆</div><div class="who">Biolum / Plus / Pro；10 秒读数；AOAC 022401；NMPA/FDA/CE 宣称</div><div class="lock">卡：仪器装机、AOAC 体系、软件</div></div>
  <div class="layer"><div class="k">试剂 / 拭子</div><div class="t">天隆 Quickswab</div><div class="who">一体化液态稳定拭子；试剂开放，但建议配套天隆拭子；专利保护拭子结构</div><div class="lock">卡：拭子结构、提取效率、批间</div></div>
  <div class="layer"><div class="k">酶原料</div><div class="t">宝锐 / 克拉玛尔</div><div class="who">宝锐 E800；克拉玛尔 13081130021；供试剂厂/拭子厂/客户自配</div><div class="lock">卡：比活、稳定性、COA、价格</div></div>
</div>

<div class="grid2">
  <div class="card">
    <h3>天隆的商业模式</h3>
    <p>整机公开中标价约 1.8–3.0 万元/台；仪器是一次性收入，<strong>拭子/试剂才是复购</strong>。天隆专利 CN203011835U 的背景明确写“现有 ATP 荧光检测拭子一般采购国外的，价格昂贵”，目标是结构简单、便宜、易推广。这说明天隆对<strong>耗材降本</strong>有真实动机。</p>
  </div>
  <div class="card">
    <h3>“试剂开放”不等于“酶可替换”</h3>
    <p>渠道资料写“试剂开放化，适合多个厂家的 ATP 检测试剂，建议使用天隆配套检测拭子”。开放的是试剂配方，锁的是拭子结构和提取体系。宝锐要进的不是“天隆的试剂采购”，而是<strong>Quickswab 的酶/配方供应</strong>；如果天隆的拭子由 OEM 生产，还要先找到那个 OEM。</p>
  </div>
</div>

<div class="callout amber">
<b>口径红线：</b>不要把“天隆官网 1×10⁻¹⁶ mol”当成 Quickswab 系统 LOD；也不要把“宝锐 0.1 pmol/管”直接换算成“天隆体系 0.1 pmol”。前者是仪器/标称口径，后者是 96 孔板酶体系；两者之间隔着拭子提取、buffer、样本体积、光路和积分时间。
</div>
</section>

<section id="s2" class="page-break">
<h2>三、横向总表：把“能比的”和“不能比的”分开</h2>
<p class="lead">这张表只做一件事：把四家的公开/内部数据放到同一组维度下，并标出证据强度。空白不是“没有”，是“未披露/待补”。</p>
@@COMPARISON@@
<p class="small">注：AOAC 022401 证书标注签发 2024-03-04、有效期至 2024-12-31；续期状态需查 AOAC 最新列表。本报告引用证书中的方法、LOD 与精密度数据，不把证书状态当作当前有效认证。</p>
<p class="small">证据标记：● 公开可核（AOAC、官网、上市公司公告）；◐ 厂商宣称或单次实验；○ 未披露/待补。表中“—”表示该主体没有这类产品，不是数据缺失。</p>
</section>

<section id="s3" class="page-break">
<h2>四、检测限：天隆的 10⁻¹⁶、10⁻¹⁵ 与 0.9 fmol</h2>
<p class="lead">同一个天隆，官网出现两套标称；AOAC 证书给出第三个数。先把三套口径换算到 fmol，再谈宝锐差多少。</p>
@@LODSVG@@
@@LODTABLE@@
<div class="grid2">
  <div class="callout">
    <b>采用 0.9 fmol 作为系统基准。</b> AOAC 022401 的适用性写的是 Biolum + ATP Test Swab 在不锈钢表面的 LOD 0.9 fmol；这是有独立实验室、n=10、纯分析物与基质数据的系统级结果。营销页的 1×10⁻¹⁶ mol 更像仪器灵敏度，不应直接作为试剂准入线。@@REF6@@
  </div>
  <div class="callout warn">
    <b>宝锐差 111 倍，不是“接近”。</b> 0.1 pmol = 100 fmol；100 / 0.9 = 111。若按营销页 1×10⁻¹⁶ = 0.1 fmol，差距是 1,000 倍。现有资料没有同体系复测，不能写“达到 10⁻¹⁶”。@@REF1@@
  </div>
</div>
<div class="callout amber">
<b>还有一个容易漏掉的口径：</b>宝锐的 0.1 pmol 没有写“每管多少 µL”、空白 n 数、CV、加标回收。按 100 µL 折算约 1 nM；天隆 AOAC 0.9 fmol/assay 若按 100 µL 折算约 9 pM。绝对量差 111 倍，浓度倍数会随体积变化，但量级不会变。@@REF6@@
</div>
</section>

<section id="s4" class="page-break">
<h2>五、精密度：低端“能检出”不等于“能定量”</h2>
<p class="lead">AOAC 022401 的纯分析物数据把“检测限”和“定量限”拆得很清楚：0.9 fmol 是 LOD，但 1 fmol 处独立实验室 RSD 75%、5 fmol 处 66%；到 100 fmol（0.1 pmol）才降到 10.24%。</p>
@@PRECSVG@@
@@PRECTABLE@@
<div class="grid2">
  <div class="callout">
    <b>天隆在 100 fmol 处是可定量的。</b> 开发者 RSD 5.65%、独立实验室 10.24%，与 Biolum Pro 标称“1×10⁻¹³ mol CV<6%”相互印证。@@REF5@@ @@REF6@@
  </div>
  <div class="callout warn">
    <b>宝锐缺的是这一组数据。</b> 宝锐只给“0.1 pmol/管（3σ）”，没有 CV、没有空白 n 数、没有加标回收。LOD 是“能检出”的门槛，不等于“批间稳定定量”的门槛；天隆的 AOAC 数据恰好说明两者差一个数量级。@@REF1@@
  </div>
</div>
<div class="callout amber">
<b>对销售的含义：</b>如果客户问“你们 0.1 pmol 准不准”，不能只回“3σ”。要能给出 0.1 pmol 处的 n=10、CV、回收率；没有这组数据，客户会拿天隆的 5.65%/10.24% 对比，宝锐会被动。
</div>
</section>

<section id="s5" class="page-break">
<h2>六、稳定性：易瑞为什么选宝锐，以及它还没跨过的线</h2>
<p class="lead">易瑞项目的数据只证明一件事：在客户 buffer 里，宝锐 37℃ 7 天比克拉玛尔稳。它不证明宝锐达到了易瑞的 7 天下降≤15% 目标。</p>
@@STABSVG@@
@@STABTABLE@@
<div class="grid2">
  <div class="callout green">
    <b>相对优势成立。</b> 37℃ 7 天：宝锐 73–74%，克拉玛尔 40%，差距 33–34 个百分点；25℃：77% vs 55%；4℃：90% vs 88%。同酶量信号约 2.7 倍。对易瑞这种要做常温运输/现场使用的 ATP 试剂，这是可感知的差异。@@REF1@@ @@REF22@@
  </div>
  <div class="callout warn">
    <b>绝对门槛未过。</b> 易瑞要求 37℃ 7 天下降≤15%（保留≥85%）、11 天≤25%（保留≥75%）。宝锐 7 天保留 73–74%，下降 26–27%，7 天这一关已经超出；11 天数据还没有。@@REF22@@
  </div>
</div>
<div class="callout amber">
<b>技术审计点：</b>如果按一级失活估算，37℃ 7 天保留 74% 对应半衰期约 16 天；文献里工程化荧光素酶在 37℃ 的半衰期约 11.5 小时，野生型约 3 分钟。这个差距说明易瑞测的是“配方液/信号保留”，不是纯酶的热稳定性；对外不能把 74% 直接说成“酶本身耐 37℃ 7 天”。@@REF17@@ @@REF18@@
</div>
</section>

<section id="s6" class="page-break">
<h2>七、易瑞案例：一场“稳定性 + 价格”的胜出</h2>
<p class="lead">易瑞不是被“LOD”打动的。把时间线摊开，能看到宝锐真正赢在哪、还差在哪。</p>
<div class="card">
<div class="tl">@@TIMELINE@@</div>
</div>
<div class="grid2">
  <div class="card">
    <h3>已经证明的</h3>
    <ul class="tight">
      <li>E800 在易瑞客户 buffer 中 37℃ 7 天保留 73%，克拉玛尔 40%；</li>
      <li>同酶量信号约 2.7 倍，客户可减少酶量；</li>
      <li>价格从 200、150 谈到 100 元/mg 后成交，和克拉玛尔拉平；</li>
      <li>2026-11-04 首单 100 mg / 10,000 元，进入小批量商业采购。</li>
    </ul>
  </div>
  <div class="card">
    <h3>还没证明的</h3>
    <ul class="tight">
      <li>37℃ 7 天下降 26–27%，未达到易瑞≤15%；</li>
      <li>没有同体系 LOD、精密度、加标回收；</li>
      <li>没有易瑞仪器型号、buffer 成分、拭子/提取细节；</li>
      <li>“pk 掉克拉玛尔”是小批量选型胜出，不是全量替代。</li>
    </ul>
  </div>
</div>
<div class="callout warn">
<b>价格口径要单独说：</b>易瑞首单 100 元/mg 是项目小批量价；宝锐标准目录 100 mg 为 16 元/mg。两者相差 6.25 倍。天隆是 3–5 g/年量级的客户，不能用易瑞的 100 元/mg 去报价；反过来，也不能拿 16 元/mg 去解释易瑞项目为什么值 100 元/mg。两套价格要分开管理。@@REF3@@ @@REF22@@
</div>
<div class="callout">
<b>可迁移的资产：</b>配方经验（客户 buffer 中的稳定性）、应用场景（餐具/环境洁净度）、竞品替换打法（先赠样、再自测、再小批量）、以及一个可匿名化的“海外食品快检客户”案例。不可直接迁移的是：易瑞的 buffer、仪器型号、价格、项目数据——这些要授权或匿名化。
</div>
</section>

<section id="s7" class="page-break">
<h2>八、天隆机会：仪器已就位，缺的是第二来源</h2>
<p class="lead">天隆的 AOAC 系统已经证明 0.9 fmol 可行；它不需要宝锐来“证明 ATP 能测”，它需要一条可切换、可降本、可保障供应的酶/配方来源。</p>
<div class="grid3">
  <div class="card"><h4>产品线</h4><p>Biolum、Biolum Plus、Biolum Pro；Quickswab A010–A036（水样/表面/冻干/CleanSure）。10 秒读数，整机 280–420 g，USB/蓝牙/WiFi/云端。</p></div>
  <div class="card"><h4>认证与价格</h4><p>AOAC 022401、NMPA、FDA/CE 宣称；整机公开中标 1.8–3.0 万元/台。仪器是一次性收入，拭子/试剂是复购。</p></div>
  <div class="card"><h4>渠道</h4><p>2025-09-04 与易瑞签约，易瑞获 Biolum 海外独家代理（除中国等少数国家），双品牌做海外食品安全。易瑞既是客户，也是天隆的渠道。</p></div>
</div>

<h3>8.1 市场侧证据：需求在，但口径很粗</h3>
<div class="grid3">
  <div class="card"><div class="small">中国快检市场（宽口径）</div><div class="kpi" style="padding:0;border:0;box-shadow:none"><div class="n" style="font-size:18pt">13.65 <small>亿美元</small></div><div class="l">2025 年；2030 年 23.88 亿美元，CAGR 11.8%</div></div><div class="small">MarketsandMarkets；含食品安全快检整体，不是 ATP 专项。@@REF19@@</div></div>
  <div class="card"><div class="small">全球食品工厂 ATP 卫生监测</div><div class="kpi" style="padding:0;border:0;box-shadow:none"><div class="n" style="font-size:18pt">5.15 <small>亿美元</small></div><div class="l">2025 年；2034 年 11.02 亿美元，CAGR 8.8%</div></div><div class="small">Growth Market Reports；全球口径，不是中国专项。@@REF20@@</div></div>
  <div class="card"><div class="small">天隆整机公开中标价</div><div class="kpi" style="padding:0;border:0;box-shadow:none"><div class="n" style="font-size:18pt">1.8–3.0 <small>万元/台</small></div><div class="l">18,000 元、19,700 元、2.9–3.0 万元</div></div><div class="small">公开中标/价格页；渠道价与成本未公开。@@REF21@@</div></div>
</div>
<div class="callout amber">
<b>市场口径提示：</b>三个数不能相加，也不能用来推算宝锐可及市场；中国 ATP 专项没有统一公开口径。本报告只用它们判断“需求存在、天隆有装机、耗材有复购”，不做市场容量承诺。
</div>

<div class="keep">
<h3>8.2 采购与决策链：技术、项目、采购要分开打</h3>
<table>
<caption>表 8  天隆 ATP 项目的关系人（内部记录 + 公开信息；职务以对方最新名片为准）</caption>
<thead><tr><th style="width:14%">角色</th><th style="width:12%">姓名</th><th style="width:38%">作用</th><th>证据/状态</th></tr></thead>
<tbody>
<tr><td>技术负责人</td><td>李红东</td><td>用户提供的 ATP 项目负责人；公开信息显示其现任科华（西安）生物副总经理，曾任天隆试剂研发总监/监事</td><td>用户口径 + 公开报道；是否仍兼天隆 ATP 职务待核实</td></tr>
<tr><td>项目接口</td><td>王新旺</td><td>天隆项目技术/项目推进；易瑞与天隆两边都有接触</td><td>内部活动记录；负责范围待确认</td></tr>
<tr><td>商务/采购</td><td>彭洪富</td><td>商务/采购接口；用户明确通过其开发 ATP 项目</td><td>用户口径；职务待确认</td></tr>
<tr><td>客户关系</td><td>王昕伟 / 刘子研 / 黄明月</td><td>天隆商机、西安华伟、苏州天隆/委托生产等既有关系</td><td>内部商机与订单记录；ATP 项目未立项</td></tr>
<tr><td>易瑞接口</td><td>陈溪子</td><td>易瑞荧光素酶替换项目销售；可协调易瑞数据授权</td><td>内部商机/订单记录</td></tr>
</tbody>
</table>
</div>

<h3>8.3 竞争位置：克拉玛尔是“目录供应商”，不是“系统厂家”</h3>
<div class="grid2">
  <div class="card"><h4>克拉玛尔的强项</h4><p>公开目录可查、1 mg/5 mg 小包装、≥98% SDS-PAGE、15–30×10⁶ 光单位/mg；对研发小试方便。易瑞项目里 37℃ 7 天 40%，说明其配方稳定性一般，但价格可以压到 100 元/mg 以内。</p></div>
  <div class="card"><h4>克拉玛尔的弱项</h4><p>没有 ATP 系统 LOD、精密度、批间 COA、应用验证；没有本地技术服务和配方定制。对天隆这种要 AOAC 体系、批间稳定、出口资料的客户，目录供应商的短板会被放大。</p></div>
</div>
<div class="callout green">
<b>宝锐的切入口：</b>不是“比克拉玛尔更便宜”，而是“比目录供应商更懂体系”：能在天隆 Quickswab 的 buffer/拭子/提取体系里做配方，能给批特异 COA，能配合出口资料，能在 37℃ 稳定性上做出差异。价格只是敲门，体系能力才是留客。
</div>
</section>

<section id="s8">
<h2>九、撬动策略：四条路，优先 B + C</h2>
<p class="lead">“撬动天隆”不是一句话，是四条路的取舍。A 是技术对标，B 是第二来源，C 是渠道带动，D 是整机/品牌。宝锐当前的资源与数据，先把 B 做成，再用 C 放大；A 只在同体系数据达标后启动。</p>
<div class="grid2">@@STRATEGY@@</div>
<div class="priority-strip"><b>优先顺序</b><span>B 第二来源（主路径）</span><span class="arrow">→</span><span>C 渠道带动（并行）</span><span class="arrow">→</span><span>A 技术对标（条件启动）</span><span class="arrow">→</span><span>D 不做</span></div>

<h3>9.1 给天隆的价值主张</h3>
<div class="callout">
<b>“同样 0.9 fmol 的系统目标，宝锐用更低的酶量、更稳的 37℃ 活性、更灵活的液体/冻干剂型，给 Quickswab 做一条可切换的第二来源；先并行验证，再小批量，再双源。”</b>
</div>
<ul class="tight">
  <li><b>降本：</b>同酶量信号约 2.7 倍，达到同等亮度所需酶量约 37%；标准目录 12.96–16 元/mg，对比克拉玛尔小包装 1,085 元/mg、默克 37–47 元/mg。</li>
  <li><b>稳供：</b>国内生产、液体/冻干两种剂型、可按项目定制 buffer；比目录型供应商的交期和批间沟通更可控。</li>
  <li><b>稳定性：</b>37℃ 7 天 73–74% vs 克拉玛尔 40%；但要注意易瑞 15% 目标还没达到，不能把“相对优势”说成“绝对达标”。</li>
  <li><b>出海：</b>易瑞是天隆海外独家代理；如果易瑞的 ATP 试剂用 E800，天隆可在海外双品牌场景先看到 field data。</li>
</ul>

<h3>9.2 必须先证明的四件事</h3>
<table>
<caption>表 9  天隆准入的四个硬指标（没有数据就不承诺）</caption>
<thead><tr><th style="width:18%">指标</th><th style="width:24%">目标</th><th style="width:30%">测试方法</th><th>当前状态</th></tr></thead>
<tbody>
<tr><td>系统 LOD</td><td>≤0.9–1 fmol / test</td><td>Biolum + Quickswab；ATP 0、0.9、1、5、25、100、500、1,500 fmol；空白 n≥10；3σ/10σ</td><td>未测；宝锐现有 0.1 pmol 是 96 孔板数据</td></tr>
<tr><td>精密度</td><td>100 fmol 处 CV≤10–15%</td><td>n=10，批内/批间；与 Klamar/现用酶平行</td><td>未披露；天隆 AOAC 为 5.65%/10.24%</td></tr>
<tr><td>稳定性</td><td>37℃ 7 天下降≤15%；11 天≤25%</td><td>客户 buffer；0/2/4/7/11 天；0 天=100%</td><td>7 天 73–74%，下降 26–27%；11 天未出</td></tr>
<tr><td>体系兼容</td><td>回收率 80–120%；CV≤15%</td><td>Quickswab 提取液、不锈钢/食品基质、与现用酶平行</td><td>未测；易瑞 buffer 数据不能直接替代</td></tr>
</tbody>
</table>
</section>

<section id="s9">
<h2>十、90 天：先拿数据，再谈商务</h2>
<p class="lead">天隆项目最大的风险不是竞争，是“没有同体系数据就承诺”。90 天的顺序：NDA/样品 → 同体系验证 → 配方优化 → 小批量/双源。每个节点都有验收，不通过就转 B 方案的成本/稳定性牌。</p>
@@ACTIONS@@
<div class="callout green">
<b>Go/No-Go 判据：</b>D60 前拿到“LOD≤1 fmol、100 fmol CV≤15%、37℃ 7 天下降≤15%、Quickswab 回收率 80–120%”四项，进入小批量；任何一项不达标，不在天隆项目上承诺 LOD，改谈“第二来源/成本/稳定性/供应”，并把 37℃ 目标降到 25℃ 或运输窗口。
</div>
</section>

<section id="s10">
<h2>十一、风险、待确认与来源</h2>
<div class="card">
<h3>第一次技术交流要问的五个问题</h3>
<ol class="tight">
  <li>合同规格到底是 1×10⁻¹⁶、1×10⁻¹⁵，还是 AOAC 0.9 fmol？测试体积、空白 n 数、积分时间怎么定？</li>
  <li>Quickswab 是自产还是 OEM？酶是自配还是外购？如果 OEM，生产主体是谁？</li>
  <li>现用酶供应商是谁、年用量多少、采购价和批间 COA 能否给样品？</li>
  <li>是否接受双源/小批量并行验证？谁签字（技术、采购、质量）？</li>
  <li>出口/海外双品牌场景是否可以用易瑞的 field data？需要什么授权？</li>
</ol>
</div>
@@RISKS@@
<h3>11.1 我不确定的五个点</h3>
<ol class="tight">
  <li><b>天隆的合同规格到底是哪一个：</b>1×10⁻¹⁶、1×10⁻¹⁵，还是 AOAC 0.9 fmol？这决定宝锐要补多少；需天隆书面测试方法。</li>
  <li><b>Quickswab 是自产还是 OEM：</b>天隆有拭子专利，但实际生产主体未公开；若是 OEM，宝锐要同时开发 OEM，路径不同。</li>
  <li><b>克拉玛尔在天隆的实际份额/价格：</b>公开资料不能证明“天隆从克拉玛尔采购”；易瑞项目里的 40% 也不能外推到天隆。</li>
  <li><b>易瑞 ATP 项目的仪器与 buffer：</b>内部记录没有型号/配方；如果易瑞用的是自有仪器，E800 数据只能证明配方能力，不能直接迁移到 Biolum。</li>
  <li><b>宝锐 0.1 pmol 的原始数据：</b>缺体积、空白 n、CV、加标回收；没有原始曲线，不能作为天隆准入证据。</li>
</ol>

<h3>11.2 来源</h3>
@@SOURCES@@

<div class="footer-note">
<p><b>报告口径：</b>客户数据来自内部 ERP/活动记录；公开数据来自官网、AOAC 证书、上市公司公告、学术文献与公开中标页。所有“判断”均已尽量落到数据或原话；未能核实的内容标“待确认”，不按经验补数。</p>
<p><b>使用边界：</b>本报告为内部研究稿，含易瑞客户数据与价格，禁止外发；对天隆/易瑞沟通时，按“匿名化案例/三方 NDA”处理。报告中的 LOD/精密度均为不同测试体系，不能直接互换算为合同指标。</p>
<p>宝锐生物 ｜ 营销中心 销售管理 ｜ 2026-11-06 ｜ V01</p>
</div>
</section>

</main>
</body>
</html>'''

# ---------- assemble ----------
html = HTML.replace('@@CSS@@', CSS)
html = html.replace('@@COMPARISON@@', comparison_table())
html = html.replace('@@LODSVG@@', svg_lod())
html = html.replace('@@LODTABLE@@', lod_table())
html = html.replace('@@PRECSVG@@', svg_precision())
html = html.replace('@@PRECTABLE@@', precision_table())
html = html.replace('@@STABSVG@@', svg_stability())
html = html.replace('@@STABTABLE@@', stability_table())
html = html.replace('@@TIMELINE@@', case_timeline())
html = html.replace('@@STRATEGY@@', strategy_cards())
html = html.replace('@@ACTIONS@@', action_table())
html = html.replace('@@RISKS@@', risk_table())
html = html.replace('@@SOURCES@@', source_list())
# refs placeholders: replace @@REF6@@ etc.
import re
html = re.sub(r'@@REF(\d+)@@', lambda m: ref(int(m.group(1))), html)
assert '@@' not in html, 'unreplaced placeholder'
assert html.count('</body>') == 1, 'body count'
assert '<title' not in re.findall(r'<svg.*?</svg>', html, re.S)[0], 'svg title'
assert '<br' not in re.findall(r'<svg.*?</svg>', html, re.S)[0], 'svg br'
# brand inject
html = BK.inject(html, theme='dark', tag='营销中心 · 销售管理', footer_extra='内部研究稿 · 含客户数据 · 禁止外发 · 2026-11-06')
assert all(BK.verify(html).values()), BK.verify(html)
os.makedirs(os.path.dirname(OUT_HTML), exist_ok=True)
open(OUT_HTML, 'w', encoding='utf-8').write(html)
print('WROTE', OUT_HTML)
print('BYTES', len(html.encode('utf-8')))
print('BRAND', BK.verify(html))
