#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""2026年10月科研朋友圈 · 拼图生成器 v2

交付：① 九宫格方案（9 图 3×3）  ② 单张长图方案
规范：白顶栏 Logo ｜ 页脚 slogan+品牌 ｜ 货号单套写法 ｜ 无客户名/内部人名
排版铁律：**全图只用 3 种字号 60 / 34 / 22 px** —— 逐图断言，出现第 4 种即判失败
"""
import base64
import io
import os
import re
import sys
from PIL import Image, ImageChops
from playwright.sync_api import sync_playwright

ROOT = '/Users/liuxinyuan/Desktop/Hermes输出-工作类/对外物料/科研朋友圈_20261009'
IMG = os.path.join(ROOT, 'images')
LOGO = '/Users/liuxinyuan/work/公司资料/logo_transparent.png'
SLOGAN = '守护生命健康，缔造美好生活'
BRAND = '宝锐生物 BIORI BIOTECH'
W = H = 1080

FS = {'标题 60': 60, '正文 34': 34, '注脚 22': 22}
FS_ALLOWED = {'%dpx' % v for v in FS.values()}


def b64(p):
    return 'data:image/png;base64,' + base64.b64encode(io.open(p, 'rb').read()).decode()


def fit(p, maxw=980, maxh=980):
    """裁白边 + 等比缩放（图表自带白边会被误读成「留白多」）"""
    im = Image.open(p).convert('RGB')
    bg = Image.new('RGB', im.size, (255, 255, 255))
    bbox = ImageChops.difference(im, bg).getbbox()
    if bbox:
        pad = 8
        im = im.crop((max(0, bbox[0] - pad), max(0, bbox[1] - pad),
                      min(im.width, bbox[2] + pad), min(im.height, bbox[3] + pad)))
    im.thumbnail((maxw, maxh), Image.LANCZOS)
    f = '/tmp/_fit_%s.jpg' % os.path.basename(p)
    im.save(f, quality=90)
    return 'data:image/jpeg;base64,' + base64.b64encode(io.open(f, 'rb').read()).decode()


LOGO_U = b64(LOGO)
P = {k: fit(os.path.join(IMG, k + '.png')) for k in
     ['p1_i1_x19', 'p1_i2_x20', 'p1_i5_x24', 'p1_i7_x26', 'p2_i2_x30', 'p2_i3_x31',
      'p3_i2_x35', 'p3_i3_x46', 'p3_i4_x47', 'p4_i2_x50', 'p4_i3_x51']}

CSS = """
*{box-sizing:border-box;margin:0;padding:0}
:root{--pri:#1852B6;--pri-d:#0F3A8A;--pri-l:#E8F0FE;--sub:#5A6475}
html,body{background:#fff}
.tile{width:1080px;height:1080px;background:#fff;position:relative;overflow:hidden;display:flex;flex-direction:column;
  font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;color:#16202F}
.topbar{height:96px;flex:0 0 96px;display:flex;align-items:center;justify-content:space-between;padding:0 42px;
  border-bottom:1px solid #E6EBF3;background:#fff}
.topbar img{height:50px;display:block}
.tag{font-size:22px;font-weight:800;color:var(--pri);border:1.6px solid #C9DBF5;border-radius:999px;padding:7px 18px;white-space:nowrap}
.body{flex:1 1 auto;min-height:0;overflow:hidden;padding:34px 42px 26px;display:flex;flex-direction:column}
.foot{height:92px;flex:0 0 92px;display:flex;align-items:center;justify-content:space-between;padding:0 42px;
  border-top:1px solid #E6EBF3;font-size:22px;color:#8A94A6;background:#fff}
.foot b{color:var(--pri);font-weight:800}
h1,h2{font-size:60px;line-height:1.18;font-weight:900;letter-spacing:-.5px;color:var(--pri-d)}
.sub{font-size:34px;color:var(--sub);line-height:1.55}
.chips{display:flex;gap:14px;flex-wrap:wrap}
.chip{background:var(--pri-l);color:var(--pri);font-weight:800;font-size:34px;border-radius:12px;padding:8px 20px}
.hero{flex:1;background:linear-gradient(140deg,#0F3A8A 0%,#1852B6 52%,#2E7BE0 100%);border-radius:22px;color:#fff;
  padding:54px 50px;display:flex;flex-direction:column;justify-content:center;position:relative;overflow:hidden}
.hero .wm{position:absolute;right:-90px;bottom:-120px;width:520px;height:520px;border-radius:50%;
  background:radial-gradient(circle at 30% 30%,rgba(255,255,255,.20),rgba(255,255,255,0) 62%)}
.hero h1{color:#fff}
.hero .sub{color:rgba(255,255,255,.92)}
.hero .chips .chip{background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)}
.hero .chips .chip.s{background:#fff;color:var(--pri-d)}
.card{background:#F7FAFF;border:1px solid #E3EBF7;border-radius:18px;padding:24px 26px}
.row{display:flex;gap:26px;align-items:center}
.pill{display:flex;gap:16px;align-items:flex-start;font-size:34px;line-height:1.45;color:#25303F;margin-bottom:20px}
.pill i{flex:0 0 38px;height:38px;border-radius:11px;background:var(--pri);color:#fff;font-size:22px;font-weight:900;
  display:flex;align-items:center;justify-content:center;font-style:normal;margin-top:4px}
.pill b{font-weight:900;color:#0F3A8A}
.big{font-size:60px;font-weight:900;color:var(--pri);line-height:1.1;letter-spacing:-.5px}
.big small{font-size:34px;font-weight:800;color:var(--sub);margin-left:2px}
.lab{font-size:22px;color:var(--sub);margin-top:10px}
.pic{border-radius:16px;overflow:hidden;background:#fff;border:1px solid #E9EEF6;display:flex;align-items:center;
  justify-content:center;min-height:0}
.pic img{display:block;width:100%;height:100%;object-fit:contain}
.sec-t{font-size:34px;font-weight:900;color:var(--pri-d);display:flex;align-items:center;gap:14px;margin-bottom:14px}
.sec-t::before{content:"";width:8px;height:34px;border-radius:4px;background:var(--pri);display:block}
.qr{width:300px;height:300px;border:8px solid #fff;border-radius:18px;box-shadow:0 6px 26px rgba(16,24,40,.12)}
.hl{background:#FFF7E6;border-left:6px solid #E9A800;border-radius:10px;padding:16px 20px;font-size:34px;color:#7A5300;line-height:1.5}
.long{width:1080px;background:#fff;display:flex;flex-direction:column;
  font-family:"PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;color:#16202F}
.lsec{padding:40px 42px;border-bottom:1px solid #EDF2F9}
.lhero{background:linear-gradient(140deg,#0F3A8A 0%,#1852B6 52%,#2E7BE0 100%);color:#fff;padding:70px 52px;position:relative;overflow:hidden}
.lhero .wm{position:absolute;right:-100px;top:-140px;width:560px;height:560px;border-radius:50%;
  background:radial-gradient(circle at 40% 40%,rgba(255,255,255,.18),rgba(255,255,255,0) 64%)}
.lgrid{display:grid;grid-template-columns:1fr 1fr;gap:26px}
"""


def chrome(body, tag, idx, total):
    return ('<div class="tile">'
            '<div class="topbar"><img src="%s"><span class="tag">%s</span></div>'
            '<div class="body">%s</div>'
            '<div class="foot"><span>%s</span><span><b>%s</b>　·　%d / %d</span></div>'
            '</div>') % (LOGO_U, tag, body, SLOGAN, BRAND, idx, total)


# ═══════════ 卡片构件 ═══════════
EYEBROW = ('<div style="font-size:34px;font-weight:800;letter-spacing:3px;opacity:.9;margin-bottom:20px">'
           '2026 秋季促销 · 实验加速季</div>')


def pills_html(feats):
    return ''.join('<div class="pill"><i>%d</i><div><b>%s</b>　%s</div></div>' % (i, a, b)
                   for i, (a, b) in enumerate(feats, 1))


def t_cover():
    return ('<div class="hero"><div class="wm"></div>' + EYEBROW +
            '<h1>RNA 定量难题，交给 RTQ</h1>'
            '<div class="sub" style="margin-top:22px">高效逆转搭配稳定 qPCR，低丰度样本也能稳定出 Ct</div>'
            '<div class="chips" style="margin-top:36px">'
            '<span class="chip s">BR1D303</span><span class="chip">BR1E102</span>'
            '<span class="chip">5 min 逆转录</span></div></div>')


def t_prod(tag, title, sub, photo, feats):
    return ('<div style="font-size:34px;font-weight:900;color:#1852B6;letter-spacing:1px;margin-bottom:10px">%s</div>'
            '<h2>%s</h2><div class="sub" style="margin:12px 0 24px">%s</div>'
            '<div class="row" style="flex:1;min-height:0;align-items:stretch">'
            '<div class="pic" style="flex:0 0 39%%"><img src="%s"></div>'
            '<div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding-left:6px">%s</div>'
            '</div>') % (tag, title, sub, photo, pills_html(feats))


def t_data(title, sub, chart, stats):
    st = ''.join('<div><div class="big">%s<small>%s</small></div><div class="lab">%s</div></div>' % (v, u, l)
                 for v, u, l in stats)
    return ('<div class="sec-t">%s</div><div class="sub" style="margin-bottom:18px">%s</div>'
            '<div class="card" style="flex:0 0 auto;padding:24px 26px">'
            '<div style="display:flex;justify-content:space-around;text-align:center">%s</div></div>'
            '<div class="pic" style="flex:1;min-height:0;margin-top:18px"><img src="%s"></div>'
            ) % (title, sub, st, chart)


CTA_ROWS = ''.join('<div style="display:flex;align-items:center;gap:16px;padding:14px 0;border-bottom:1px dashed #E3EBF7">'
                   '<span class="chip" style="flex:0 0 176px;text-align:center">%s</span>'
                   '<span style="font-size:34px;color:#25303F">%s</span></div>' % (a, b)
                   for a, b in [('BR1D303', '一管式 cDNA 合成'), ('BR1E102', '快速 qPCR 预混液'),
                                ('BR1C103', '室温无缝克隆')])


def t_cta(title, sub, qr, box, tag='扫码申请试用装'):
    return ('<div class="row" style="flex:1;gap:34px;align-items:center"><div style="flex:1;min-width:0">'
            '<div class="sec-t">%s</div><div class="sub" style="margin-bottom:16px">%s</div>'
            '<div class="card" style="padding:6px 22px;margin-bottom:16px">%s</div>%s</div>'
            '<div style="flex:0 0 320px;text-align:center">'
            '<div style="font-size:34px;font-weight:900;color:#0F3A8A;margin-bottom:16px">%s</div>'
            '<img class="qr" src="%s"></div></div>') % (title, sub, CTA_ROWS, box, tag, qr)


# ═══════════ ① 九宫格（3×3）═══════════
NINE = [
    t_cover(),
    t_prod('BR1D303', '5 min 一管出 cDNA', 'gDNA 清除 + 逆转录同管完成，告别两步操作', P['p1_i1_x19'],
           [('快速', '5 min 完成逆转录'), ('高效', '更高产量 cDNA，更小 Ct'), ('方便', '即用型预混液，单管完成')]),
    t_prod('BR1E102', '30 min 跑完 40 循环', '快速 qPCR 预混液，标准与快速程序皆可', P['p1_i2_x20'],
           [('颜色指示', '绿色染料提示加样'), ('复杂模板', '降解样本 / 高 GC 稳定扩增'),
            ('平台通用', '通用 ROX，免调浓度')]),
    t_data('扩增效率高', '标准曲线：模板 1:10 系列稀释', P['p1_i5_x24'],
           [('100.795', '%', '扩增效率'), ('1.000', '', 'R² 线性度')]),
    t_data('稳定性好', '熔解曲线单峰，特异性良好', P['p1_i7_x26'],
           [('单峰', '', '熔解曲线'), ('无杂峰', '', '特异性')]),
    t_data('耐抑制性好', '5 种常见抑制剂梯度对比（匿名对照）', P['p2_i2_x30'],
           [('5', ' 种', '抑制剂'), ('乙醇等', '', '异丙醇｜苯酚｜腐植酸｜SDS')]),
    t_prod('BR1C103', '室温 5 min 无缝克隆', 'VellixClon™ —— 打破温度限制，克隆随时随地', P['p3_i2_x35'],
           [('室温快速', '25℃ 即可反应，无需冰盒'), ('模板兼容广', '单片段构建适用'),
            ('操作便利', '桌面配完直接上机')]),
    t_data('兼容性数据', '未纯化 PCR 产物 · 反应温度 25℃ · 反应时间', P['p4_i3_x51'],
           [('25', '℃', '通用反应温度'), ('未纯化', '', 'PCR 产物可直接用')]),
    t_cta('三款均开放试用', 'RNA → cDNA → 结果 一套跑通；载体构建不用再守冰盒', P['p2_i3_x31'],
          '<div class="hl">BR1D303 ｜ BR1E102 ｜ BR1C103 三款均开放试用</div>'),
]


# ═══════════ ② 单张长图 ═══════════
def l_stats(stats):
    return ''.join('<div><div class="big">%s<small>%s</small></div><div class="lab">%s</div></div>' % (v, u, l)
                   for v, u, l in stats)


def l_prod(tag, title, sub, photo, feats, bg=''):
    return ('<div class="lsec" style="display:flex;gap:32px;align-items:center;%s">'
            '<div class="pic" style="flex:0 0 40%%;height:400px"><img src="%s"></div>'
            '<div style="flex:1"><div style="font-size:34px;font-weight:900;color:#1852B6;margin-bottom:10px">%s</div>'
            '<h2>%s</h2><div class="sub" style="margin:12px 0 22px">%s</div>%s</div></div>'
            ) % (bg, photo, tag, title, sub, pills_html(feats))


def l_prod_mini(tag, title, sub, photo, feats, ph=240):
    """精简版：产品卡（左右并排用，图更小、要点更少）"""
    return ('<div><div style="font-size:34px;font-weight:900;color:#1852B6;margin-bottom:6px">%s</div>'
            '<h2>%s</h2><div class="sub" style="margin:8px 0 14px">%s</div>'
            '<div class="pic" style="height:%dpx"><img src="%s"></div>'
            '<div style="margin-top:16px">%s</div></div>'
            ) % (tag, title, sub, ph, photo, pills_html(feats))


def l_data2(t1, s1, c1, st1, t2, s2, c2):
    return ('<div class="lsec"><div class="lgrid">'
            '<div><div class="sec-t">%s</div><div class="sub" style="margin-bottom:18px">%s</div>%s'
            '<div class="pic" style="height:430px;margin-top:18px"><img src="%s"></div></div>'
            '<div><div class="sec-t">%s</div><div class="sub" style="margin-bottom:18px">%s</div>'
            '<div class="pic" style="height:430px;margin-top:18px"><img src="%s"></div></div>'
            '</div></div>') % (t1, s1, l_stats(st1), c1, t2, s2, c2)


LONG = ''.join([
    '<div class="long">',
    '<div class="topbar"><img src="%s"><span class="tag">科研好物 · 2026.10</span></div>' % LOGO_U,
    '<div class="lhero"><div class="wm"></div>' + EYEBROW +
    '<div style="font-size:60px;font-weight:900;color:#fff;line-height:1.18">RNA 定量难题，交给 RTQ</div>'
    '<div class="sub" style="color:rgba(255,255,255,.92);margin-top:22px">'
    '高效逆转搭配稳定 qPCR，低丰度样本也能稳定出 Ct</div>'
    '<div class="chips" style="margin-top:34px">'
    '<span class="chip" style="background:#fff;color:#0F3A8A">BR1D303</span>'
    '<span class="chip" style="background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)">BR1E102</span>'
    '<span class="chip" style="background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)">5 min 逆转录</span>'
    '</div></div>',
    l_prod('BR1D303', '5 min 一管出 cDNA', 'gDNA 清除 + 逆转录同管完成，告别两步操作', P['p1_i1_x19'],
           [('快速', '5 min 完成逆转录'), ('高效', '更高产量 cDNA，更小 Ct'), ('方便', '即用型预混液，单管完成')]),
    l_prod('BR1E102', '30 min 跑完 40 循环', '快速 qPCR 预混液，标准与快速程序皆可', P['p1_i2_x20'],
           [('颜色指示', '绿色染料提示加样'), ('复杂模板', '降解样本 / 高 GC 稳定扩增'),
            ('平台通用', '通用 ROX，免调浓度')], bg='background:#FAFCFF'),
    l_data2('扩增效率高', '标准曲线：模板 1:10 系列稀释', P['p1_i5_x24'],
            [('100.795', '%', '扩增效率'), ('1.000', '', 'R² 线性度')],
            '稳定性好', '熔解曲线单峰，特异性良好', P['p1_i7_x26']),
    '<div class="lsec"><div class="sec-t">耐抑制性好</div>'
    '<div class="sub" style="margin-bottom:20px">5 种常见抑制剂梯度对比（宝锐 vs 供应商 A / B，匿名对照）</div>'
    '<div class="pic" style="height:520px"><img src="%s"></div></div>' % P['p2_i2_x30'],
    l_prod('BR1C103', '室温 5 min 无缝克隆', 'VellixClon™ —— 打破温度限制，克隆随时随地', P['p3_i2_x35'],
           [('室温快速', '25℃ 即可反应，无需冰盒'), ('模板兼容广', '单片段构建适用'),
            ('操作便利', '桌面配完直接上机')]),
    '<div class="lsec"><div class="sec-t">兼容性数据</div>'
    '<div class="sub" style="margin-bottom:20px">未纯化 PCR 产物 · 反应温度 25℃ · 反应时间与低投入量</div>'
    '<div class="lgrid">'
    '<div class="card" style="text-align:center"><div class="big">25<small>℃</small></div>'
    '<div class="lab">通用反应温度</div>'
    '<div class="pic" style="height:270px;margin-top:16px"><img src="%s"></div></div>'
    '<div class="card" style="text-align:center"><div class="big">未纯化</div>'
    '<div class="lab">PCR 产物可直接用</div>'
    '<div class="pic" style="height:270px;margin-top:16px"><img src="%s"></div></div>'
    '</div></div>' % (P['p3_i3_x46'], P['p4_i3_x51']),
    '<div class="lsec" style="display:flex;gap:34px;align-items:center">'
    '<div style="flex:1"><div class="sec-t">三款均开放试用</div>'
    '<div class="sub" style="margin-bottom:16px">RNA → cDNA → 结果 一套跑通；载体构建不用再守冰盒</div>'
    '<div class="card" style="padding:6px 22px">' + CTA_ROWS + '</div>'
    '</div>'
    '<div style="flex:0 0 320px;text-align:center">'
    '<div style="font-size:34px;font-weight:900;color:#0F3A8A;margin-bottom:16px">扫码申请试用装</div>'
    '<img class="qr" src="%s"></div></div>' % P['p2_i3_x31'],
    '<div class="foot" style="flex:0 0 92px"><span>%s</span><span><b>%s</b></span></div>' % (SLOGAN, BRAND),
    '</div>',
])


LONG2 = ''.join([
    '<div class="long">',
    '<div class="topbar"><img src="%s"><span class="tag">科研好物 · 2026.10</span></div>' % LOGO_U,
    '<div class="lhero" style="padding:34px 52px"><div class="wm"></div>' + EYEBROW +
    '<div style="font-size:60px;font-weight:900;color:#fff;line-height:1.18">RNA 定量难题，交给 RTQ</div>'
    '<div class="sub" style="color:rgba(255,255,255,.92);margin-top:14px">'
    '高效逆转搭配稳定 qPCR，低丰度样本也能稳定出 Ct</div>'
    '<div class="chips" style="margin-top:20px">'
    '<span class="chip" style="background:#fff;color:#0F3A8A">BR1D303</span>'
    '<span class="chip" style="background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)">BR1E102</span>'
    '<span class="chip" style="background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)">BR1C103</span>'
    '</div></div>',
    '<div class="lsec" style="padding:28px 42px"><div class="sec-t">两款主力 · 数据说话</div>'
    '<div class="sub" style="margin-bottom:16px">一管完成逆转录，快速程序跑完 qPCR</div>'
    '<div class="lgrid">'
    + l_prod_mini('BR1D303', '5 min 一管出 cDNA', 'gDNA 清除 + 逆转录同管完成', P['p1_i1_x19'],
                  [('快速', '5 min 完成逆转录'), ('高效', '更高产量 cDNA，更小 Ct')])
    + l_prod_mini('BR1E102', '30 min 跑完 40 循环', '快速 qPCR 预混液，两种程序皆可', P['p1_i2_x20'],
                  [('颜色指示', '绿色染料提示加样'), ('平台通用', '通用 ROX，免调浓度')])
    + '</div>'
    '<div class="card" style="padding:18px 20px;margin:20px 0">'
    '<div style="display:flex;justify-content:space-around;text-align:center">'
    + l_stats([('100.795', '%', '扩增效率'), ('1.000', '', 'R² 线性度'),
               ('5', ' 种', '抑制剂覆盖'), ('25', '℃', '克隆反应温度')])
    + '</div></div>'
    '<div class="lgrid">'
    '<div class="pic" style="height:230px"><img src="%s"></div>'
    '<div class="pic" style="height:230px"><img src="%s"></div>'
    '</div></div>' % (P['p1_i5_x24'], P['p2_i2_x30']),
    '<div class="lsec" style="padding:26px 42px;display:flex;gap:30px;align-items:center">'
    '<div style="flex:1"><div class="sec-t">三款均开放试用</div>'
    '<div class="chips" style="margin-bottom:14px">'
    '<span class="chip">BR1D303 一管式 cDNA</span><span class="chip">BR1E102 快速 qPCR</span>'
    '<span class="chip">BR1C103 室温克隆</span></div>'
    '</div>'
    '<div style="flex:0 0 250px;text-align:center">'
    '<div style="font-size:34px;font-weight:900;color:#0F3A8A;margin-bottom:10px">扫码申请试用装</div>'
    '<img class="qr" style="width:210px;height:210px" src="%s"></div></div>' % P['p2_i3_x31'],
    '<div class="foot" style="flex:0 0 92px"><span>%s</span><span><b>%s</b></span></div>' % (SLOGAN, BRAND),
    '</div>',
])


LONG3 = ''.join([
    '<div class="long">',
    '<div class="topbar"><img src="%s"><span class="tag">科研好物 · 2026.10</span></div>' % LOGO_U,
    '<div class="lhero" style="padding:30px 52px"><div class="wm"></div>' + EYEBROW +
    '<div style="font-size:60px;font-weight:900;color:#fff;line-height:1.18">载体构建，不用再守冰盒</div>'
    '<div class="sub" style="color:rgba(255,255,255,.92);margin-top:14px">'
    'VellixClon™ 室温 5 min 无缝克隆 —— 打破温度限制，克隆随时随地</div>'
    '<div class="chips" style="margin-top:20px">'
    '<span class="chip" style="background:#fff;color:#0F3A8A">BR1C103</span>'
    '<span class="chip" style="background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)">室温 5 min</span>'
    '<span class="chip" style="background:rgba(255,255,255,.16);color:#fff;border:1.5px solid rgba(255,255,255,.42)">同源重组无缝克隆</span>'
    '</div></div>',
    '<div class="lsec" style="padding:22px 42px"><div class="sec-t">核心产品 · VellixClon™</div>'
    '<div class="sub" style="margin-bottom:18px">Ambient Seamless Cloning Mix —— 单片段同源重组，一步到位</div>'
    '<div class="row" style="align-items:stretch">'
    '<div class="pic" style="flex:0 0 30%%;height:264px"><img src="%s"></div>'
    '<div style="flex:1;display:flex;flex-direction:column;justify-content:center;padding-left:26px">%s</div>'
    '</div>'
    '<div class="card" style="padding:18px 20px;margin:16px 0 0">'
    '<div style="display:flex;justify-content:space-around;text-align:center">'
    % (P['p3_i2_x35'], pills_html([
        ('室温快速', '25℃ 即可反应，无需冰盒'),
        ('模板兼容广', '单片段载体构建适用'),
        ('操作便利', '桌面配完直接上机')]))
    + l_stats([('25', '℃', '通用反应温度'), ('5', ' min', '室温反应时长'),
               ('未纯化', '', 'PCR 产物可直接用'), ('单片段', '', '适用构建场景')])
    + '</div></div></div>',
    '<div class="lsec" style="padding:22px 42px;background:#FAFCFF"><div class="sec-t">实测数据</div>'
    '<div class="sub" style="margin-bottom:12px">反应温度测试 · 25℃ 可作为该试剂较通用的反应温度</div>'
    '<div class="pic" style="height:288px"><img src="%s"></div>'
    '<div class="sub" style="margin:18px 0 12px">未纯化 PCR 产物测试 · 对未经纯化回收的 PCR 产物具有良好的兼容性</div>'
    '<div class="pic" style="height:288px"><img src="%s"></div>'
    '</div>' % (P['p3_i3_x46'], P['p4_i3_x51']),
    '<div class="lsec" style="padding:26px 42px;display:flex;gap:30px;align-items:center">'
    '<div style="flex:1"><div class="sec-t">BR1C103 开放试用</div>'
    '<div class="chips" style="margin-bottom:14px">'
    '<span class="chip">BR1C103 室温同源重组</span><span class="chip">BR1D303 一管式 cDNA</span>'
    '<span class="chip">BR1E102 快速 qPCR</span></div>'
    '</div>'
    '<div style="flex:0 0 250px;text-align:center">'
    '<div style="font-size:34px;font-weight:900;color:#0F3A8A;margin-bottom:10px">扫码申请试用装</div>'
    '<img class="qr" style="width:210px;height:210px" src="%s"></div></div>' % P['p2_i3_x31'],
    '<div class="foot" style="flex:0 0 92px"><span>%s</span><span><b>%s</b></span></div>' % (SLOGAN, BRAND),
    '</div>',
])


def check_fs(html, label):
    sizes = set(re.findall(r'font-size:\s*([\d.]+px)', html))
    bad = sizes - FS_ALLOWED
    if bad:
        print('  ❌ 字号越界 [%s]：%s（仅允许 %s）' % (label, sorted(bad), sorted(FS_ALLOWED)))
        return False
    print('  ✅ 字号守卫 [%s]：%s' % (label, ' / '.join(sorted(sizes, key=lambda x: float(x[:-2])))))
    return True


ok_all = True
with sync_playwright() as pw:
    br = pw.chromium.launch(channel='chrome', headless=True)
    pg = br.new_page(viewport={'width': W, 'height': H}, device_scale_factor=1)

    # ── ① 九宫格 ──
    outdir = os.path.join(ROOT, '九宫格_方案C')
    os.makedirs(outdir, exist_ok=True)
    n = len(NINE)
    files = []
    for i, body in enumerate(NINE, 1):
        html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>%s</style></head><body>%s</body></html>' % (
            CSS, chrome(body, '科研好物 · 2026.10', i, n))
        io.open('/tmp/_tile.html', 'w', encoding='utf-8').write(html)
        pg.goto('file:///tmp/_tile.html', wait_until='load')
        pg.wait_for_timeout(300)
        ok_all &= check_fs(html, '九宫格 C%d' % i)
        m = pg.evaluate("() => {var b=document.querySelector('.body'),t=document.querySelector('.tile');"
                        "return [b.scrollHeight,b.clientHeight,t.scrollHeight,t.scrollWidth];}")
        if m[0] > m[1] + 2 or m[2] > H + 2 or m[3] > W + 2:
            print('  ⚠️ 溢出 C%d  body %d/%d  tile %dx%d' % (i, m[0], m[1], m[3], m[2]))
            ok_all = False
        out = os.path.join(outdir, 'C%d_9.png' % i)
        pg.screenshot(path=out, clip={'x': 0, 'y': 0, 'width': W, 'height': H})
        files.append(out)

    sheet = Image.new('RGB', (3 * 392 + 12, 3 * 392 + 12), '#E9EEF6')
    for i, f in enumerate(files):
        im = Image.open(f).convert('RGB')
        im.thumbnail((380, 380), Image.LANCZOS)
        sheet.paste(im, (12 + (i % 3) * 392, 12 + (i // 3) * 392))
    prev = os.path.join(outdir, 'preview_九宫格.png')
    sheet.save(prev)

    # ── ② 单张长图 ──
    html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>%s</style></head><body>%s</body></html>' % (CSS, LONG)
    io.open('/tmp/_long.html', 'w', encoding='utf-8').write(html)
    pg.set_viewport_size({'width': W, 'height': 1200})
    pg.goto('file:///tmp/_long.html', wait_until='load')
    pg.wait_for_timeout(600)
    ok_all &= check_fs(html, '长图')
    dim = pg.evaluate("() => {var t=document.querySelector('.long');return [t.scrollWidth,t.scrollHeight];}")
    if dim[0] > W + 2:
        print('  ⚠️ 长图超宽 %dx%d' % (dim[0], dim[1]))
        ok_all = False
    lout = os.path.join(ROOT, '长图_单张_方案.png')
    pg.screenshot(path=lout, full_page=True)

    # ── ③ 精简长图（约 1:2）──
    html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>%s</style></head><body>%s</body></html>' % (CSS, LONG2)
    io.open('/tmp/_long2.html', 'w', encoding='utf-8').write(html)
    pg.goto('file:///tmp/_long2.html', wait_until='load')
    pg.wait_for_timeout(600)
    ok_all &= check_fs(html, '精简长图')
    dim2 = pg.evaluate("() => {var t=document.querySelector('.long');return [t.scrollWidth,t.scrollHeight];}")
    if dim2[0] > W + 2:
        print('  ⚠️ 精简长图超宽 %dx%d' % (dim2[0], dim2[1]))
        ok_all = False
    lout2 = os.path.join(ROOT, '长图_精简版.png')
    pg.screenshot(path=lout2, full_page=True)

    # ── ④ 精简长图 · BR1C103 同源重组主推 ──
    html = '<!DOCTYPE html><html><head><meta charset="utf-8"><style>%s</style></head><body>%s</body></html>' % (CSS, LONG3)
    io.open('/tmp/_long3.html', 'w', encoding='utf-8').write(html)
    pg.goto('file:///tmp/_long3.html', wait_until='load')
    pg.wait_for_timeout(600)
    ok_all &= check_fs(html, '精简长图·1C103')
    dim3 = pg.evaluate("() => {var t=document.querySelector('.long');return [t.scrollWidth,t.scrollHeight];}")
    if dim3[0] > W + 2:
        print('  ⚠️ 1C103 长图超宽 %dx%d' % (dim3[0], dim3[1]))
        ok_all = False
    lout3 = os.path.join(ROOT, '长图_精简版_1C103主推.png')
    pg.screenshot(path=lout3, full_page=True)
    br.close()

print('\n九宫格预览：%s' % prev)
print('长图（完整版）：%s  %dx%d（1:%.2f）' % (lout, dim[0], dim[1], dim[1] / float(dim[0])))
print('长图（精简·RTQ）：%s  %dx%d（1:%.2f）' % (lout2, dim2[0], dim2[1], dim2[1] / float(dim2[0])))
print('长图（精简·1C103）：%s  %dx%d（1:%.2f）' % (lout3, dim3[0], dim3[1], dim3[1] / float(dim3[0])))
print('\n%s' % ('✅ 全部通过（字号仅 3 种 / 无溢出 / 无超宽）' if ok_all else '❌ 有失败项，见上'))
sys.exit(0 if ok_all else 1)
