#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LAMP × 熔解曲线法 可行性精读页 —— 自包含 HTML 生成器
================================================================
数据来源：2026-10-14 晚的技术取证（文献 + 专利 + 竞品一手页面 + 我方产品线）
输入：无（内容内联，全部为本轮取证的既有结论）
输出：知识沉淀/LAMP熔解曲线可行性_精读页_20261015.html
规范：baoyu-brand-standard（Logo+slogan）/ baoyu-html-inline-svg-figures（SVG 守卫）
      / baoyu-selfcontained-html-delivery（零外链 + 表格增强）
重跑：改内容 → 直接重跑本脚本，不手改产物。
"""
import os
import re
import sys
from datetime import datetime

OUT_DIR = os.path.expanduser('~/Desktop/Hermes输出-工作类/知识沉淀')
OUT = os.path.join(OUT_DIR, 'LAMP熔解曲线可行性_精读页_20261015.html')
NOW = datetime.now().strftime('%Y-%m-%d %H:%M')

sys.path.insert(0, os.path.expanduser('~/Desktop/Hermes输出-工作类/脚本'))
import brand_kit as BK  # noqa: E402

# ══════════════════════════════════════════════════════════════
# 一、内容数据
# ══════════════════════════════════════════════════════════════

KPI = [
    ('8', '条', '学术证据', '2018–2025，含 1 条反例（分辨率边界）'),
    ('5', '件', '活跃专利', '2023-11 起，集中在熔解曲线做 LAMP 多重'),
    ('0', '家', '竞品原料专供', '翌圣/全式金/NEB/Thermo 均无“LAMP+熔解”体系'),
    ('3/3', '', '已获证熔解产品', '全部是 PCR 体系，无 LAMP 体系'),
    ('0.2', '℃', '单通道可分辨 ΔTm', 'LAMP + HRM 模式识别实测下限'),
    ('1', '层', '我方差距', '只差饱和染料兼容 buffer + 探针熔解设计'),
]

# 学术证据表：断言 / 定量证据 / 出处 / 年份 / 强度
EVIDENCE = [
    ('LAMP-MC 可做 SNP 基因分型', '5 个 SNP，与 TaqMan 一致性 kappa > 0.80（其中 1 个仅血样达 0.61）',
     'Biotechnol Rep 2020 · PMC7240324', '2020', '直接证据', 'ok'),
    ('⚠ 分辨率有硬边界（反例）', '乳糖酶持久性 5 个变异体中，-13907C>G 与 -13915T>G 熔解曲线不可区分',
     'Clin Chim Acta 2018 · 29596814', '2018', '直接反例', 'bad'),
    ('熔解曲线可区分近缘病原', '溶组织内阿米巴 Tm 84–86 ℃ vs 蓝氏贾第鞭毛虫 Tm 88–90 ℃；灵敏度 1 fg',
     'Lett Appl Microbiol 2023 · 37740570', '2023', '直接证据', 'ok'),
    ('多重 LAMP 按 Tm 分型病毒', 'HAdV-B/C/E 各按自身 Tm 分型；331 份鼻拭子与 qPCR 一致率 90–94%，与目视法一致 99%',
     'IJMS 2024 · PMC11241107', '2024', '直接证据', 'ok'),
    ('单通道多重化的分辨率上限', 'LAMP + HRM 模式识别可分辨 ΔTm ≈ 0.2 ℃（10–10⁸ copies 区间）',
     'Chem Commun 2019 · 30734782', '2019', '直接证据', 'ok'),
    ('熔解可判别特异 / 非特异扩增', '数字 LAMP + HRM 单分子分析，约 120 万个扩增事件',
     'NAR 2020 · PMC7144905', '2020', '直接证据', 'ok'),
    ('实时 RT-LAMP 用熔解验证特异性', '检出 15 copies/反应；熔解温度分析确认扩增特异性（262 份样本）',
     'Rev Inst Med Trop Sao Paulo 2025 · PMC12204207', '2025', '直接证据', 'ok'),
    ('2025 现状：Tm 多重化被认为是经济路线', '三重 RT-LAMP，论文明确表述“基于熔解温度的多重化简单且经济”',
     'Biomethods 2025 · bpaf010', '2025', '趋势判断', 'info'),
]

# 专利时间线：优先权日 / 号 / 主题
PATENTS = [
    ('2023-11-01', 'WO2025091343A1 / CN121311601A', '核酸扩增产物检测方法、引物组合物及试剂盒',
     '摘要明示 LAMP 常规检测机制难以多重，落到熔解与退火曲线分析'),
    ('2024-02-01', 'WO2024023510A1', 'LAMP 检测单核苷酸多态性（SNP）的方法与试剂盒', '分型方向布局'),
    ('2025-05-08', 'WO2025161028A1', '引物及其在 LAMP 扩增产物检测中的应用', 'LAMP 产物检测端布局'),
    ('2025-08-07', 'JP7737474B2', '使用多个检测温度的多个靶标核酸检测', '多温度（熔解）分辨多靶标'),
    ('2026-03-20', 'CN121700046A', 'RNA 反转录结合焦磷酸水解激活 PCR 扩增的熔解曲线检测法', '熔解曲线检测法持续有新申请'),
]

# 市场「主流读出方式」表：页面 / 原文口径 / 是否列熔解曲线
MAINSTREAM = [
    ('赛默飞 Thermo（官网等温核酸扩增表）', 'LAMP 检测方法：荧光法、比色法、浊度法、侧流法', '未列', 'bad'),
    ('全式金 TransGen（等温扩增技术系列页）', '染色检测法（钙黄绿素 / SYBR Green / 羟基萘酚蓝）、免疫试纸条法、实时荧光法、荧光探针法', '未列', 'bad'),
    ('宝锐生物（官网 2026-01 LAMP 新品页）', '终点法（琼脂糖凝胶电泳、比浊法、目测法）+ 实时荧光法（染料法、探针法）', '未列', 'bad'),
]

# 竞品原料矩阵：公司 / 荧光染料法 / 显色-pH 法 / 探针法 / 可冻干 / LAMP+熔解专供
MATRIX = [
    ('翌圣 Yeasen', '有（RT-LAMP 染料法 16729ES）', '有（pH 指示剂法 13906ES / 可冻干 13920ES）', '—', '有（16730ES、Bst 14405ES）', '无'),
    ('全式金 TransGen', '有（Bst II / III，兼容 dUTP-UDG）', '有（染色法：钙黄绿素 / 羟基萘酚蓝）', '有（探针法）', '未列', '无'),
    ('NEB', '有（WarmStart LAMP Kit）', '—', '有', '未列', '无'),
    ('赛默飞 Thermo', '有（SuperScript IV RT-LAMP 预混液）', '有（比色法）', '有', '组分可冻干（预混液仅含甘油）', '无'),
    ('宝锐生物（我方）', '有（LAMP / RT-LAMP Premix）', '有（Colorimetric LAMP / RT-LAMP Kit RY）', '有（LAMP / RT-LAMP Premix Probe）', '有（Bst 3.0 HS 含可冻干版）', '无 ← 缺口'),
]

# 已获证熔解曲线法产品：产品 / 方法学 / 体系 / 状态
CERTIFIED = [
    ('致善生物 MMCA 系列', '荧光 PCR 熔解曲线法', 'PCR', '公开披露口径：同类产品中首个取得三类医疗器械注册证'),
    ('苏州淦江生物 SMN1 检测试剂盒', '荧光 PCR 熔解曲线法', 'PCR', '受理号 CSZ2500221，审评报告载明熔解 40–80 ℃、升温 0.04 ℃/s'),
    ('BioFire FilmArray ME', '封闭巢式多重 PCR 熔解曲线法', 'PCR（巢式）', '2022 年获批，一次检测脑脊液中 14 种病原'),
]

# 三道坎
OBSTACLES = [
    ('① 仪器', 'LAMP 的卖点是“恒温水浴 / 普通 PCR 仪即可”',
     ['熔解 / HRM 需要高分辨温控与光学：可做 HRM 的机型集中在 QuantStudio 系列、LightCycler 480、Rotor-Gene 等高端平台',
      '普通 qPCR 仪的“熔解曲线”功能 ≠ HRM 级温控，“有熔解曲线”不能直接等同于“能做熔解分型”',
      '结论：客户群从“现场快检”收窄为“已有 qPCR / HRM 装机的实验室”'],
     '受影响最大的一面：直接抵消 LAMP 的低成本设备优势'),
    ('② 产物性质', 'LAMP 产物是花椰菜状多聚体（多种茎环结构）',
     ['与 PCR 的线性扩增子相比，染料法熔解峰更宽、更易出现多峰，分型分辨率下降',
      '文献里的两条绕行路线：挑序列差异足够大的靶标；或改走探针熔解（需专门设计引物 / 环区探针，并为探针提供单链靶标）',
      '已有实测边界：部分近缘变异体熔解曲线不可区分（见反例行）'],
     '决定“能做”与“能稳定分型”之间的落差'),
    ('③ 原料', '熔解 / HRM 必须使用饱和染料',
     ['EvaGreen / LCGreen 一类饱和染料才支持 HRM；SYBR Green I 因“染料重分布”不能用于 HRM',
      '客户若当前用 SYBR 法做 LAMP，改熔解分型必须换整套染料体系',
      '熔解曲线给的是分型 / 多重 / 特异性确认，不是灵敏度提升；开管操作仍有气溶胶污染风险'],
     '最容易被漏掉的一步，也是我方最可发力的原料位'),
]

# 我方产品线现状：业务线 / 现有体系 / 读出方式 / 是否支持熔解曲线
OURS = [
    ('LAMP 酶', 'Bst 2.0 / Bst 2.0 HS / Bst 3.0 HS（适配体封闭，含可冻干版）', '—', '否', 'warn'),
    ('LAMP 试剂', 'Colorimetric LAMP / RT-LAMP Kit（RY）', '显色法（肉眼判读）', '否', 'warn'),
    ('LAMP 试剂', 'LAMP / RT-LAMP Premix（Probe）', '实时荧光探针法', '否', 'warn'),
    ('LAMP 缓冲', '2×LAMP Premix Buffer（RY）/ Ⅱ、2×RT-LAMP Premix Buffer Ⅱ', '—', '否', 'warn'),
    ('熔解曲线线', 'FM0301-042901（Robustart Melt Multiplex Premix）', '荧光 PCR 熔解曲线（MMCA 路线）', '是（PCR 体系）', 'ok'),
    ('熔解曲线线', 'M3291（SensiMelt Multiplex Premix）', '荧光 PCR 熔解曲线（MMCA 路线）', '是（PCR 体系）', 'ok'),
    ('熔解曲线线', 'FM0324-074901（Robustart Melt RT Multiplex Premix）', '荧光 PCR 熔解曲线（MMCA 路线）', '是（PCR 体系）', 'ok'),
    ('缺口', 'LAMP + 熔解曲线 体系（等温版熔解分型）', '—', '无此产品', 'bad'),
]

# 行动建议：动作 / 主责 / 协同 / 时限 / 验收标准
ACTIONS = [
    ('内部可行性实验：Bst 3.0 HS 在 EvaGreen 体系下的扩增效率与熔解峰形；dUTP/UNG 与饱和染料兼容性；冻干前后峰形稳定性',
     '技术支持部（研发）', '市场（资料准备）', '1–2 周',
     '输出峰形对比图与结论（可用 / 需改配方 / 不可行），这是全部判断的支点'),
    ('体系补齐：饱和染料兼容 buffer + 探针熔解引物设计模板（若走探针路线）',
     '技术支持部（研发）', '—', '随①结论启动',
     '给出可交付的体系配方与一套演示引物设计'),
    ('商务落点：优先触达已有熔解曲线装机与分型需求的客户（现有 MMCA 熔解曲线线客户）',
     '大客户部 / 销售拓展部', '技术支持部', '①出结论后 2 周内',
     '形成意向客户清单 + 每家一句场景判断（等温版是否有价值）'),
    ('资料与话术：产出 LAMP 熔解曲线可行性说明（含三道坎与边界）',
     '市场', '技术支持部', '①出结论后 1 周内',
     '一份对外可讲的技术说明 + 一份内部应答话术（含反例与边界）'),
    ('不建议动作：把 LAMP + 熔解曲线推给现场快检 / POCT 场景',
     '销售管理岗（口径把关）', '—', '即刻',
     '在资料与话术中明确写出“不适用场景”，避免误售'),
]

# 缺口清单：缺口 / 需谁补 / 会如何改变结论
GAPS = [
    ('国内是否有已获证的「LAMP / 等温 + 熔解曲线」试剂盒',
     '在注册数据库按“等温”“熔解”逐条精检（需人工核）',
     '若有获证产品 → “市场空白”的结论不成立，从“先发机会”改为“追赶定位”'),
    ('竞品在研管线（未公开部分）',
     '公开渠道查不到，需客户侧或展会信息补充',
     '若已有竞品在研 → 我方验证实验的优先级应从“有机会”上调为“不能等”'),
    ('这题是哪个客户提出的、他的场景是什么',
     '提问来源确认（IVD 客户开新项目 / 科研客户问方法学）',
     '决定话术重心：IVD 客户讲体系与注册、科研客户讲分型能力与文献'),
]

# 来源与口径
SOURCES = [
    '文献：Biotechnol Rep 2020 (PMC7240324)；Clin Chim Acta 2018 (29596814, 29247634)；Lett Appl Microbiol 2023 (37740570)；'
    'IJMS 2024 (PMC11241107)；Chem Commun 2019 (30734782)；NAR 2020 (PMC7144905)；Biomethods 2025 (bpaf010)；Rev Inst Med Trop Sao Paulo 2025 (PMC12204207)',
    '专利：WO2025091343A1 / CN121311601A；WO2024023510A1；WO2025161028A1；JP7737474B2；CN121700046A',
    '竞品与行业页面：赛默飞等温核酸扩增页、全式金等温扩增技术系列页、翌圣 LAMP 产品选择指南页、NEB 等温扩增产品页、宝锐官网 2026-01 LAMP 新品页',
    '监管文件：BioFire FilmArray ME（2022 年度医疗器械注册工作报告）；苏州淦江生物 SMN1 试剂审评报告（CSZ2500221）',
    '我方资料：内部产品推荐清单（PCR-LAMP，2026-07 口径）、熔解曲线产品说明书清单、产品线货号与读出方式',
]


# ══════════════════════════════════════════════════════════════
# 二、HTML 片段构造
# ══════════════════════════════════════════════════════════════

def td(v, num=False, cls=''):
    dv = ''
    if isinstance(v, (int, float)):
        dv = ' data-v="%s"' % v
        num = True
    c = ' class="%s"' % cls if cls else ''
    return '<td%s%s>%s</td>' % (dv, c, v)


def table(headers, rows, note='', cls='tbl'):
    """headers: [(标题, 是否数值列)]"""
    th = ''.join('<th%s>%s</th>' % (' data-num' if n else '', t) for t, n in headers)
    body = []
    for r in rows:
        body.append('<tr>' + ''.join(r) + '</tr>')
    return ('<div class="tblwrap"><table class="%s"><thead><tr>%s</tr></thead><tbody>%s</tbody></table></div>%s'
            % (cls, th, ''.join(body), ('<p class="tnote">%s</p>' % note if note else '')))


def badge(txt, kind):
    return '<span class="bdg %s">%s</span>' % (kind, txt)


# ── 图 1：熔解峰形对比（LAMP vs PCR）─────────────────────────
def fig1():
    return '''
<figure class="figbox">
  <svg class="fig" viewBox="0 0 900 330" role="img" aria-label="LAMP 产物与 PCR 产物的熔解峰形对比">
    <defs>
      <linearGradient id="gLamp" x1="0" y1="0" x2="1" y2="0">
        <stop offset="0" stop-color="#38bdf8"/><stop offset="1" stop-color="#818cf8"/>
      </linearGradient>
      <linearGradient id="gPcr" x1="0" y1="0" x2="1" y2="0">
        <stop offset="0" stop-color="#34d399"/><stop offset="1" stop-color="#22d3ee"/>
      </linearGradient>
    </defs>
    <rect x="0" y="0" width="900" height="330" rx="14" fill="rgba(255,255,255,.02)"/>
    <text x="36" y="34" class="fg-t" >PCR：线性扩增子</text>
    <text x="36" y="54" class="fg-s">单一序列 → 尖而窄的单峰</text>
    <text x="486" y="34" class="fg-t">LAMP：花椰菜状多聚体</text>
    <text x="486" y="54" class="fg-s">多种茎环结构 → 宽峰 / 多峰</text>
    <line x1="450" y1="70" x2="450" y2="300" stroke="rgba(255,255,255,.10)" stroke-width="1"/>

    <g transform="translate(0,0)">
      <line x1="66" y1="272" x2="410" y2="272" stroke="rgba(255,255,255,.22)" stroke-width="1"/>
      <line x1="66" y1="272" x2="66" y2="92" stroke="rgba(255,255,255,.22)" stroke-width="1"/>
      <text x="238" y="296" class="fg-a" text-anchor="middle">温度 →</text>
      <text x="74" y="104" class="fg-a">-dF/dT</text>
      <path class="draw" d="M70,268 C150,266 196,258 214,180 C222,140 232,96 248,96 C264,96 274,140 282,180 C300,258 344,266 406,268"
            fill="url(#gPcr)" fill-opacity=".16" stroke="url(#gPcr)" stroke-width="2.6"/>
      <circle cx="248" cy="96" r="4.5" fill="#34d399"/>
      <text x="248" y="84" class="fg-v" text-anchor="middle">单峰 · 可判型</text>
    </g>

    <g transform="translate(450,0)">
      <line x1="66" y1="272" x2="410" y2="272" stroke="rgba(255,255,255,.22)" stroke-width="1"/>
      <line x1="66" y1="272" x2="66" y2="92" stroke="rgba(255,255,255,.22)" stroke-width="1"/>
      <text x="238" y="296" class="fg-a" text-anchor="middle">温度 →</text>
      <text x="74" y="104" class="fg-a">-dF/dT</text>
      <path class="draw" d="M70,268 C130,264 168,246 186,206 C196,186 204,158 216,158 C230,158 238,180 246,196 C258,220 272,232 288,220 C300,212 306,192 316,192 C330,192 338,212 348,226 C364,246 384,260 406,264"
            fill="url(#gLamp)" fill-opacity=".16" stroke="url(#gLamp)" stroke-width="2.6"/>
      <circle cx="216" cy="158" r="4.5" fill="#38bdf8"/>
      <circle cx="316" cy="192" r="4.5" fill="#818cf8"/>
      <line x1="216" y1="168" x2="316" y2="202" stroke="rgba(129,140,248,.45)" stroke-width="1" stroke-dasharray="4 4"/>
      <text x="266" y="128" class="fg-v" text-anchor="middle">宽峰 / 多峰</text>
      <text x="266" y="110" class="fg-s" text-anchor="middle">主峰 + 肩峰叠加，峰位判型变难</text>
    </g>
  </svg>
  <figcaption>图 1｜同样是“熔解曲线”，PCR 给的是窄单峰、LAMP 给的是宽峰 / 多峰。这是 LAMP 熔解分型难做的根本原因，也解释了为什么文献多集中在“序列差异大”或“探针熔解”两类设计上。</figcaption>
</figure>'''


# ── 图 2：两步流程（等温扩增 → 变温熔解）────────────────────
def fig2():
    return '''
<figure class="figbox">
  <svg class="fig" viewBox="0 0 900 250" role="img" aria-label="等温扩增到变温熔解的两步流程">
    <rect x="0" y="0" width="900" height="250" rx="14" fill="rgba(255,255,255,.02)"/>
    <line x1="70" y1="150" x2="860" y2="150" stroke="rgba(255,255,255,.22)" stroke-width="1"/>
    <text x="30" y="154" class="fg-a">温度</text>
    <text x="70" y="186" class="fg-a">60 ℃</text>
    <text x="70" y="118" class="fg-a">90 ℃</text>

    <path class="draw" d="M70,150 L70,96 L300,96 L300,150 L470,150 L470,70 L820,70"
          fill="none" stroke="url(#gPcr)" stroke-width="3"/>
    <path d="M70,96 L300,96" stroke="#34d399" stroke-width="8" stroke-opacity=".28"/>
    <path d="M470,70 L820,70" stroke="#f59e0b" stroke-width="8" stroke-opacity=".28" stroke-dasharray="7 5"/>

    <text x="185" y="80" class="fg-v" text-anchor="middle">① 等温扩增 60–65 ℃</text>
    <text x="185" y="212" class="fg-s" text-anchor="middle">恒温 15–60 min，只需恒温设备</text>
    <text x="645" y="54" class="fg-v" text-anchor="middle">② 升温熔解 40 → 90 ℃</text>
    <text x="645" y="212" class="fg-s" text-anchor="middle">需变温模块 + 高分辨温控（HRM 级）</text>

    <rect x="330" y="104" width="110" height="30" rx="8" fill="rgba(239,68,68,.14)" stroke="rgba(239,68,68,.5)"/>
    <text x="385" y="124" class="fg-bad" text-anchor="middle">变温切换点</text>
  </svg>
  <figcaption>图 2｜LAMP 熔解曲线是“两段式”：前半段仍是标准等温扩增，后半段要升温并逐度采集荧光。第二段才是新增门槛——它要求仪器具备变温与高分辨温控能力，“恒温水浴锅就能做 LAMP”这句话在这一步不成立。</figcaption>
</figure>'''


# ── 图 3：LAMP 读出方式谱系 ─────────────────────────────────
def fig3():
    chips = [
        ('终点电泳', '传统 / 研究', 'dim'),
        ('浊度法', '焦磷酸镁沉淀', 'dim'),
        ('显色法', '钙黄绿素 / 羟基萘酚蓝 / pH', 'main'),
        ('荧光染料法', 'SYBR / Syto（非 HRM）', 'main'),
        ('荧光探针法', '多重首选', 'main'),
        ('侧流试纸条', 'POCT 常用', 'main'),
        ('熔解曲线', '多篇文献 + 活跃专利，但无原料专供', 'hi'),
    ]
    out = []
    for i, (name, sub, kind) in enumerate(chips):
        x = 34 + i * 124
        cls = 'chip-hi' if kind == 'hi' else ('chip-dim' if kind == 'dim' else 'chip-main')
        out.append('<g><rect class="%s" x="%d" y="96" width="112" height="86" rx="11"/>'
                   '<text x="%d" y="130" class="fg-chip" text-anchor="middle">%s</text>'
                   '<text x="%d" y="152" class="fg-chips" text-anchor="middle">%s</text></g>'
                   % (cls, x, x + 56, name, x + 56, sub))
    return '''
<figure class="figbox">
  <svg class="fig" viewBox="0 0 900 240" role="img" aria-label="LAMP 读出方式谱系，熔解曲线为小众但成立的一支">
    <rect x="0" y="0" width="900" height="240" rx="14" fill="rgba(255,255,255,.02)"/>
    <text x="34" y="46" class="fg-t">LAMP 的读出方式谱系</text>
    <text x="34" y="70" class="fg-s">主流三支＝显色 / 荧光染料 / 荧光探针；熔解曲线成立但属小众，且目前没有原料商做专用体系</text>
    %s
    <text x="34" y="212" class="fg-s">行业口径（赛默飞 / 全式金 / 我方官网）把 LAMP 检测方法统一写成“荧光法、比色法、浊度法、侧流法”，均未列入熔解曲线</text>
  </svg>
  <figcaption>图 3｜熔解曲线在 LAMP 家族里是“成立但不主流”的一支。它不是新发明，而是把读出端从“有没有扩增”升级为“扩增产物是什么序列”——这正是多重分型与 SNP 检测需要的。</figcaption>
</figure>''' % ''.join(out)


# ── 图 4：专利时间轴 ────────────────────────────────────────
def fig4():
    # 节点标签用短号，避免溢出画布；完整主题在正文与图注中给出
    labels = [
        ('2023-11-01', 'WO2025091343A1', '（同族 CN121311601A）'),
        ('2024-02-01', 'WO2024023510A1', '（LAMP 检测 SNP）'),
        ('2025-05-08', 'WO2025161028A1', '（LAMP 产物检测）'),
        ('2025-08-07', 'JP7737474B2', '（多检测温度分辨）'),
        ('2026-03-20', 'CN121700046A', '（熔解曲线检测法）'),
    ]
    x0, step = 110, 170
    nodes = []
    for i, (date, num, note) in enumerate(labels):
        x = x0 + i * step
        nodes.append('<circle cx="%d" cy="120" r="8" fill="#38bdf8"/>'
                     '<circle cx="%d" cy="120" r="14" fill="rgba(56,189,248,.16)"/>'
                     '<text x="%d" y="98" class="fg-v" text-anchor="middle">%s</text>'
                     '<text x="%d" y="150" class="fg-chip" text-anchor="middle">%s</text>'
                     '<text x="%d" y="170" class="fg-chips" text-anchor="middle">%s</text>'
                     % (x, x, x, date, x, num, x, note))
    cap = '；'.join('%s（%s）' % (t, n) for _, n, t, _ in PATENTS)
    return '''
<figure class="figbox">
  <svg class="fig" viewBox="0 0 900 230" role="img" aria-label="熔解曲线做 LAMP 多重检测的专利时间线">
    <rect x="0" y="0" width="900" height="230" rx="14" fill="rgba(255,255,255,.02)"/>
    <text x="34" y="42" class="fg-t">专利面：熔解曲线 + LAMP 多重检测正在被密集布局</text>
    <text x="34" y="66" class="fg-s">近三年公开的 5 件相关申请，主题高度集中在“用熔解 / 退火曲线做 LAMP 产物检测与多重分辨”</text>
    <line x1="52" y1="120" x2="880" y2="120" stroke="rgba(56,189,248,.35)" stroke-width="2"/>
    %s
    <text x="34" y="212" class="fg-s">用途提示：立项或自由实施分析前，需逐件核对权利要求范围与地域（中 / 美 / 日 / 欧）。</text>
  </svg>
  <figcaption>图 4｜五件申请的完整主题（按优先权日先后）：%s。专利活跃度是“技术方向被认可”的信号：布局者认为这条路值得占位，这也是本次判断要从“能不能做”升级为“要不要现在做”的依据之一。</figcaption>
</figure>''' % (''.join(nodes), cap)


# ── 图 5：我方产品线缺口 ────────────────────────────────────
def fig5():
    return '''
<figure class="figbox">
  <svg class="fig" viewBox="0 0 900 250" role="img" aria-label="我方 LAMP 产品线与熔解曲线产品线，以及二者之间的缺口">
    <rect x="0" y="0" width="900" height="250" rx="14" fill="rgba(255,255,255,.02)"/>
    <text x="34" y="42" class="fg-t">我方产品线：两条线都在，缺的是中间那一格</text>

    <rect x="34" y="70" width="360" height="132" rx="12" fill="rgba(56,189,248,.10)" stroke="rgba(56,189,248,.42)"/>
    <text x="54" y="98" class="fg-v">LAMP 线（等温）</text>
    <text x="54" y="126" class="fg-s">Bst 2.0 / 2.0 HS / 3.0 HS（适配体封闭）</text>
    <text x="54" y="148" class="fg-s">显色法 Kit、探针法 Premix、LAMP 缓冲</text>
    <text x="54" y="170" class="fg-s">已具备：dUTP/UNG 防污染、可冻干版</text>
    <text x="54" y="192" class="fg-s">读出：比色 / 荧光 / 探针 —— 无熔解曲线</text>

    <rect x="506" y="70" width="360" height="132" rx="12" fill="rgba(52,211,153,.10)" stroke="rgba(52,211,153,.42)"/>
    <text x="526" y="98" class="fg-v">熔解曲线线（PCR）</text>
    <text x="526" y="126" class="fg-s">FM0301-042901 · Robustart Melt Multiplex</text>
    <text x="526" y="148" class="fg-s">M3291 · SensiMelt Multiplex Premix</text>
    <text x="526" y="170" class="fg-s">FM0324-074901 · Robustart Melt RT Multiplex</text>
    <text x="526" y="192" class="fg-s">读出：荧光 PCR 熔解曲线（客户按 MMCA 口径）</text>

    <rect x="410" y="104" width="80" height="64" rx="10" fill="rgba(239,68,68,.16)" stroke="rgba(239,68,68,.55)" stroke-dasharray="5 4"/>
    <text x="450" y="132" class="fg-bad" text-anchor="middle">缺口</text>
    <text x="450" y="152" class="fg-bad" text-anchor="middle">等温 + 熔解</text>

    <text x="34" y="230" class="fg-s">补齐这一格需要的新增能力只有两层：饱和染料兼容的等温 buffer，以及（若走探针路线）探针熔解的引物设计。</text>
  </svg>
  <figcaption>图 5｜我方不是从零起步：等温端与熔解端各自都有成熟体系，缺的是把两端接起来的中间产品。这是“可投入”与“不必重开赛道”的关键判断依据。</figcaption>
</figure>'''


# ══════════════════════════════════════════════════════════════
# 三、表格数据
# ══════════════════════════════════════════════════════════════

def build_tables():
    t = {}

    t['evidence'] = table(
        [('断言', False), ('定量证据', False), ('出处 / 标识', False), ('年份', True), ('证据强度', False)],
        [[td(a), td(b), td(c), td(d, num=True), td(badge(e, f))] for a, b, c, d, e, f in EVIDENCE],
        note='证据强度说明：直接证据＝同行评审文献中直接测得；直接反例＝同一方法上被实测出的分辨边界；趋势判断＝综述 / 方法学论文的定性结论。')

    t['mainstream'] = table(
        [('页面（一手）', False), ('原文口径（LAMP 检测方法）', False), ('是否列熔解曲线', False)],
        [[td(a), td(b), td(badge(c, d))] for a, b, c, d in MAINSTREAM],
        note='三处一手页面口径一致：主流读出里没有熔解曲线。这一点决定了“能做”不等于“市场已接受”。')

    t['matrix'] = table(
        [('公司 / 产品线', False), ('荧光染料法', False), ('显色 / pH 法', False),
         ('探针法', False), ('可冻干', False), ('LAMP + 熔解曲线专供', False)],
        [[td(a), td(b), td(c), td(d), td(e), td(badge(f, 'bad') if f.startswith('无') else td(f))] for a, b, c, d, e, f in MATRIX],
        note='“无”＝本次核查范围内未见该形态产品；不代表对方没有在研管线（未公开部分无法检索）。')

    t['certified'] = table(
        [('已获证 / 报批产品', False), ('方法学', False), ('体系', False), ('状态与要点', False)],
        [[td(a), td(b), td(c), td(d)] for a, b, c, d in CERTIFIED],
        note='三件全部是 PCR 体系——熔解曲线法在国内商业化的成熟载体仍是 PCR，LAMP 体系尚未见到获证产品。')

    t['ours'] = table(
        [('产品线', False), ('现有体系 / 货号', False), ('读出方式', False), ('支持熔解曲线', False)],
        [[td(a), td(b), td(c), td(badge(d, e))] for a, b, c, d, e in OURS],
        note='“是（PCR 体系）”＝该体系本身走熔解曲线读出，但酶与扩增机制是 PCR，不能直接等同于 LAMP 熔解曲线能力。')

    t['actions'] = table(
        [('序号', True), ('动作', False), ('主责', False), ('协同', False), ('时限', False), ('验收标准', False)],
        [[td(str(i + 1), num=True), td(a), td(b), td(c), td(d), td(e)]
         for i, (a, b, c, d, e) in enumerate(ACTIONS)],
        note='主责唯一；验收标准须做到“可判定完成 / 未完成”。')

    t['gaps'] = table(
        [('缺口', False), ('需要谁补 / 怎么补', False), ('它会如何改变结论', False)],
        [[td(a), td(b), td(c)] for a, b, c in GAPS],
        note='这三条是本次判断的不确定项，按对结论的影响排序。')

    return t


# ══════════════════════════════════════════════════════════════
# 四、页面模板
# ══════════════════════════════════════════════════════════════

CSS = '''
:root{
  --bg:#070f1c; --bg2:#0b1729; --card:rgba(255,255,255,.035); --line:rgba(255,255,255,.10);
  --ink:#e8f0fb; --ink2:#a8bcd6; --ink3:#7d93b0;
  --blue:#3b82f6; --cyan:#38bdf8; --ok:#34d399; --warn:#fbbf24; --bad:#f87171; --info:#a5b4fc;
}
*{box-sizing:border-box}
html,body{margin:0;padding:0}
body{background:radial-gradient(1200px 600px at 12% -8%,#123055 0%,var(--bg) 55%) no-repeat,var(--bg);
  color:var(--ink);display:flex;flex-direction:column;min-height:100vh;
  font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
  line-height:1.72;-webkit-font-smoothing:antialiased}
main{flex:1 0 auto;width:100%;max-width:1180px;margin:0 auto;padding:0 18px 56px}

nav.bar{position:sticky;top:0;z-index:20;background:rgba(7,15,28,.86);backdrop-filter:blur(9px);
  border-bottom:1px solid var(--line)}
nav.bar .inner{max-width:1180px;margin:0 auto;padding:9px 18px;display:flex;gap:8px;flex-wrap:wrap;align-items:center}
nav.bar a{color:var(--ink2);text-decoration:none;font-size:12.5px;padding:5px 11px;border:1px solid var(--line);
  border-radius:999px;white-space:nowrap}
nav.bar a:hover{color:#fff;border-color:rgba(56,189,248,.55);background:rgba(56,189,248,.10)}

.hero{padding:34px 0 6px}
.eyebrow{display:inline-block;font-size:12px;letter-spacing:2px;color:var(--cyan);
  border:1px solid rgba(56,189,248,.4);border-radius:999px;padding:4px 12px;background:rgba(56,189,248,.08)}
h1{font-size:34px;line-height:1.3;margin:16px 0 10px;letter-spacing:.5px}
h1 em{font-style:normal;background:linear-gradient(100deg,#38bdf8,#818cf8 60%,#34d399);
  -webkit-background-clip:text;background-clip:text;color:transparent}
.verdict{border-left:3px solid var(--cyan);background:linear-gradient(90deg,rgba(56,189,248,.12),rgba(56,189,248,0));
  padding:14px 18px;border-radius:0 12px 12px 0;font-size:16.5px;color:#dbe9fb;margin:18px 0 8px}
.verdict b{color:#fff}

h2{font-size:21px;margin:44px 0 6px;padding-top:14px;border-top:1px solid var(--line);letter-spacing:.4px}
h2 .num{color:var(--cyan);font-size:13px;letter-spacing:2px;display:block;margin-bottom:4px}
h3{font-size:16px;margin:26px 0 8px;color:#d8e6f8}
p{margin:9px 0;color:var(--ink2)}
p.lead{color:#cfdff2;font-size:15.5px}
ul{margin:8px 0 8px 2px;padding-left:20px;color:var(--ink2)}
li{margin:5px 0}
b,strong{color:#fff}
code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px;color:#bfe4ff;
  background:rgba(56,189,248,.10);padding:1px 6px;border-radius:5px;white-space:normal;word-break:break-word}

.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(258px,1fr));gap:13px;margin:16px 0}
.card{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:15px 16px}
.card .k{font-size:11.5px;letter-spacing:1.4px;color:var(--ink3);margin-bottom:7px}
.card .v{font-size:14.5px;color:#e6f0fd;font-weight:600;margin-bottom:5px}
.card.ok{border-color:rgba(52,211,153,.42);background:rgba(52,211,153,.07)}
.card.warn{border-color:rgba(251,191,36,.42);background:rgba(251,191,36,.07)}
.card.bad{border-color:rgba(248,113,113,.42);background:rgba(248,113,113,.07)}
.card p{font-size:13px;color:var(--ink2);margin:6px 0 0}

.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(178px,1fr));gap:12px;margin:18px 0}
.kpi{background:linear-gradient(160deg,rgba(255,255,255,.06),rgba(255,255,255,.02));
  border:1px solid var(--line);border-radius:13px;padding:14px}
.kpi .n{font-size:27px;font-weight:700;letter-spacing:.5px;
  background:linear-gradient(100deg,#38bdf8,#818cf8);-webkit-background-clip:text;background-clip:text;color:transparent}
.kpi .n small{font-size:13px;color:var(--ink3);-webkit-text-fill-color:var(--ink3);margin-left:3px}
.kpi .t{font-size:12.5px;color:#d5e4f6;margin-top:3px;font-weight:600}
.kpi .s{font-size:11.5px;color:var(--ink3);margin-top:4px;line-height:1.55}

.figbox{margin:20px 0 6px;background:var(--card);border:1px solid var(--line);border-radius:14px;padding:12px 12px 6px}
.fig{width:100%;height:auto;display:block}
figcaption{font-size:12.5px;color:var(--ink3);padding:8px 4px 10px;line-height:1.7}

.fg-t{fill:#eaf3ff;font-size:14px;font-weight:600}
.fg-s{fill:#8ea9c8;font-size:11.5px}
.fg-a{fill:#7d93b0;font-size:11px}
.fg-v{fill:#d8e8ff;font-size:12px;font-weight:600}
.fg-bad{fill:#fca5a5;font-size:11.5px;font-weight:600}
.fg-chip{fill:#eaf3ff;font-size:12.5px;font-weight:600}
.fg-chips{fill:#94aabd;font-size:10.5px}
.chip-main{fill:rgba(56,189,248,.10);stroke:rgba(56,189,248,.40)}
.chip-dim{fill:rgba(255,255,255,.04);stroke:rgba(255,255,255,.12)}
.chip-hi{fill:rgba(251,191,36,.14);stroke:rgba(251,191,36,.62);stroke-width:1.6}
svg .draw{stroke-dasharray:1200;stroke-dashoffset:1200;animation:drawin 1.5s ease-out forwards}
@keyframes drawin{to{stroke-dashoffset:0}}
@media (prefers-reduced-motion:reduce){svg .draw{animation:none;stroke-dashoffset:0}}

.tblwrap{overflow-x:auto;margin:14px 0 4px;border:1px solid var(--line);border-radius:13px;background:rgba(255,255,255,.02)}
table.tbl{width:100%;border-collapse:collapse;font-size:13px;min-width:660px}
table.tbl th{position:sticky;top:0;z-index:2;background:#0f2138;color:#dbe9fb;text-align:left;
  font-size:12px;letter-spacing:.6px;padding:10px 12px;border-bottom:1px solid rgba(56,189,248,.28);
  white-space:nowrap;user-select:none}
table.tbl th.sortable{cursor:pointer}
table.tbl th.sortable:hover{color:#fff;background:#14294a}
table.tbl th.asc::after{content:" ▲";color:var(--cyan);font-size:10px}
table.tbl th.desc::after{content:" ▼";color:var(--cyan);font-size:10px}
table.tbl td{padding:10px 12px;border-bottom:1px solid rgba(255,255,255,.06);color:#c9dbef;
  vertical-align:top;white-space:normal;word-break:break-word}
table.tbl tbody tr:hover td{background:rgba(56,189,248,.05)}
table.tbl td[data-v]{text-align:left;font-variant-numeric:tabular-nums}
.tnote{font-size:11.5px;color:var(--ink3);margin:6px 2px 14px;line-height:1.65}
.tblbar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;padding:10px 12px 2px}
.tblsearch{background:rgba(255,255,255,.05);border:1px solid var(--line);border-radius:8px;color:var(--ink);
  padding:6px 10px;font-size:12.5px;min-width:190px;outline:none}
.tblsearch:focus{border-color:rgba(56,189,248,.6)}
.tblbtn{background:rgba(56,189,248,.14);border:1px solid rgba(56,189,248,.42);color:#d9edff;
  border-radius:8px;padding:6px 12px;font-size:12.5px;cursor:pointer}
.tblbtn:hover{background:rgba(56,189,248,.24)}
.tblbtn.ghost{background:transparent;border-color:var(--line);color:var(--ink2)}
.tblinfo{font-size:11.5px;color:var(--ink3);margin-left:auto}

.bdg{display:inline-block;font-size:11.5px;padding:2px 9px;border-radius:999px;white-space:nowrap;border:1px solid transparent}
.bdg.ok{background:rgba(52,211,153,.14);color:#6ee7b7;border-color:rgba(52,211,153,.42)}
.bdg.warn{background:rgba(251,191,36,.14);color:#fcd34d;border-color:rgba(251,191,36,.42)}
.bdg.bad{background:rgba(248,113,113,.14);color:#fca5a5;border-color:rgba(248,113,113,.42)}
.bdg.info{background:rgba(165,180,252,.14);color:#c7d2fe;border-color:rgba(165,180,252,.42)}

.obs{display:grid;gap:13px;margin:16px 0}
.ob{background:var(--card);border:1px solid var(--line);border-left:3px solid var(--warn);border-radius:12px;padding:15px 17px}
.ob h4{margin:0 0 6px;font-size:15px;color:#ffe9b8}
.ob .sub{font-size:12.5px;color:var(--ink3);margin-bottom:8px}
.ob ul{margin:6px 0 8px}
.ob .foot{font-size:12px;color:#fcd34d;border-top:1px dashed rgba(251,191,36,.3);padding-top:8px;margin-top:4px}

.callout{background:linear-gradient(90deg,rgba(52,211,153,.12),rgba(52,211,153,0));border-left:3px solid var(--ok);
  border-radius:0 12px 12px 0;padding:13px 17px;margin:16px 0;color:#d8f5e8}
.callout.red{background:linear-gradient(90deg,rgba(248,113,113,.12),rgba(248,113,113,0));
  border-left-color:var(--bad);color:#fbdada}

.src ol{margin:10px 0;padding-left:22px;color:var(--ink2);font-size:13px;line-height:1.8}
.src li{margin:7px 0;word-break:break-word}
.meta{font-size:12px;color:var(--ink3);margin-top:14px;border-top:1px solid var(--line);padding-top:12px}

@media (max-width:760px){
  /* 窄屏修正：品牌页眉允许换行 + 压缩字距，消除横向溢出 */
  .bior-header .bior-header-inner{flex-wrap:wrap;padding:8px 12px;gap:8px}
  .bior-header .bior-name{font-size:13px;letter-spacing:.3px}
  .bior-header .bior-cn{font-size:10px;letter-spacing:.6px}
  .bior-header .bior-slogan{font-size:10px;letter-spacing:0}
  .bior-header .bior-logo-chip img{height:22px}
  .bior-header .bior-tagwrap{text-align:right}
  h1{font-size:25px}
  h2{font-size:19px}
  main{padding:0 13px 42px}
  nav.bar .inner{padding:8px 13px}
  nav.bar a{font-size:11.5px;padding:4px 9px}
  .kpi .n{font-size:23px}
  .verdict{font-size:15px;padding:12px 14px}
  table.tbl{font-size:12.5px;min-width:600px}
}
'''

JS = '''
(function(){
  var NL=String.fromCharCode(10), BOM=String.fromCharCode(0xFEFF);
  function plain(s){ return s===null||s===undefined?'':String(s); }
  document.querySelectorAll('table.tbl').forEach(function(tb,ti){
    var wrap=tb.parentNode;
    var bar=document.createElement('div'); bar.className='tblbar';
    var inp=document.createElement('input'); inp.type='search'; inp.className='tblsearch';
    inp.placeholder='搜索本表…'; inp.setAttribute('aria-label','搜索本表');
    var rst=document.createElement('button'); rst.type='button'; rst.className='tblbtn ghost'; rst.textContent='重置';
    var csv=document.createElement('button'); csv.type='button'; csv.className='tblbtn'; csv.textContent='导出 CSV';
    var inf=document.createElement('span'); inf.className='tblinfo';
    bar.appendChild(inp); bar.appendChild(rst); bar.appendChild(csv); bar.appendChild(inf);
    wrap.insertBefore(bar, tb);
    var tbody=tb.tBodies[0];
    var rows=Array.prototype.slice.call(tbody.rows);
    var sk=null, sd=1;
    function refresh(){
      var q=inp.value.trim().toLowerCase(), n=0;
      rows.forEach(function(r){
        var hit=(!q)||r.textContent.toLowerCase().indexOf(q)>=0;
        r.style.display = hit?'':'none';
        if(hit){ n++; }
      });
      inf.textContent='命中 '+n+' / '+rows.length+' 行';
    }
    inp.addEventListener('input', refresh);
    rst.addEventListener('click', function(){
      inp.value=''; sk=null; sd=1;
      Array.prototype.forEach.call(tb.tHead.rows[0].cells,function(h){ h.classList.remove('asc','desc'); });
      rows.sort(function(a,b){ return Number(a.dataset.i)-Number(b.dataset.i); });
      rows.forEach(function(r){ tbody.appendChild(r); });
      refresh();
    });
    rows.forEach(function(r,i){ r.dataset.i=i; });
    Array.prototype.forEach.call(tb.tHead.rows[0].cells, function(th,ci){
      th.classList.add('sortable');
      var isNum = th.hasAttribute('data-num');
      th.addEventListener('click', function(){
        sd = (sk===ci) ? -sd : 1; sk=ci;
        var val=function(r){
          var c=r.cells[ci];
          var v=(c && c.dataset && c.dataset.v!==undefined) ? c.dataset.v : (c?c.textContent:'');
          return plain(v);
        };
        rows.sort(function(a,b){
          var x=val(a), y=val(b);
          if(isNum){
            var fx=parseFloat(x.replace(/[^0-9.\\-]/g,''))||0;
            var fy=parseFloat(y.replace(/[^0-9.\\-]/g,''))||0;
            return (fx-fy)*sd;
          }
          return x.localeCompare(y,'zh')*sd;
        });
        rows.forEach(function(r){ tbody.appendChild(r); });
        Array.prototype.forEach.call(tb.tHead.rows[0].cells,function(h){ h.classList.remove('asc','desc'); });
        th.classList.add(sd>0?'asc':'desc');
        refresh();
      });
    });
    csv.addEventListener('click', function(){
      var out=[];
      var head=[];
      Array.prototype.forEach.call(tb.tHead.rows[0].cells,function(c){ head.push('"'+c.textContent.replace(/"/g,'""')+'"'); });
      out.push(head.join(','));
      rows.forEach(function(r){
        if(r.style.display==='none'){ return; }
        var cells=[];
        Array.prototype.forEach.call(r.cells,function(c){
          cells.push('"'+c.textContent.replace(/\\s+/g,' ').replace(/"/g,'""').trim()+'"');
        });
        out.push(cells.join(','));
      });
      var blob=new Blob([BOM+out.join(NL)],{type:'text/csv;charset=utf-8'});
      var a=document.createElement('a');
      a.href=URL.createObjectURL(blob);
      a.download='LAMP-melting-table-'+(ti+1)+'.csv';
      a.style.display='none';
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
    });
    refresh();
  });
})();
'''


def build_page():
    T = build_tables()

    kpi_html = ''.join(
        '<div class="kpi"><div class="n">%s<small>%s</small></div><div class="t">%s</div><div class="s">%s</div></div>'
        % (n, u, t, s) for n, u, t, s in KPI)

    obs_html = ''.join(
        '<div class="ob"><h4>%s</h4><div class="sub">%s</div><ul>%s</ul><div class="foot">%s</div></div>'
        % (h, sub, ''.join('<li>%s</li>' % i for i in items), foot)
        for h, sub, items, foot in OBSTACLES)

    src_html = ''.join('<li>%s</li>' % s for s in SOURCES)

    return '''<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>LAMP × 熔解曲线法：可行性精读页 ｜ 宝锐生物 · 诊断原料</title>
<style>@@CSS@@</style>
</head>
<body>
<nav class="bar"><div class="inner">
  <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">缺口清单</a>
  <a href="#s10">口径与来源</a>
</div></nav>

<main>
  <header class="hero">
    <span class="eyebrow">技术可行性精读 · 诊断原料</span>
    <h1>LAMP 能不能做<em>熔解曲线法</em>检测？</h1>
    <div class="verdict">
      <b>能做，而且已有一批十年量级的文献与近三年的活跃专利布局。</b>
      但它不是 LAMP 的主流读出方式 —— 能落地的形态是「等温扩增 → 变温熔解」两步，
      价值落在<b>多重分型 / SNP / 特异性确认</b>，而不是常规病原定性；
      也不是“LAMP 便宜”这个卖点的延伸，反而是对它的一次削弱。
    </div>
  </header>

  <section id="s1">
    <h2><span class="num">01 · 结论速览</span>一句话拆成三档，先说能做什么、不能做什么</h2>
    <div class="cards">
      <div class="card ok">
        <div class="k">可 行</div>
        <div class="v">序列差异足够大的靶标 → 染料法熔解分型</div>
        <p>已有多个平行对照研究支持：病原种间鉴别、病毒分型、部分 SNP 分型均有实测一致率数据。前提是靶序列本身差异够大，且仪器具备 HRM 级温控。</p>
      </div>
      <div class="card warn">
        <div class="k">需 改 造</div>
        <div class="v">探针熔解 · 单通道多重化</div>
        <p>LAMP 产物是花椰菜状多聚体，染料法峰宽；要稳定分型需走探针熔解（专门的引物 / 环区探针设计），或依赖精细的熔解模式识别（实测可分辨 ΔTm ≈ 0.2 ℃）。</p>
      </div>
      <div class="card bad">
        <div class="k">不 建 议</div>
        <div class="v">把 LAMP + 熔解曲线推给现场快检 / POCT</div>
        <p>熔解段需要变温与高分辨温控，“恒温水浴锅就能做”的前提在这一步失效；开管操作还叠加气溶胶污染风险。这两个场景卖它，客户现场会被问住。</p>
      </div>
    </div>
    <div class="kpis">@@KPI@@</div>
    <div class="callout">
      <b>一句话给客户：</b>“LAMP 熔解曲线法是成立的技术路线，文献与专利都支持，但它换来的是<b>分型能力</b>，代价是<b>仪器门槛</b>和<b>体系设计难度</b>；如果你的场景是多重分型或 SNP，且实验室已有 qPCR/HRM 装机，这条路值得试；如果目标是现场快检，请先别走这条。”
    </div>
  </section>

  <section id="s2">
    <h2><span class="num">02 · 原理</span>同样叫“熔解曲线”，LAMP 与 PCR 拿到的信息不一样</h2>
    <p class="lead">熔解曲线分析的基本逻辑是：双链 DNA 的热稳定性由碱基序列决定，序列变了熔解行为就变。PCR 的产物是长度确定、序列单一的线性扩增子，所以给的是窄而尖的单峰，峰位（Tm）可直接用于判型。LAMP 的产物是花椰菜状多聚体，同一管里存在多种茎环结构，熔解行为叠加在一起，峰形变宽、甚至出现多峰。</p>
    @@FIG1@@
    <p>因此文献里的成功案例几乎都做了同一件事：<b>把“分辨率需求”和“产物异质性”错开</b>——要么挑序列差异足够大的靶标（病原种间、病毒型间），要么改用探针熔解，让探针（而不是整个扩增子）来承担分辨任务。</p>
    <h3>熔解段是新增门槛：它是“两段式”反应</h3>
    @@FIG2@@
    <h3>放进 LAMP 家族看，它属于“成立但不主流”的一支</h3>
    @@FIG3@@
  </section>

  <section id="s3">
    <h2><span class="num">03 · 学术证据</span>八条可溯源结论，含一条必须说清的分辨边界</h2>
    <p>下表按“断言—定量证据—出处—强度”排列。其中 2018 年那条是<b>反例</b>：同一方法在近缘变异体上被实测出不可区分。报结论时带上它，比只报成功案例更能站住脚。</p>
    @@T_EVIDENCE@@
  </section>

  <section id="s4">
    <h2><span class="num">04 · 专利面</span>近三年这类申请在密集出现，说明方向被认可</h2>
    @@FIG4@@
  </section>

  <section id="s5">
    <h2><span class="num">05 · 市场证据</span>主流读出方式里没有它；也没有一家原料商做专用体系</h2>
    <h3>5.1 三家一手页面口径（含我们自己）</h3>
    @@T_MAINSTREAM@@
    <h3>5.2 竞品原料矩阵：熔解曲线专供一栏全是“无”</h3>
    @@T_MATRIX@@
    <div class="callout">
      <b>这栏“无”是本页最有商业价值的一格。</b>显色法、染料法、探针法、可冻干，家家都有；但<b>没有一家原料商在打“LAMP + 熔解曲线”专用体系这张牌</b>。这意味着：这个方向的原料供给还是空的，先做出来的人定话语权。
    </div>
    <h3>5.3 已获证 / 报批的“熔解曲线法”产品：全是 PCR 体系</h3>
    @@T_CERTIFIED@@
    <p>这说明熔解曲线在国内的合规与商业化路径是<b>沿 PCR 走通的</b>，LAMP 体系目前还没有走到获证这一步。对客户讲的时候，这条要提前说，避免对方按 PCR 熔解曲线的成熟度来预期 LAMP。</p>
  </section>

  <section id="s6">
    <h2><span class="num">06 · 三道坎</span>决定“做得出来”与“卖得出去”之间的距离</h2>
    <div class="obs">@@OBS@@</div>
    <div class="callout red">
      <b>风险合并陈述：</b>把这三道坎叠起来看，LAMP 熔解曲线的真实定位是“<b>给已有分子实验室加一项分型能力</b>”，而不是“给现场快检换一种读法”。方向判断若错，后面所有资料与话术都会偏。
    </div>
  </section>

  <section id="s7">
    <h2><span class="num">07 · 我方现状与缺口</span>不是从零起步，缺的是把两端接起来的那一格</h2>
    @@FIG5@@
    @@T_OURS@@
    <p>内部产品清单口径（2026-07）里，“熔曲”一律标 <code>MMCA</code>、体系走 PCR；<b>没有任何“LAMP + 熔曲”条目</b>。也就是说，这个组合目前是我方产品线的真实空白，而不是“已有产品换个说法”。</p>
    <div class="callout">
      <b>可投入性判断：</b>等温端已有 <b>Bst 3.0 HS（适配体封闭、含可冻干版、支持 dUTP/UNG 防污染、耐受全血/口腔拭子/粪便等复杂样本）</b>；熔解端已有三条成熟熔解曲线体系与客户基础。补齐中间一格需要的新增能力只有两层：<b>饱和染料兼容的等温 buffer</b>，以及（若走探针路线）<b>探针熔解的引物设计</b>。
    </div>
  </section>

  <section id="s8">
    <h2><span class="num">08 · 行动建议</span>五条，主责唯一、验收可判定</h2>
    @@T_ACTIONS@@
  </section>

  <section id="s9">
    <h2><span class="num">09 · 缺口清单</span>我没查到、需要补的三件事，以及它们如何改变结论</h2>
    @@T_GAPS@@
  </section>

  <section id="s10" class="src">
    <h2><span class="num">10 · 口径与来源</span>可复核、可回原文</h2>
    <p><b>口径说明：</b>本页所有“主流 / 无 / 未列”的判断，均取自本次核查范围内的一手页面与公开文献；未公开的竞品在研管线不在可检索范围内，因此“无专供”不等于“对方没有在做”。文献条目给出 DOI / PMCID 等标识，可逐条回原文核对；专利给出公开号与优先权日，用于立项或自由实施分析时逐件核对权利要求。</p>
    <ol>@@SRC@@</ol>
    <p class="meta">生成时间：@@NOW@@ ｜ 内容为内部技术判断，涉及我方产品线的价格、客户与商务信息不在本页范围 ｜ 如需对外版本，请另行裁剪并复核禁用表述。</p>
  </section>
</main>

<script>@@JS@@</script>
</body>
</html>'''.replace('@@CSS@@', CSS) \
        .replace('@@JS@@', JS) \
        .replace('@@KPI@@', kpi_html) \
        .replace('@@OBS@@', obs_html) \
        .replace('@@SRC@@', src_html) \
        .replace('@@FIG1@@', fig1()) \
        .replace('@@FIG2@@', fig2()) \
        .replace('@@FIG3@@', fig3()) \
        .replace('@@FIG4@@', fig4()) \
        .replace('@@FIG5@@', fig5()) \
        .replace('@@T_EVIDENCE@@', T['evidence']) \
        .replace('@@T_MAINSTREAM@@', T['mainstream']) \
        .replace('@@T_MATRIX@@', T['matrix']) \
        .replace('@@T_CERTIFIED@@', T['certified']) \
        .replace('@@T_OURS@@', T['ours']) \
        .replace('@@T_ACTIONS@@', T['actions']) \
        .replace('@@T_GAPS@@', T['gaps']) \
        .replace('@@NOW@@', NOW)


# ══════════════════════════════════════════════════════════════
# 五、守卫与落盘
# ══════════════════════════════════════════════════════════════

def guard(html):
    """写盘即断言：SVG 禁止项 + 占位符 + 零外链 + 品牌。"""
    errs = []
    svgs = re.findall(r'<svg[^>]*class="fig".*?</svg>', html, re.S)
    if len(svgs) != 5:
        errs.append('SVG 图数量异常：%d（应 5）' % len(svgs))
    for i, s in enumerate(svgs):
        if '<title' in s:
            errs.append('SVG#%d 含 <title>，会静默吞掉后续图形' % (i + 1))
        if '<br' in s:
            errs.append('SVG#%d 含 <br>，第二行会被丢掉' % (i + 1))
        if s.count('<rect') < 1 and i != 1:
            errs.append('SVG#%d 无 <rect>，疑似未渲染' % (i + 1))
    if re.search(r'@@[A-Z_0-9]+@@', html):
        errs.append('存在未替换占位符')
    links = re.findall(r'(?:src|href)=["\'](?:https?:)?//', html)
    if links:
        errs.append('存在外链 %d 处' % len(links))
    for need in ('守护生命健康，缔造美好生活', 'data:image/png;base64', 'BIORI BIOTECH'):
        if need not in html:
            errs.append('缺品牌要素：%s' % need)
    if html.count('<body') != 1:
        errs.append('<body> 数量异常：%d' % html.count('<body'))
    if html.count('</body>') != 1:
        errs.append('</body> 数量异常：%d' % html.count('</body>'))
    return errs


def main():
    os.makedirs(OUT_DIR, exist_ok=True)
    html = build_page()
    html = BK.inject(html, theme='dark', tag='营销中心 · 诊断原料 · 技术精读',
                     footer_extra='内部技术判断文档 · 生成于 ' + NOW)
    errs = guard(html)
    if errs:
        print('❌ 守卫未过：')
        for e in errs:
            print('   -', e)
        sys.exit(1)
    with open(OUT, 'w', encoding='utf-8') as f:
        f.write(html)
    v = BK.verify(html)
    print('✅ 已生成：%s' % OUT)
    print('   大小 %.1f KB ｜ 品牌自检：%s' % (len(html.encode()) / 1024, '全过' if all(v.values()) else v))
    print('   SVG 图 5 张 ｜ 表格 %d 张 ｜ 零外链 ✓' % html.count('<table class="tbl"'))
    m = re.search(r'<script>(.*?)</script>', html, re.S)
    if m:
        with open(os.path.join(OUT_DIR, '_lamp_page_inline.js'), 'w', encoding='utf-8') as f:
            f.write(m.group(1))
        print('   内联 JS 已导出供 node --check')


if __name__ == '__main__':
    main()
