#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
宝锐诊断原料 · 客户宣讲 PPT 生成器 v4（科技风动画版）
输入：deck_data.json
输出：宝锐诊断原料_客户宣讲_科技风动画版_41页_可编辑.html

设计原则
- 零外部依赖（无 CDN / 无框架），单文件，离线可开
- 瑞士风基座：宝锐蓝 #002FA7 + 白底大留白 + 极简排版
- 动画 4 套：A 流动数据线｜B 分子粒子网络｜C DNA 双螺旋｜D 全页分子背景
- 动画只出现在 封面 / 章节承接页 / 3 个技术页；正文页保持纯白
- 全部文本 contenteditable，可直接在浏览器里改
"""
import json, os, html

BASE = os.path.dirname(os.path.abspath(__file__))
DATA = json.load(open(os.path.join(BASE, 'deck_data.json'), encoding='utf-8'))
OUT = os.path.join(BASE, '宝锐诊断原料_客户宣讲_科技风动画版_41页_可编辑.html')

def esc(s): return html.escape(str(s), quote=False)

pages = []           # 页面 HTML 列表
page_titles = []     # 导航用

def add(title, body, cls='', nav=None):
    # 每页自动附加审稿 UI（徽章 + 审稿条），编号由渲染时回填
    pages.append({'title': title, 'body': body, 'cls': cls, 'nav': nav or title})
    page_titles.append(nav or title)

def review_bar(n):
    """每页的审稿 UI：右上状态徽章 + 底部审稿条"""
    return f"""<div class="rvBadge" data-s=""><span>●</span><span class="txt">待确认</span></div>
<div class="rvbar">
  <span class="rvt" contenteditable="false">本页审稿</span>
  <span class="rvsel">
    <button class="rvbtn" data-s="ok"  onclick="rvSetStatus('ok')">✓ 已确认</button>
    <button class="rvbtn" data-s="fix" onclick="rvSetStatus('fix')">✎ 需修改</button>
  </span>
  <input class="rvnote" placeholder="备注（改什么 / 为什么，可直接回车）" oninput="rvSetNote(this.value)">
  <span class="rvsave"></span>
</div>"""

# ============================================================
#  CSS
# ============================================================
CSS = r"""
:root{
  --blue:#002FA7; --blue2:#1E5BFF; --cyan:#00D4FF; --ink:#0a0a0a; --ink2:#3a3a3c;
  --grey:#6e6e73; --tint:#f5f5f7; --line:rgba(0,47,167,.14); --line2:rgba(0,0,0,.07);
  --fnt:"Microsoft YaHei","PingFang SC","Helvetica Neue",Arial,sans-serif;
  --logo:url('data:image/png;base64,LOGO_B64');
  --t-hero:min(5.4vw,8.2vh); --t-sec:min(3.9vw,6.0vh); --t-h2:min(2.5vw,3.9vh);
  --t-h3:min(1.55vw,2.4vh); --t-sub:min(1.45vw,2.25vh); --t-body:min(1.16vw,1.82vh);
  --t-note:min(.96vw,1.5vh); --t-label:min(.86vw,1.35vh);
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;background:#e9ecf2;font-family:var(--fnt);color:var(--ink)}
body{overflow:hidden}
.deck{position:relative;width:100vw;height:100vh;overflow:hidden}

/* ---------- 幻灯片 ---------- */
.slide{
  position:absolute;inset:0;background:#fff;
  padding:6.2vh 5.6vw 5vh;display:none;flex-direction:column;
  overflow:hidden;
}
.slide.on{display:flex;animation:fadeIn .42s ease-out}
@keyframes fadeIn{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* ---------- 顶栏（宝锐规范：白顶栏 + Logo + BIORI BIOTECH） ---------- */
.topbar{
  position:absolute;top:0;left:0;right:0;height:5.4vh;min-height:38px;z-index:60;
  display:flex;align-items:center;justify-content:space-between;
  padding:0 2.4vw;background:rgba(255,255,255,.94);backdrop-filter:blur(8px);
  border-bottom:1px solid var(--line);
}
.tb-left{display:flex;align-items:center;gap:.62vw}
.tb-logo{
  width:7.2vh;height:2.44vh;min-width:64px;min-height:22px;
  background:var(--logo) left center/contain no-repeat;
  flex:0 0 auto;
}
.tb-name{font-size:1.42vh;font-weight:700;color:var(--blue);letter-spacing:.4px}
.tb-en{font-size:1.02vh;color:var(--grey);letter-spacing:1.5px}
.tb-slogan{font-size:1.0vh;color:var(--grey);letter-spacing:1.2px;font-weight:600}
.tb-slogan b{color:var(--blue);font-weight:700}
.tb-mid{font-size:1.16vh;color:var(--ink2);font-weight:600}
.tb-mid em{font-style:normal;color:var(--blue)}
.tb-right{font-size:1.02vh;color:var(--grey);letter-spacing:.5px}

/* ---------- 标题体系 ---------- */
.kicker{font-size:var(--t-label);font-weight:700;letter-spacing:2.4px;color:var(--blue);text-transform:uppercase;margin-bottom:1.1vh}
h1.hero{font-size:var(--t-hero);font-weight:800;line-height:1.14;letter-spacing:-.8px}
h2.sec{font-size:var(--t-sec);font-weight:800;line-height:1.16;letter-spacing:-.6px}
h3.h3{font-size:var(--t-h2);font-weight:800;line-height:1.24;letter-spacing:-.3px}
.sub{font-size:var(--t-h3);font-weight:600;color:var(--ink2);line-height:1.42;margin-top:.9vh}
.rule{width:4.6vw;height:3px;background:var(--blue);border-radius:2px;margin:1.5vh 0 2.2vh}
.rule.cy{background:linear-gradient(90deg,var(--blue),var(--cyan))}
.lead{font-size:var(--t-body);color:var(--ink2);line-height:1.92}

/* ---------- 布局 ---------- */
.grow{flex:1;min-height:0;display:flex;flex-direction:column}
.row{display:flex;gap:2.2vw}
.col{flex:1;min-width:0}
.card{
  background:#fff;border:1px solid var(--line);border-radius:12px;
  padding:2.1vh 1.5vw;box-shadow:0 2px 16px rgba(0,47,167,.045);
}
.card.blue{background:linear-gradient(160deg,rgba(0,47,167,.035),rgba(0,212,255,.02))}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:1.5vw}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:1.3vw}
.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:1.1vw}

/* ---------- 列表 / 项目符号 ---------- */
.ul{list-style:none;display:flex;flex-direction:column;gap:1.15vh}
.ul li{position:relative;padding-left:1.5vw;font-size:var(--t-body);line-height:1.8;color:var(--ink2)}
.ul li::before{content:'';position:absolute;left:.32vw;top:.78em;width:.42vw;height:.42vw;min-width:4px;min-height:4px;border-radius:50%;background:var(--blue)}
.ul.num{counter-reset:n}
.ul.num li::before{counter-increment:n;content:counter(n);background:none;color:var(--blue);font-weight:800;font-size:var(--t-note);width:auto;height:auto;top:0;left:0}

/* ---------- 数据表 ---------- */
table.dt{width:100%;border-collapse:collapse;font-size:var(--t-note)}
table.dt th{
  background:rgba(0,47,167,.055);color:var(--blue);font-weight:700;
  text-align:left;padding:1.05vh .68vw;border-bottom:1.5px solid var(--line);
  font-size:var(--t-label);letter-spacing:.4px;white-space:nowrap;
}
table.dt td{padding:1.0vh .68vw;border-bottom:1px solid var(--line2);color:var(--ink2);line-height:1.6;vertical-align:top}
table.dt tr:last-child td{border-bottom:none}
table.dt td.k{font-weight:700;color:var(--ink);white-space:nowrap}
table.dt td.v{color:var(--ink2)}
table.dt td.src{color:var(--grey);font-size:calc(var(--t-label) * .94);white-space:nowrap}
table.dt tr.hl td{background:rgba(0,212,255,.06)}
table.dt td.num{font-variant-numeric:tabular-nums;text-align:right;font-weight:600}

/* ---------- 图表（纯 CSS/SVG 柱状） ---------- */
.chart{display:flex;align-items:flex-end;gap:1.1vw;height:30vh;padding:0 .4vw;border-bottom:1.5px solid var(--line);position:relative}
.bargrp{flex:1;display:flex;align-items:flex-end;justify-content:center;gap:.22vw;height:100%;position:relative}
.bar{
  width:100%;max-width:1.5vw;border-radius:4px 4px 0 0;
  background:linear-gradient(180deg,var(--blue2),var(--blue));
  transform-origin:bottom;animation:growBar .9s cubic-bezier(.2,.8,.3,1) both;
  position:relative;
}
.bar.c{background:linear-gradient(180deg,var(--cyan),#0086a8)}
.bar.g{background:linear-gradient(180deg,#9aa4b2,#6e6e73)}
.bar.dim{opacity:.42}
@keyframes growBar{from{transform:scaleY(0)}to{transform:scaleY(1)}}
.blabel{position:absolute;bottom:-3.1vh;left:50%;transform:translateX(-50%);font-size:var(--t-label);color:var(--grey);white-space:nowrap;font-weight:600}
.bval{position:absolute;top:-2.15vh;left:50%;transform:translateX(-50%);font-size:var(--t-label);color:var(--blue);font-weight:700;white-space:nowrap}
.legend{display:flex;gap:1.5vw;flex-wrap:wrap;margin-top:4.4vh;font-size:var(--t-label);color:var(--ink2)}
.legend span{display:flex;align-items:center;gap:.34vw;font-weight:600}
.legend i{width:.78vw;height:.78vw;min-width:9px;min-height:9px;border-radius:3px;display:inline-block}

/* ---------- 角标 / 标签 ---------- */
.pill{
  display:inline-block;padding:.45vh .72vw;border-radius:20px;font-size:var(--t-label);
  background:rgba(0,47,167,.07);color:var(--blue);font-weight:700;letter-spacing:.6px;
}
.pill.cy{background:rgba(0,212,255,.13);color:#007d9c}
.pill.g{background:rgba(0,160,90,.1);color:#0a7f3f}
.src-tag{font-size:calc(var(--t-label) * .92);color:var(--grey);font-weight:600;letter-spacing:.3px}

/* ---------- 承接页（方案 D） ---------- */
.slide.stage{padding:0}
.slide.stage .canvasHost{position:absolute;inset:0;z-index:1}
.slide.stage .stageTxt{
  position:relative;z-index:3;flex:1;display:flex;flex-direction:column;
  align-items:center;justify-content:center;text-align:center;padding:8vh 6vw;
}
.slide.stage h2{font-size:var(--t-hero);font-weight:800;color:var(--blue);letter-spacing:-.6px;text-shadow:0 2px 26px rgba(255,255,255,.95)}
.slide.stage .st-sub{margin-top:1.9vh;font-size:var(--t-h3);color:var(--ink2);letter-spacing:.8px;font-weight:600;max-width:60vw;line-height:1.7}
.slide.stage .u{width:5.4vw;height:3px;background:var(--blue);margin:2.4vh auto 0;border-radius:2px}
.slide.stage .st-num{
  position:absolute;top:9vh;left:6vw;font-size:min(13vw,19vh);font-weight:800;
  color:rgba(0,47,167,.055);line-height:1;z-index:2;letter-spacing:-2px;
}

/* ---------- 封面 ---------- */
.cover{position:absolute;inset:0;z-index:3;display:flex;flex-direction:column;justify-content:center;padding:0 7vw}
.cover .logoRow{display:flex;align-items:center;gap:1.5vw;margin-bottom:3.4vh}
.cover .logoRow .tb-logo{width:20vh;height:6.8vh;min-width:180px;min-height:60px}
.cover .logoRow .cslogan{font-size:min(1.5vw,2.3vh);font-weight:700;color:var(--blue);letter-spacing:2.2px}
.cover .logoRow .cslogan b{color:var(--cyan);font-weight:800}
.cover h1{font-size:min(6.2vw,9.4vh);font-weight:800;line-height:1.1;letter-spacing:-1.4px}
.cover h1 em{font-style:normal;background:linear-gradient(96deg,var(--blue),var(--cyan));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent}
.cover .csub{margin-top:2.6vh;font-size:min(1.75vw,2.7vh);color:var(--ink2);font-weight:600;letter-spacing:.6px}
.cover .cline{width:6.4vw;height:3px;background:linear-gradient(90deg,var(--blue),var(--cyan));margin:3vh 0 2.6vh;border-radius:2px}
.cover .cmeta{font-size:var(--t-body);color:var(--grey);line-height:2}
.cover .chips{display:flex;gap:.7vw;flex-wrap:wrap;margin-top:2.2vh}
.cover .chips span{padding:.55vh .85vw;border:1px solid var(--line);border-radius:20px;font-size:var(--t-label);color:var(--blue);font-weight:700}

/* ---------- 7 场景总览 ---------- */
.scGrid{display:grid;grid-template-columns:repeat(4,1fr);gap:1.05vw;margin-top:.6vh}
.scCard{
  background:#fff;border:1px solid var(--line);border-radius:11px;padding:1.7vh 1.0vw;
  position:relative;overflow:hidden;transition:.22s;
}
.scCard:hover{transform:translateY(-3px);box-shadow:0 8px 26px rgba(0,47,167,.13);border-color:rgba(0,47,167,.34)}
.scCard .no{font-size:var(--t-note);font-weight:800;color:var(--cyan);letter-spacing:1px}
.scCard .nm{font-size:var(--t-h3);font-weight:800;color:var(--ink);margin-top:.5vh;line-height:1.3}
.scCard .ds{font-size:var(--t-label);color:var(--grey);margin-top:.62vh;line-height:1.62}
.scCard::after{content:'';position:absolute;left:0;top:0;width:3px;height:100%;background:linear-gradient(180deg,var(--blue),var(--cyan))}

/* ---------- 场景页编号 ---------- */
.scHead{display:flex;align-items:center;gap:.9vw;margin-bottom:1.5vh}
.scHead .big{font-size:min(4.4vw,6.6vh);font-weight:800;color:rgba(0,47,167,.13);line-height:1;letter-spacing:-1px}
.scHead .nm{font-size:var(--t-sec);font-weight:800;letter-spacing:-.5px}
.kv{display:flex;flex-direction:column;gap:0}
.kv .kvrow{display:flex;gap:1.1vw;padding:1.1vh 0;border-bottom:1px solid var(--line2)}
.kv .kvrow:last-child{border-bottom:none}
.kv .k{width:9.6vw;flex:0 0 9.6vw;font-weight:700;color:var(--ink);font-size:var(--t-note);line-height:1.6}
.kv .v{flex:1;color:var(--ink2);font-size:var(--t-note);line-height:1.72}
.kv .s{flex:0 0 auto;color:var(--grey);font-size:calc(var(--t-label)*.94);white-space:nowrap;padding-top:.18em}

/* ---------- 时间线（方案 A 流程） ---------- */
.tl{position:relative;flex:1;display:flex;align-items:center;min-height:0}
.tl svg{width:100%;height:100%;overflow:visible}
.flowline{fill:none;stroke:var(--blue);stroke-width:2;stroke-dasharray:9 11;animation:flowA 2.6s linear infinite}
.flowline.c2{stroke:var(--cyan);animation-duration:3.2s}
@keyframes flowA{to{stroke-dashoffset:-200}}
.tlnode{fill:#fff;stroke:var(--blue);stroke-width:2.4}
.tlnode.hi{fill:var(--blue);stroke:var(--cyan)}
.tlY{font-size:1.42vh;font-weight:800;fill:var(--blue);font-family:var(--fnt)}
.tlT{font-size:1.06vh;fill:var(--grey);font-family:var(--fnt)}

/* ---------- 页脚 / 导航 ---------- */
.footbar{
  position:absolute;bottom:0;left:0;right:0;height:4.4vh;min-height:30px;z-index:60;
  display:flex;align-items:center;justify-content:space-between;
  padding:0 2.4vw;font-size:var(--t-label);color:var(--grey);
  background:linear-gradient(180deg,rgba(255,255,255,0),rgba(255,255,255,.9));
}
.footbar .fm{color:var(--ink2);font-weight:600}
.footbar .fm em{font-style:normal;color:var(--blue);font-weight:800}
.footbar .fsl{color:var(--blue);font-weight:700;letter-spacing:1.4px}
.pgnum{font-variant-numeric:tabular-nums;font-weight:700;color:var(--blue)}
.navbtn{
  position:absolute;top:50%;transform:translateY(-50%);z-index:70;
  width:3.1vh;height:3.1vh;min-width:26px;min-height:26px;border-radius:50%;border:1px solid var(--line);
  background:rgba(255,255,255,.9);color:var(--blue);cursor:pointer;
  display:flex;align-items:center;justify-content:center;font-size:1.5vh;font-weight:800;
  opacity:0;transition:.22s;
}
.slide:hover ~ .navbtn, .deck:hover .navbtn{opacity:.72}
.navbtn:hover{opacity:1;background:var(--blue);color:#fff}
.navbtn.prev{left:1.1vw}
.navbtn.next{right:1.1vw}
.progress{position:absolute;bottom:0;left:0;height:2px;background:linear-gradient(90deg,var(--blue),var(--cyan));z-index:80;transition:width .3s}

/* ---------- 缩略导航（按 N 呼出） ---------- */
.toc{
  position:fixed;inset:0;z-index:200;background:rgba(10,16,40,.93);backdrop-filter:blur(10px);
  display:none;padding:6vh 6vw;overflow-y:auto;
}
.toc.on{display:block}
.toc h3{color:#fff;font-size:2.1vh;margin-bottom:2.6vh;font-weight:800;letter-spacing:.6px}
.toc .tgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(15vw,1fr));gap:1.1vh}
.toc a{
  display:block;color:rgba(255,255,255,.76);text-decoration:none;font-size:1.32vh;
  padding:1.05vh .8vw;border-radius:7px;border:1px solid rgba(255,255,255,.13);
  line-height:1.45;transition:.16s;
}
.toc a:hover{background:rgba(0,212,255,.16);color:#fff;border-color:rgba(0,212,255,.5)}
.toc a b{color:var(--cyan);font-weight:800;margin-right:.4em}

/* ---------- 编辑提示 ---------- */
[contenteditable]:focus{outline:2px solid rgba(0,212,255,.75);outline-offset:2px;border-radius:3px}
.editHint{
  position:fixed;bottom:5vh;right:1.4vw;z-index:90;
  background:rgba(0,47,167,.9);color:#fff;font-size:1.12vh;padding:.75vh .9vw;
  border-radius:7px;letter-spacing:.4px;opacity:0;transition:.3s;pointer-events:none;
}
.editHint.on{opacity:1}

@media print{
  body{overflow:visible;background:#fff}
  .deck{height:auto;overflow:visible}
  .slide{position:relative;display:flex !important;page-break-after:always;height:100vh}
  .navbtn,.toc,.editHint,.progress{display:none !important}
}
"""

# ============================================================
#  JS（4 套动画 + 翻页 + 导航）
# ============================================================
JS = r"""
/* ============ 翻页 ============ */
var CUR = 0, TOT = document.querySelectorAll('.slide').length;
function show(i){
  if(i<0||i>=TOT) return;
  document.querySelectorAll('.slide')[CUR].classList.remove('on');
  CUR = i;
  var s = document.querySelectorAll('.slide')[CUR];
  s.classList.add('on');
  var pg = s.querySelector('.pgnum');
  if(pg) pg.textContent = (CUR+1) + ' / ' + TOT;
  document.getElementById('prog').style.width = ((CUR+1)/TOT*100) + '%';
  // ★ 修复：display:none 时 Canvas 尺寸为 0，翻页后必须重设尺寸并重绘
  s.querySelectorAll('canvas').forEach(function(cv){
    if(cv.__molResize){ cv.__molResize(); }
  });
  // 重放入场动画
  s.querySelectorAll('.bar').forEach(function(b,i2){
    b.style.animation='none'; void b.offsetWidth;
    b.style.animation='growBar .9s cubic-bezier(.2,.8,.3,1) both';
    b.style.animationDelay = (0.06*i2)+'s';
  });
  s.querySelectorAll('.growPath').forEach(function(p){
    var L=p.getTotalLength();
    p.style.strokeDasharray=L; p.style.strokeDashoffset=L;
    p.style.transition='none'; void p.getBBox();
    setTimeout(function(){ p.style.transition='stroke-dashoffset 2.4s ease-in-out'; p.style.strokeDashoffset=0; },60);
  });
}
/* ============ 全局：判断是否正在输入（防翻页误触） ============ */
function isTyping(e){
  var t = e.target;
  if(!t) return false;
  // ① contenteditable 元素  ② 表单控件  ③ 中文 IME 组合中
  if(t.isContentEditable) return true;
  if(/^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)) return true;
  if(e.isComposing || e.keyCode === 229) return true;   // IME 正在组词（关键！）
  return false;
}

document.addEventListener('keydown', function(e){
  if(isTyping(e)) return;          // ★ 打字/选字/组词时，一律不触发翻页与快捷键
  if(e.key==='ArrowRight'||e.key==='PageDown'||e.key===' ')  { e.preventDefault(); show(CUR+1); }
  if(e.key==='ArrowLeft' ||e.key==='PageUp')                  { e.preventDefault(); show(CUR-1); }
  if(e.key==='Home'){ show(0); }
  if(e.key==='End'){ show(TOT-1); }
  if(e.key==='n'||e.key==='N'){ document.getElementById('toc').classList.toggle('on'); }
  if(e.key==='Escape'){ document.getElementById('toc').classList.remove('on'); }
});
document.getElementById('prevBtn').onclick=function(){ show(CUR-1); };
document.getElementById('nextBtn').onclick=function(){ show(CUR+1); };
document.getElementById('toc').onclick=function(e){ if(e.target===this) this.classList.remove('on'); };

/* 触屏滑动 */
(function(){
  var x0=null;
  document.addEventListener('touchstart',function(e){ x0=e.touches[0].clientX; },{passive:true});
  document.addEventListener('touchend',function(e){
    if(x0===null) return;
    var dx=e.changedTouches[0].clientX-x0;
    if(Math.abs(dx)>52){ show(dx<0?CUR+1:CUR-1); }
    x0=null;
  },{passive:true});
})();

/* ============ 方案 A/B/D：Canvas 分子粒子网络 ============ */
function molNet(cv, opt){
  opt = opt || {};
  var N = opt.n || 58, DLINK = opt.d || 132, ALPHA = opt.a || 0.30, SP = opt.sp || 0.42;
  var ctx = cv.getContext('2d'), W, H, pts = [], mouse={x:-999,y:-999}, RA = false;
  function resize(){
    var r = cv.parentElement.getBoundingClientRect(), dpr = window.devicePixelRatio||1;
    W = cv.width = Math.max(1, r.width*dpr); H = cv.height = Math.max(1, r.height*dpr);
    cv.style.width=r.width+'px'; cv.style.height=r.height+'px';
  }
  function init(){
    pts=[]; var dpr=window.devicePixelRatio||1;
    for(var i=0;i<N;i++) pts.push({
      x:Math.random()*W, y:Math.random()*H,
      vx:(Math.random()-.5)*SP*dpr, vy:(Math.random()-.5)*SP*dpr,
      r:(Math.random()*2.1+1.3)*dpr
    });
  }
  function draw(){
    if(!cv.offsetParent){ RA=false; return; }
    ctx.clearRect(0,0,W,H);
    var dpr=window.devicePixelRatio||1, D2=DLINK*dpr;
    for(var i=0;i<N;i++){
      var p=pts[i]; p.x+=p.vx; p.y+=p.vy;
      if(p.x<0||p.x>W) p.vx*=-1;
      if(p.y<0||p.y>H) p.vy*=-1;
      var dx=p.x-mouse.x*dpr, dy=p.y-mouse.y*dpr, md=Math.sqrt(dx*dx+dy*dy);
      if(md<130*dpr && md>0.1){ p.x-=dx/md*1.1; p.y-=dy/md*1.1; }
      for(var j=i+1;j<N;j++){
        var q=pts[j], ax=p.x-q.x, ay=p.y-q.y, d=Math.sqrt(ax*ax+ay*ay);
        if(d<D2){
          ctx.beginPath(); ctx.strokeStyle='rgba(0,47,167,'+(ALPHA*(1-d/D2))+')';
          ctx.lineWidth=1*dpr; ctx.moveTo(p.x,p.y); ctx.lineTo(q.x,q.y); ctx.stroke();
        }
      }
    }
    for(var k=0;k<N;k++){
      var pt=pts[k];
      ctx.beginPath();
      ctx.fillStyle = (k%4===0)?'rgba(0,212,255,.86)':'rgba(0,47,167,.62)';
      ctx.arc(pt.x,pt.y,pt.r,0,6.284); ctx.fill();
    }
    RA = requestAnimationFrame(draw);
  }
  cv.parentElement.addEventListener('mousemove',function(e){
    var r=cv.getBoundingClientRect(); mouse.x=e.clientX-r.left; mouse.y=e.clientY-r.top;
  });
  cv.parentElement.addEventListener('mouseleave',function(){ mouse.x=-999; mouse.y=-999; });
  resize(); init();
  // ★ 暴露 resize，供 show() 在翻页（display 由 none→flex）后重设尺寸
  cv.__molResize = function(){ resize(); init(); if(!RA){ RA=true; draw(); } };
  new IntersectionObserver(function(es){
    es.forEach(function(en){
      if(en.isIntersecting && !RA){ RA=true; draw(); }
      else if(!en.isIntersecting && RA){ cancelAnimationFrame(RA); RA=false; }
    });
  }).observe(cv);
  window.addEventListener('resize',function(){ resize(); init(); });
}

/* ============ 方案 C：DNA 双螺旋入场生长 ============ */
function helix(root){
  var svg = root.querySelector('svg');
  if(!svg || svg.dataset.done) return;
  svg.dataset.done='1';
  var VW=1000, VH=340, NS='http://www.w3.org/2000/svg';
  var mid=VH/2, amp=(root.dataset.amp?+root.dataset.amp:96), turns=(root.dataset.turns?+root.dataset.turns:3.1);
  var p1=[],p2=[],rungs=[];
  for(var x=0;x<=VW;x+=4){
    var t=x/VW*Math.PI*2*turns;
    var y1=mid-Math.sin(t)*amp, y2=mid+Math.sin(t)*amp;
    p1.push([x,y1]); p2.push([x,y2]);
    if(x%42===0) rungs.push([x,y1,y2]);
  }
  function toPath(p){
    var d='M'+p[0][0].toFixed(1)+','+p[0][1].toFixed(1);
    for(var i=1;i<p.length;i++) d+=' L'+p[i][0].toFixed(1)+','+p[i][1].toFixed(1);
    return d;
  }
  rungs.forEach(function(r){
    var ln=document.createElementNS(NS,'line');
    ln.setAttribute('x1',r[0]); ln.setAttribute('x2',r[0]);
    ln.setAttribute('y1',r[1].toFixed(1)); ln.setAttribute('y2',r[2].toFixed(1));
    ln.setAttribute('stroke','#1E5BFF'); ln.setAttribute('stroke-width','2');
    ln.setAttribute('opacity','.34');
    svg.appendChild(ln);
  });
  [[p1,'#002FA7'],[p2,'#00D4FF']].forEach(function(pair,idx){
    var pa=document.createElementNS(NS,'path');
    pa.setAttribute('d',toPath(pair[0]));
    pa.setAttribute('fill','none'); pa.setAttribute('stroke',pair[1]);
    pa.setAttribute('stroke-width','3.4'); pa.setAttribute('stroke-linecap','round');
    pa.setAttribute('class','growPath');
    pa.setAttribute('opacity', idx?'0.85':'1');
    svg.appendChild(pa);
    var L=pa.getTotalLength();
    pa.style.strokeDasharray=L; pa.style.strokeDashoffset=L;
    setTimeout(function(){
      pa.style.transition='stroke-dashoffset 2.6s ease-in-out';
      pa.style.strokeDashoffset=0;
    },120+idx*260);
  });
}

/* ============ 初始化 ============ */
document.addEventListener('DOMContentLoaded', function(){
  // 分子网络：任何带 data-mol 的 host
  document.querySelectorAll('[data-mol]').forEach(function(h){
    var cv=document.createElement('canvas');
    h.appendChild(cv);
    molNet(cv, { n:+h.dataset.n||58, d:+h.dataset.d||132, a:+h.dataset.a||0.30, sp:+h.dataset.sp||0.42 });
  });
  // 双螺旋
  document.querySelectorAll('[data-helix]').forEach(helix);
  show(0);
});
"""

# ============================================================
#  审稿模式（Review Mode）—— 逐页确认工作台
# ============================================================
REVIEW_CSS = r"""
/* ---------- 审稿条（每页底部） ---------- */
.rvbar{
  position:absolute;left:0;right:0;bottom:4.4vh;z-index:55;
  display:flex;align-items:center;gap:.62vw;
  padding:.85vh 2.4vw;
  background:linear-gradient(90deg,rgba(0,47,167,.045),rgba(0,212,255,.03),rgba(0,47,167,.045));
  border-top:1px solid var(--line);border-bottom:1px solid var(--line);
  font-size:var(--t-label);
  transform:translateY(110%);transition:transform .28s cubic-bezier(.2,.8,.3,1);
}
body.rv .rvbar{transform:none}
.rvbar .rvt{font-weight:800;color:var(--blue);letter-spacing:.6px;white-space:nowrap}
.rvbar .rvsel{display:flex;gap:.34vw}
.rvbtn{
  padding:.5vh .72vw;border-radius:7px;border:1px solid var(--line);background:#fff;
  color:var(--ink2);font-size:var(--t-label);font-weight:700;cursor:pointer;
  font-family:var(--fnt);white-space:nowrap;transition:.16s;
}
.rvbtn:hover{border-color:rgba(0,47,167,.44);color:var(--blue)}
.rvbtn.on{background:var(--blue);border-color:var(--blue);color:#fff}
.rvbtn.on[data-s="fix"]{background:#b45309;border-color:#b45309}
.rvbtn.on[data-s="ok"]{background:#0a7f3f;border-color:#0a7f3f}
.rvbar .rvnote{
  flex:1;min-width:0;padding:.5vh .7vw;border:1px solid var(--line);border-radius:7px;
  font-family:var(--fnt);font-size:var(--t-label);color:var(--ink);background:#fff;
}
.rvbar .rvnote:focus{outline:2px solid rgba(0,212,255,.6);outline-offset:1px}
.rvbar .rvsave{font-size:var(--t-label);color:var(--grey);white-space:nowrap;min-width:5.4vw;text-align:right}

/* ---------- 状态徽章（每页右上） ---------- */
.rvBadge{
  position:absolute;top:6.6vh;right:2.4vw;z-index:56;
  display:none;align-items:center;gap:.4vw;
  padding:.55vh .78vw;border-radius:20px;font-size:var(--t-label);font-weight:800;letter-spacing:.5px;
}
body.rv .rvBadge{display:flex}
.rvBadge[data-s=""]{background:rgba(0,0,0,.055);color:var(--grey)}
.rvBadge[data-s="ok"]{background:rgba(10,127,63,.11);color:#0a7f3f}
.rvBadge[data-s="fix"]{background:rgba(180,83,9,.11);color:#b45309}

/* ---------- 审稿面板（按 R 呼出） ---------- */
.rvPanel{
  position:fixed;top:0;right:0;bottom:0;width:26vw;min-width:330px;z-index:210;
  background:#fff;border-left:1px solid var(--line);
  box-shadow:-10px 0 40px rgba(0,47,167,.13);
  display:flex;flex-direction:column;transform:translateX(102%);
  transition:transform .3s cubic-bezier(.2,.8,.3,1);
}
.rvPanel.on{transform:none}
.rvHead{padding:2.1vh 1.2vw 1.6vh;border-bottom:1px solid var(--line);background:linear-gradient(180deg,#fbfcff,#fff)}
.rvHead h4{font-size:1.72vh;font-weight:800;color:var(--ink);margin-bottom:.5vh}
.rvHead .rvd{font-size:1.16vh;color:var(--grey);line-height:1.6}
.rvStat{display:flex;gap:.6vw;margin-top:1.2vh}
.rvStat div{
  flex:1;text-align:center;padding:.7vh .3vw;border:1px solid var(--line);border-radius:8px;
}
.rvStat b{display:block;font-size:1.86vh;font-weight:800;color:var(--blue);font-variant-numeric:tabular-nums}
.rvStat span{font-size:1.0vh;color:var(--grey)}
.rvStat .s-ok b{color:#0a7f3f}
.rvStat .s-fix b{color:#b45309}
.rvList{flex:1;overflow-y:auto;padding:.9vh .8vw}
.rvItem{
  display:flex;align-items:center;gap:.5vw;padding:.85vh .68vw;border-radius:8px;
  border:1px solid transparent;cursor:pointer;transition:.14s;margin-bottom:.42vh;
}
.rvItem:hover{background:rgba(0,47,167,.045)}
.rvItem.cur{background:rgba(0,212,255,.1);border-color:rgba(0,212,255,.4)}
.rvItem .n{
  flex:0 0 auto;width:2.1em;text-align:right;font-size:1.08vh;font-weight:800;
  color:var(--blue);font-variant-numeric:tabular-nums;
}
.rvItem .tt{flex:1;min-width:0;font-size:1.14vh;color:var(--ink2);line-height:1.42;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.rvItem .dot{flex:0 0 auto;width:9px;height:9px;border-radius:50%;background:rgba(0,0,0,.13)}
.rvItem[data-s="ok"] .dot{background:#0a7f3f}
.rvItem[data-s="fix"] .dot{background:#b45309}
.rvItem .hasNote{flex:0 0 auto;font-size:.94vh;color:#b45309;font-weight:800}
.rvFoot{padding:1.3vh .9vw;border-top:1px solid var(--line);display:flex;gap:.5vw;flex-wrap:wrap}
.rvFoot button{
  flex:1;min-width:calc(50% - .3vw);padding:.85vh .5vw;border-radius:8px;cursor:pointer;
  font-family:var(--fnt);font-size:1.08vh;font-weight:700;border:1px solid var(--line);
  background:#fff;color:var(--ink2);transition:.16s;
}
.rvFoot button:hover{border-color:var(--blue);color:var(--blue)}
.rvFoot button.pri{background:var(--blue);border-color:var(--blue);color:#fff}
.rvFoot button.pri:hover{background:#00237d}
.rvFoot button.warn{color:#b45309;border-color:rgba(180,83,9,.3)}

/* ---------- 审稿总开关 ---------- */
.rvToggle{
  position:fixed;top:.9vh;right:1.1vw;z-index:150;
  padding:.72vh .88vw;border-radius:8px;cursor:pointer;
  font-family:var(--fnt);font-size:1.16vh;font-weight:800;letter-spacing:.4px;
  border:1px solid var(--line);background:rgba(255,255,255,.95);color:var(--blue);
  box-shadow:0 2px 12px rgba(0,47,167,.1);transition:.18s;
}
.rvToggle:hover{background:var(--blue);color:#fff;border-color:var(--blue)}
body.rv .rvToggle{background:var(--blue);color:#fff;border-color:var(--blue)}

/* 审稿模式下给底部审稿条让位 */
body.rv .slide{padding-bottom:9.4vh}
body.rv .footbar{bottom:0}

/* 打印时隐藏所有审稿 UI */
@media print{
  .rvbar,.rvPanel,.rvToggle,.rvBadge{display:none !important}
  body.rv .slide{padding-bottom:5vh}
}
"""

REVIEW_JS = r"""
/* ============================================================
   审稿模式：逐页确认 · 自动保存 · 导出确稿
   ============================================================ */
var RVKEY = 'deckReview::' + document.title;
var RV = {pages:{}, updated:''};
try{ var _r = JSON.parse(localStorage.getItem(RVKEY)||'null'); if(_r && _r.pages) RV = _r; }catch(e){}

var STAT_LABEL = {'':'待确认', 'ok':'已确认', 'fix':'需修改'};

function rvSave(){
  RV.updated = new Date().toISOString();
  try{ localStorage.setItem(RVKEY, JSON.stringify(RV)); rvTick('已保存 '+new Date().toLocaleTimeString('zh-CN',{hour:'2-digit',minute:'2-digit',second:'2-digit'})); }
  catch(e){ rvTick('⚠ 保存失败（存储已满？）'); }
}
function rvTick(msg){
  var el = document.querySelector('.slide.on .rvsave'); if(el) el.textContent = msg;
}
function rvGet(i){
  if(!RV.pages[i]) RV.pages[i] = {status:'', note:'', edits:{}};
  return RV.pages[i];
}
function rvRefresh(){
  var s = document.querySelectorAll('.slide')[CUR]; if(!s) return;
  var rec = rvGet(CUR+1);
  // 徽章
  var bd = s.querySelector('.rvBadge');
  if(bd){ bd.dataset.s = rec.status; bd.querySelector('.txt').textContent = STAT_LABEL[rec.status] || '待确认'; }
  // 按钮态
  s.querySelectorAll('.rvbar .rvbtn').forEach(function(b){ b.classList.toggle('on', b.dataset.s === rec.status); });
  // 备注
  var nt = s.querySelector('.rvbar .rvnote');
  if(nt && document.activeElement !== nt) nt.value = rec.note || '';
  // 侧栏高亮
  document.querySelectorAll('.rvItem').forEach(function(it){
    it.classList.toggle('cur', +it.dataset.i === CUR+1);
  });
  rvStats();
  rvCountEdits();
}
function rvSetStatus(st){
  var rec = rvGet(CUR+1);
  rec.status = (rec.status === st) ? '' : st;   // 再点一次取消
  rvSave(); rvRefresh();
}
function rvSetNote(v){
  var rec = rvGet(CUR+1); rec.note = v; rvSave();
}
function rvStats(){
  var tot = document.querySelectorAll('.slide').length, ok=0, fix=0, pend=0, notes=0;
  for(var i=1;i<=tot;i++){
    var r = RV.pages[i] || {};
    if(r.status==='ok') ok++; else if(r.status==='fix') fix++; else pend++;
    if(r.note) notes++;
  }
  var q=function(sel,v){ var e=document.querySelector(sel); if(e) e.textContent = v; };
  q('#rvOk', ok); q('#rvFix', fix); q('#rvPend', pend); q('#rvNotes', notes);
  document.querySelectorAll('.rvItem').forEach(function(it){
    var r = RV.pages[+it.dataset.i] || {};
    it.dataset.s = r.status || '';
    var hn = it.querySelector('.hasNote');
    if(hn) hn.textContent = r.note ? '✎' : '';
  });
}
function rvBuildPanel(){
  var list = document.getElementById('rvList'); if(!list) return;
  var html='';
  document.querySelectorAll('.slide').forEach(function(s,i){
    var t = s.querySelector('h1, h2, h3');
    var txt = (t?t.textContent.trim():'').replace(/\s+/g,' ').slice(0,38) || ('第 '+(i+1)+' 页');
    html += '<div class="rvItem" data-i="'+(i+1)+'" onclick="show('+i+');rvRefresh();">'
          + '<span class="n">'+(i+1)+'</span>'
          + '<span class="tt">'+txt.replace(/</g,'&lt;').slice(0,40)+'</span>'
          + '<span class="hasNote"></span><span class="dot"></span></div>';
  });
  list.innerHTML = html;
}
function rvToggleForce(mode){
  document.body.classList.toggle('rv', mode===undefined ? !document.body.classList.contains('rv') : mode);
}
function rvExportDeck(){
  // 克隆当前文档，剥离审稿 UI，导出确稿 HTML
  var doc = document.cloneNode(true);
  doc.querySelectorAll('.rvPanel,.rvToggle,.rvbar,.rvBadge,.elPanel,.elToggle,.elbar,.elHint,.editHint,#toc').forEach(function(e){ e.remove(); });
  doc.querySelectorAll('body *').forEach(function(e){ e.removeAttribute('contenteditable'); });
  var body = doc.querySelector('body'); if(body) body.className = '';
  // 重置到第 1 页
  var html = '<!DOCTYPE html>\n' + doc.documentElement.outerHTML;
  rvDownload(html, document.title.replace(/[\/\\:*?"<>|🐴 ]/g,'') + '_确稿版.html', 'text/html');
}
function rvExportReport(){
  var tot = document.querySelectorAll('.slide').length;
  var lines = ['# 审稿报告 · ' + document.title, '', '导出时间：' + new Date().toLocaleString('zh-CN'), '', '| 页 | 标题 | 状态 | 备注 |', '|---|---|---|---|'];
  for(var i=1;i<=tot;i++){
    var s = document.querySelectorAll('.slide')[i-1];
    var t = s.querySelector('h1, h2, h3');
    var txt = ((t?t.textContent.trim():'')||'').replace(/\s+/g,' ').replace(/\|/g,'／').slice(0,34);
    var r = RV.pages[i] || {};
    lines.push('| ' + i + ' | ' + txt + ' | ' + (STAT_LABEL[r.status]||'待确认') + ' | ' + (r.note||'').replace(/\|/g,'／').replace(/\n/g,' ') + ' |');
  }
  var tot2 = tot, ok=0, fix=0, pend=0;
  for(var j=1;j<=tot2;j++){ var rr=RV.pages[j]||{}; if(rr.status==='ok')ok++; else if(rr.status==='fix')fix++; else pend++; }
  lines.push('', '## 汇总', '', '- 总页数：' + tot2, '- 已确认：' + ok + ' 页', '- 需修改：' + fix + ' 页', '- 待确认：' + pend + ' 页');
  rvDownload(lines.join('\n'), document.title.replace(/[\/\\:*?"<>|🐴 ]/g,'') + '_审稿报告.md', 'text/markdown');
}
function rvExportJSON(){
  rvDownload(JSON.stringify(RV, null, 2), document.title.replace(/[\/\\:*?"<>|🐴 ]/g,'') + '_审稿数据.json', 'application/json');
}
function rvImportJSON(){
  var inp = document.createElement('input'); inp.type='file'; inp.accept='.json';
  inp.onchange = function(){
    var f = inp.files[0]; if(!f) return;
    var rd = new FileReader();
    rd.onload = function(){
      try{ var d = JSON.parse(rd.result); if(!d.pages) throw 0;
        RV = d; rvSave(); rvBuildPanel(); rvRefresh();
        alert('已导入审稿数据：' + Object.keys(d.pages).length + ' 页记录');
      }catch(e){ alert('导入失败：文件格式不正确'); }
    };
    rd.readAsText(f);
  };
  inp.click();
}
function rvReset(){
  if(!confirm('确定清空全部审稿状态与备注？（编辑过的文字内容不受影响）')) return;
  RV = {pages:{}, updated:''}; rvSave(); rvBuildPanel(); rvRefresh();
}
function rvDownload(content, name, mime){
  var b = new Blob([content], {type: mime + ';charset=utf-8'});
  var a = document.createElement('a');
  a.href = URL.createObjectURL(b); a.download = name;
  document.body.appendChild(a); a.click();
  setTimeout(function(){ URL.revokeObjectURL(a.href); a.remove(); }, 400);
  rvTick('已导出 ' + name.slice(-22));
}

/* ---------- 编辑增强：改动高亮 / 单页还原 / 撤销 ---------- */
var EDIT_UNDO = {};     // { 页号: [旧文案, ...] }

function markEdited(el){
  el.dataset.edited = '1';
  el.style.background = 'rgba(0,212,255,.085)';
  el.style.boxShadow = 'inset 0 -2px 0 rgba(0,212,255,.55)';
  var i = +el.closest('.slide').dataset.i + 1;
  if(!EDIT_UNDO[i]) EDIT_UNDO[i] = [];
}
function clearEdited(el){
  el.removeAttribute('data-edited');
  el.style.background = ''; el.style.boxShadow = '';
}
function rvUndoPage(){
  var s = document.querySelectorAll('.slide')[CUR]; if(!s) return;
  var i = +s.dataset.i + 1, st = EDIT_UNDO[i];
  if(!st || !st.length){ rvTick('本页无编辑记录'); return; }
  var last = st.pop();
  var el = last.el;
  if(last.html) el.innerHTML = last.html;
  delete el.dataset._orig;
  clearEdited(el);
  rvSave(); rvRefresh(); rvTick('已撤销本页上一处编辑');
}
function rvResetPageEdits(){
  var s = document.querySelectorAll('.slide')[CUR]; if(!s) return;
  if(!confirm('还原本页全部文字到初始内容？')) return;
  location.reload();
}
function rvCountEdits(){
  var n = document.querySelectorAll('[data-edited]').length;
  var e = document.getElementById('rvEdits'); if(e) e.textContent = n;
  return n;
}

/* 聚焦即存原始快照（撤销基准，必须在编辑前拿到） */
document.addEventListener('focusin', function(e){
  if(e.target.isContentEditable && e.target.dataset._orig === undefined){
    e.target.dataset._orig = e.target.innerHTML;
  }
}, true);

/* 文字编辑自动保存 */
document.addEventListener('input', function(e){
  if(e.target.isContentEditable){
    var s = e.target.closest('.slide'); if(!s) return;
    var i = +s.dataset.i + 1;
    var rec = rvGet(i);
    rec.edits = rec.edits || {};
    rec.edits['t'+i+'_'+(rec._n = (rec._n||0)+1)] = e.target.textContent;
    // 撤销快照：同一元素连续输入只记首次（避免每键一条）
    var st = EDIT_UNDO[i] = EDIT_UNDO[i] || [];
    if(!st.length || st[st.length-1].el !== e.target){
      if(st.length < 60) st.push({el: e.target, html: e.target.dataset._orig || ''});
    }
    markEdited(e.target);
    rvCountEdits();
    rvSave();
  }
});
document.addEventListener('blur', function(e){
  if(e.target.isContentEditable) rvSave();
}, true);

/* 键盘：R 开审稿 / V 显隐审稿条 */
document.addEventListener('keydown', function(e){
  if(typeof isTyping==='function' ? isTyping(e) : e.target.isContentEditable) return;
  if(e.key==='r'||e.key==='R'){ rvToggleForce(); document.getElementById('rvPanel').classList.toggle('on'); if(document.body.classList.contains('rv')) rvRefresh(); }
  if(e.key==='Escape'){ document.getElementById('rvPanel').classList.remove('on'); }
});
"""

# ============================================================
#  页面构造工具
# ============================================================
LOGO_B64 = ''
try:
    import base64
    lp = os.path.expanduser('~/work/公司资料/logo_transparent.png')
    if os.path.exists(lp):
        LOGO_B64 = base64.b64encode(open(lp,'rb').read()).decode()
except Exception:
    pass

def topbar(mid=''):
    return f"""<div class="topbar">
  <div class="tb-left"><div class="tb-logo" role="img" aria-label="宝锐生物 BIORI BIOTECH"></div><span class="tb-slogan" contenteditable="true">守护生命健康<b>·</b>缔造美好生活</span></div>
  <div class="tb-mid">{mid}</div>
  <div class="tb-right">诊断原料 · 客户宣讲</div>
</div>"""

def footbar(n):
    return f"""<div class="footbar"><span class="fm">珠海宝锐生物科技有限公司 · <em>把体系交给我们，把时间留给您的团队</em></span><span class="fsl" contenteditable="true">守护生命健康 · 缔造美好生活</span><span class="pgnum">{n}</span></div>"""

def ul(items):
    return '<ul class="ul">' + ''.join(f'<li contenteditable="true">{esc(x)}</li>' for x in items) + '</ul>'

def kv(items, with_src=True):
    rows = []
    for it in items:
        s = f'<span class="s">{esc(it["src"])}</span>' if with_src and it.get('src') else ''
        rows.append(f'<div class="kvrow"><span class="k" contenteditable="true">{esc(it["k"])}</span><span class="v" contenteditable="true">{esc(it["v"])}</span>{s}</div>')
    return '<div class="kv">' + ''.join(rows) + '</div>'

def bartable(chart, h='30vh', hl_last=False):
    """纯 CSS 柱状图（div 高度百分比）"""
    series = chart['series']; labels = chart['labels']
    mx = max(v for s in series for v in s['vals']) or 1
    groups = []
    for gi, lab in enumerate(labels):
        bars = []
        for si, s in enumerate(series):
            v = s['vals'][gi]
            ph = v / mx * 88
            cls = 'bar c' if si == 0 else ('bar' if si < len(series)-1 else 'bar g')
            bars.append(f'<div class="{cls}" style="height:{ph:.2f}%" title="{esc(s["name"])}: {v}"></div>')
        groups.append(f'<div class="bargrp">{"".join(bars)}<span class="blabel" contenteditable="true">{esc(lab)}</span></div>')
    lg = ''.join(f'<span><i style="background:{["#002FA7","#00D4FF","#9aa4b2","#1E5BFF","#6e6e73"][i%5]}"></i>{esc(s["name"])}</span>' for i, s in enumerate(series))
    return f'<div class="chart" style="height:{h}">{"".join(groups)}</div><div class="legend">{lg}</div>'

def dtable(rows, headers):
    th = ''.join(f'<th>{esc(h)}</th>' for h in headers)
    trs = []
    for r in rows:
        tds = ''.join(f'<td class="k" contenteditable="true">{esc(c)}</td>' if i == 0 else f'<td contenteditable="true">{esc(c)}</td>' for i, c in enumerate(r))
        trs.append(f'<tr>{tds}</tr>')
    return f'<table class="dt"><thead><tr>{th}</tr></thead><tbody>{"".join(trs)}</tbody></table>'

def card(title, body, cls=''):
    return f'<div class="card {cls}"><div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.2vh" contenteditable="true">{esc(title)}</div>{body}</div>'

def stage_page(num, h2, sub, mol_opt='data-mol data-n="52" data-d="150" data-a="0.16"'):
    return f"""<div class="canvasHost" {mol_opt}></div>
<div class="st-num">{num}</div>
<div class="stageTxt">
  <h2 contenteditable="true">{esc(h2)}</h2>
  <div class="st-sub" contenteditable="true">{esc(sub)}</div>
  <div class="u"></div>
</div>"""

def scene_head(no, name, kicker=''):
    return f"""<div class="scHead"><span class="big">{no}</span><span class="nm" contenteditable="true">{esc(name)}</span></div>
{f'<div class="kicker" contenteditable="true">{esc(kicker)}</div>' if kicker else ''}"""

# ============================================================
#  P1  封面（方案 D）
# ============================================================
add('封面', f"""<div class="canvasHost" data-mol data-n="64" data-d="145" data-a="0.13"></div>
<div class="cover">
  <div class="logoRow">
    <div class="tb-logo" role="img" aria-label="宝锐生物 BIORI BIOTECH"></div>
    <span class="cslogan" contenteditable="true">守护生命健康<b>·</b>缔造美好生活</span>
  </div>
  <h1 contenteditable="true">诊断原料整体解决方案<br><em>从原料到体系的深度协同</em></h1>
  <div class="csub" contenteditable="true">分子诊断 · 七大热点应用场景 · 数据说话</div>
  <div class="cline"></div>
  <div class="cmeta" contenteditable="true">珠海宝锐生物科技有限公司 ｜ 2026</div>
  <div class="chips">
    <span contenteditable="true">2012 年成立</span>
    <span contenteditable="true">国家高新技术企业</span>
    <span contenteditable="true">专精特新企业</span>
    <span contenteditable="true">省分子诊断原料酶工程技术研究中心</span>
  </div>
</div>""", cls='stage')

# ============================================================
#  P2  今天只讲三件事
# ============================================================
add('议题', f"""{topbar('本次交流')}
<div class="grow" style="justify-content:center">
  <div class="kicker" contenteditable="true">AGENDA · 三件事</div>
  <h2 class="sec" contenteditable="true">今天只讲三件事</h2>
  <div class="rule"></div>
  <div class="grid3" style="margin-top:2.6vh">
    <div class="card blue">
      <div style="font-size:min(3.4vw,5vh);font-weight:800;color:var(--blue);line-height:1">01</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.1vh 0 .8vh" contenteditable="true">行业在往哪走</div>
      <div class="lead" contenteditable="true">快速化、冻干化、NA 控制、多重直扩、出海合规 —— 五个趋势正在重排原料选型标准。</div>
    </div>
    <div class="card blue">
      <div style="font-size:min(3.4vw,5vh);font-weight:800;color:var(--blue);line-height:1">02</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.1vh 0 .8vh" contenteditable="true">为什么要深度协同</div>
      <div class="lead" contenteditable="true">原料不是一次性买卖。联合开发、工艺共建、放大量产，才能把交期和成本真正压下来。</div>
    </div>
    <div class="card blue">
      <div style="font-size:min(3.4vw,5vh);font-weight:800;color:var(--blue);line-height:1">03</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.1vh 0 .8vh" contenteditable="true">七个场景怎么落地</div>
      <div class="lead" contenteditable="true">每个场景先讲你的需求与卡点，再给宝锐的实测数据 —— 能不能用，数据说话。</div>
    </div>
  </div>
</div>
{footbar(2)}""")

# ============================================================
#  P3  目录（方案 A 流程）
# ============================================================
toc_items = [('一','行业痛点与趋势','P4–P10 · 5 个趋势 + 三大痛点'),
             ('二','产业链协同 · 深度合作','P11–P16 · 从买原料到共建体系'),
             ('三','宝锐能力与成功案例','P17–P25 · 十余年积累'),
             ('四','七大热点应用场景','P26–P39 · 需求分析 + 宝锐数据')]
add('目录', f"""{topbar('目录')}
<div class="grow" style="justify-content:center">
  <div class="kicker" contenteditable="true">CONTENTS</div>
  <h2 class="sec" contenteditable="true">目录</h2>
  <div class="rule cy"></div>
  <div style="display:flex;flex-direction:column;gap:1.35vh;margin-top:1.4vh">
    {''.join(f'''<div style="display:flex;align-items:center;gap:1.5vw;padding:1.55vh 1.3vw;border:1px solid var(--line);border-radius:11px;background:linear-gradient(90deg,rgba(0,47,167,.028),transparent)">
      <span style="font-size:min(2.2vw,3.3vh);font-weight:800;color:var(--blue);min-width:3.4vw" contenteditable="true">{n}</span>
      <div style="flex:1"><div style="font-size:var(--t-h3);font-weight:800" contenteditable="true">{t}</div>
      <div class="src-tag" contenteditable="true">{d}</div></div>
      <span style="width:2.6vw;height:2px;background:linear-gradient(90deg,var(--blue),var(--cyan));border-radius:2px"></span>
    </div>''' for n,t,d in toc_items)}
  </div>
</div>
{footbar(3)}""")

# ============================================================
#  第一部分 · 行业痛点与趋势（P4–P10）
# ============================================================
add('一 · 承接页', stage_page('01', '行业正在发生什么', '五个趋势 + 三个最痛的卡点'),
    cls='stage', nav='一 · 行业痛点与趋势 · 承接页')

ind = DATA['industry']
add('趋势大盘', f"""{topbar('行业趋势')}
<div class="grow">
  <div class="kicker" contenteditable="true">INDUSTRY · 趋势大盘</div>
  <h3 class="h3" contenteditable="true">五个趋势，正在重排原料选型标准</h3>
  <div class="rule"></div>
  <div class="grid2" style="margin-top:.8vh">
    <div>{dtable([[t['no']+' '+t['t'], t['d']] for t in ind['trends']], ['趋势','对原料的要求'])}</div>
    <div class="card blue">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.3vh" contenteditable="true">三个最痛的卡点</div>
      {ul([p['t']+'：'+p['d'] for p in ind['painpoints'][:3]])}
      <div style="margin-top:1.6vh;padding-top:1.4vh;border-top:1px dashed var(--line)">
        <span class="pill cy" contenteditable="true">这三个卡点，本质都是原料问题</span>
      </div>
    </div>
  </div>
</div>
{footbar(5)}""", nav='一 · 5 个趋势 + 三大痛点')

# --- P6 三大核心痛点（汇聚式） ---
add('三大痛点', f"""{topbar('行业痛点')}
<div class="grow">
  <div class="kicker" contenteditable="true">PAIN POINTS</div>
  <h3 class="h3" contenteditable="true">你的产线，正在被这三件事拖住</h3>
  <div class="rule"></div>
  <div class="grid3" style="margin-top:1vh">
    {''.join(f'''<div class="card">
      <div style="width:2.9vh;height:2.9vh;min-width:26px;min-height:26px;border-radius:8px;background:linear-gradient(135deg,var(--blue),var(--blue2));display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:1.42vh;margin-bottom:1.3vh">{i+1}</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:.8vh" contenteditable="true">{p['t']}</div>
      <div class="lead" contenteditable="true">{p['d']}</div>
    </div>''' for i,p in enumerate(ind['painpoints'] if len(ind['painpoints'])==3 else ind['painpoints'][:3]))}
  </div>
</div>
{footbar(6)}""", nav='一 · 三大核心痛点')

# --- P7–P10 趋势详解（4 页） ---
trend_pages = [
 ('趋势① 快速化', '30 分钟内出结果，卡点在酶的催化效率',
  ['急诊、发热门诊、POCT 场景要求 30 min 内完成扩增检测',
   '提速会牺牲灵敏度与特异性 → 需要酶本征催化效率足够高',
   '快速程序对酶耐热性与热启动封闭效果要求更严'],
  [('扩增速率','不低于 1 kb/10s，最快 30 min 内完成检测','PPT2 P22'),
   ('RT 快速','qRT-PCR 检测用时低至 20 min','PPT2 P21'),
   ('催化效率','Rob TaqQS 3.0 的 Kcat/Km = 5.191，为竞品1 的 3.61 倍','PPT1 P25')]),
 ('趋势② 冻干化 + 全预混', '免冷链、免分装，让试剂走到基层与海外',
  ['冷链成本高、断链风险大，基层与出海市场难以承受',
   '冻干后需保持活性与复溶性，保护剂配方是关键',
   '量产放大与批间稳定性，决定能否真正替代进口'],
  [('常温稳定性','产品已完成常温放置 18 个月稳定性验证','PPT2 冻干平台'),
   ('全预混稳定性','全预混 37℃ 7 天稳定；抗吸湿冻干保护剂','PPT2 新品页'),
   ('获批成果','冻干型超灵敏/高灵敏直扩试剂、全血直扩分型试剂','PPT2 P11')]),
 ('趋势③ NA 控制 + 复杂样本直扩', '准入门槛 + 免提取，两个方向同时加压',
  ['重组酶宿主核酸残留（E.coli 基因组）直接带来假阳性',
   '全血/血浆/拭子/粪便免提取直扩，对抗抑制能力要求极高',
   'UNG/dUTP 防污染体系配合，全流程才能可控'],
  [('宿主残留','低至约 1×10⁻³ copies/U','PPT2 P31'),
   ('致病菌检测','NTC 47 复管阴性，8 种常见条件致病菌均未检出','PPT2 P31'),
   ('直扩加样','全血 ≤5% ｜ 血浆、血清 ≤30%','PPT2 P22')]),
 ('趋势④ 多重检测 + 出海合规 + 国产替代', '一管多靶标、批间稳定、供应链安全',
  ['呼吸道多联检、耐药突变分型要求一管多靶标',
   '出海注册以批间稳定性为门票，换批即需重新验证',
   '集采与内卷下，原料国产化是成本唯一出路'],
  [('多重体系','MMCA 多重熔解曲线法；峰形好、高特异性','PPT2 P21'),
   ('批间稳定','工艺稳定技术先进，有效控制批间差异','PPT2 P19'),
   ('配套酶','温启动 Bst 酶、温启动逆转录酶（国内首款）','PPT2 P11')]),
]
for i,(ttl, stl, needs, datas) in enumerate(trend_pages):
    pno = 7+i
    add(ttl, f"""{topbar('行业趋势')}
<div class="grow">
  <div class="kicker" contenteditable="true">TREND {i+1} / 5</div>
  <h3 class="h3" contenteditable="true">{ttl}</h3>
  <div class="sub" contenteditable="true">{stl}</div>
  <div class="rule"></div>
  <div class="row" style="margin-top:.6vh">
    <div class="col">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.3vh" contenteditable="true">客户端的真实需求</div>
      {ul(needs)}
    </div>
    <div class="col">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.3vh" contenteditable="true">宝锐对应能力</div>
      {dtable([[k,v,s] for k,v,s in datas], ['能力','指标 / 事实','来源'])}
    </div>
  </div>
</div>
{footbar(pno)}""", nav=f'一 · 趋势{i+1}')

# ============================================================
#  第二部分 · 产业链协同（P11–P16）
# ============================================================
add('二 · 承接页', stage_page('02', '为什么要和我们深度协同', '从「买原料」到「共建体系」'),
    cls='stage', nav='二 · 产业链协同 · 承接页')

add('产业链位置', f"""{topbar('产业链协同')}
<div class="grow">
  <div class="kicker" contenteditable="true">VALUE CHAIN</div>
  <h3 class="h3" contenteditable="true">诊断原料在产业链上的位置</h3>
  <div class="rule cy"></div>
  <div class="tl" data-tl>
    <svg viewBox="0 0 1000 200" preserveAspectRatio="xMidYMid meet">
      <path class="flowline" d="M60,100 L250,100"/>
      <path class="flowline c2" d="M330,100 L520,100"/>
      <path class="flowline" d="M600,100 L790,100"/>
      <path class="flowline c2" d="M870,100 L960,100"/>
      <circle class="tlnode hi" cx="60"  cy="100" r="9"/><text class="tlY" x="60"  y="140" text-anchor="middle">上游</text><text class="tlT" x="60"  y="162" text-anchor="middle">酶 / 抗体 / 探针</text>
      <circle class="tlnode"    cx="290" cy="100" r="9"/><text class="tlY" x="290" y="140" text-anchor="middle">宝锐</text><text class="tlT" x="290" y="162" text-anchor="middle">原料智造 · 工艺开发</text>
      <circle class="tlnode"    cx="560" cy="100" r="9"/><text class="tlY" x="560" y="140" text-anchor="middle">中游</text><text class="tlT" x="560" y="162" text-anchor="middle">试剂盒 / 仪器厂商</text>
      <circle class="tlnode"    cx="830" cy="100" r="9"/><text class="tlY" x="830" y="140" text-anchor="middle">下游</text><text class="tlT" x="830" y="162" text-anchor="middle">医院 / ICL / 疾控 / 海关</text>
    </svg>
  </div>
  <div class="card blue" style="margin-top:1.4vh">
    <div class="lead" contenteditable="true"><b>我们不在你的下游，我们在你的上游。</b>原料的批间差、交期、放大量产能力，会沿着这条链一路放大到终端结果 —— 所以协同必须发生在研发早期，而不是采购环节。</div>
  </div>
</div>
{footbar(12)}""", nav='二 · 产业链位置')

# --- P13 三个真实困境 ---
add('三个困境', f"""{topbar('产业链协同')}
<div class="grow">
  <div class="kicker" contenteditable="true">PAIN · 采购视角</div>
  <h3 class="h3" contenteditable="true">换一个原料，为什么这么难</h3>
  <div class="rule"></div>
  <div class="grid3" style="margin-top:1vh">
    <div class="card"><div class="pill" contenteditable="true">困境 1</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.1vh 0 .7vh" contenteditable="true">交期不可控</div>
      <div class="lead" contenteditable="true">进口原料周期长、断供风险高；国产切换又要重新验证，周期同样长。项目等不起。</div></div>
    <div class="card"><div class="pill" contenteditable="true">困境 2</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.1vh 0 .7vh" contenteditable="true">定制化没人接</div>
      <div class="lead" contenteditable="true">特殊体系、特殊浓度、特殊冻干形态 —— 大厂不接小单，小厂做不了工艺。</div></div>
    <div class="card"><div class="pill" contenteditable="true">困境 3</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.1vh 0 .7vh" contenteditable="true">放大就失控</div>
      <div class="lead" contenteditable="true">实验室级别能用，一到 g 级/批的放大量产，批间差就压不住，注册材料要重做。</div></div>
  </div>
  <div class="card blue" style="margin-top:1.6vh">
    <div class="lead" contenteditable="true"><b>这三个困境的共同解法是同一个：把供应商变成研发伙伴。</b>越早介入，验证成本越低，量产越稳。</div>
  </div>
</div>
{footbar(13)}""", nav='二 · 三个真实困境')

# --- P14 协同模式 ---
add('协同模式', f"""{topbar('产业链协同')}
<div class="grow">
  <div class="kicker" contenteditable="true">CO-DEVELOPMENT</div>
  <h3 class="h3" contenteditable="true">宝锐的三种协同模式</h3>
  <div class="rule cy"></div>
  <div class="grid3" style="margin-top:1vh">
    <div class="card"><div style="font-size:var(--t-h3);font-weight:800;color:var(--blue)" contenteditable="true">① 联合开发</div>
      <div class="lead" style="margin-top:.9vh" contenteditable="true">从体系设计阶段介入，共同定义酶的性能指标（催化效率 / 抗抑制 / 耐热），而非在采购阶段比价。</div></div>
    <div class="card"><div style="font-size:var(--t-h3);font-weight:800;color:var(--blue)" contenteditable="true">② 定制化</div>
      <div class="lead" style="margin-top:.9vh" contenteditable="true">按客户体系定制浓度、缓冲体系、冻干形态与包装规格；支持从 mg 级到 g 级的梯度放大。</div></div>
    <div class="card"><div style="font-size:var(--t-h3);font-weight:800;color:var(--blue)" contenteditable="true">③ 工艺共建</div>
      <div class="lead" style="margin-top:.9vh" contenteditable="true">开放冻干服务平台：原料筛选验证 → 冻干工艺优化 → 冻干量产，三步走通。</div></div>
  </div>
  <div class="card" style="margin-top:1.5vh">
    <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.2vh" contenteditable="true">冻干服务平台 · 三步走通</div>
    {dtable([['第一步','原料筛选验证','确认原料可冻干性与配方兼容性'],
             ['第二步','冻干工艺优化','保护剂配方 · 冻干曲线 · 复溶性'],
             ['第三步','冻干量产','批间稳定性 · 常温储运验证']], ['阶段','内容','交付'])}
  </div>
</div>
{footbar(14)}""", nav='二 · 三种协同模式')

# --- P15 协同价值对照 ---
add('协同价值', f"""{topbar('产业链协同')}
<div class="grow">
  <div class="kicker" contenteditable="true">VALUE</div>
  <h3 class="h3" contenteditable="true">从「买原料」到「共建体系」，差在哪</h3>
  <div class="rule"></div>
  <div style="margin-top:.6vh">
    {dtable([
      ['关系定位','一次性买卖，比价采购','长期协同，共担指标'],
      ['介入时点','采购环节（体系已定型）','研发早期（体系可调）'],
      ['性能指标','按目录规格交付','按你的体系反向定制'],
      ['放大能力','mg 级样品','mg → g 级梯度放大'],
      ['批间差','换批需重新验证','工艺稳定，有效控制批间差异'],
      ['冻干','自行摸索或外委','冻干平台全流程支持'],
      ['风险','断供 / 涨价由你承担','供应安全 + 国产替代'],
    ], ['维度','传统采购','深度协同'])}
  </div>
  <div class="card blue" style="margin-top:1.5vh">
    <div class="lead" contenteditable="true"><b>结论：</b>深度协同省下的不是采购单价，而是<b>重复验证的时间</b>与<b>放大失败的代价</b> —— 这两项在试剂注册周期里往往比原料本身贵得多。</div>
  </div>
</div>
{footbar(15)}""", nav='二 · 协同价值对照')

# --- P16 合作方式 + 4 步走通 ---
add('合作方式', f"""{topbar('产业链协同')}
<div class="grow">
  <div class="kicker" contenteditable="true">HOW TO START</div>
  <h3 class="h3" contenteditable="true">怎么开始 —— 三种方式 + 四步走通</h3>
  <div class="rule cy"></div>
  <div class="row" style="margin-top:.6vh">
    <div class="col">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.2vh" contenteditable="true">三种合作方式</div>
      <div style="display:flex;flex-direction:column;gap:1.2vh">
        <div class="card"><div style="font-weight:800;color:var(--blue);font-size:var(--t-note)" contenteditable="true">目录采购</div><div class="lead" style="margin-top:.5vh" contenteditable="true">标准货号直接下单，适合已验证体系的快速放量。</div></div>
        <div class="card"><div style="font-weight:800;color:var(--blue);font-size:var(--t-note)" contenteditable="true">定制开发</div><div class="lead" style="margin-top:.5vh" contenteditable="true">按体系定制指标与规格，签署联合开发协议。</div></div>
        <div class="card"><div style="font-weight:800;color:var(--blue);font-size:var(--t-note)" contenteditable="true">战略绑定</div><div class="lead" style="margin-top:.5vh" contenteditable="true">年度框架 + 供应保障 + 联合注册支持。</div></div>
      </div>
    </div>
    <div class="col">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.2vh" contenteditable="true">四步走通</div>
      {dtable([['01','样品测试','免费提供试用样品与应用支持'],
               ['02','性能验证','按你的体系做平行比对，数据留档'],
               ['03','小试中试','mg → g 级放大，确认批间稳定性'],
               ['04','量产供货','签订供货协议，纳入年度供应保障']], ['步骤','阶段','内容'])}
    </div>
  </div>
</div>
{footbar(16)}""", nav='二 · 合作方式 + 4 步走通')

# ============================================================
#  第三部分 · 宝锐能力与成功案例（P17–P25）
# ============================================================
add('三 · 承接页', stage_page('03', '十余年，我们积累了什么', '研发平台 · 制造体系 · 冻干能力 · 服务经验'),
    cls='stage', nav='三 · 宝锐能力 · 承接页')

co = DATA['company']
add('公司概况', f"""{topbar('关于宝锐')}
<div class="grow">
  <div class="kicker" contenteditable="true">COMPANY</div>
  <h3 class="h3" contenteditable="true">珠海宝锐生物科技有限公司</h3>
  <div class="sub" contenteditable="true">自 2012 年成立以来，专注为生物科技领域的工业与科研用户提供卓越品质的生物原料与试剂解决方案</div>
  <div class="rule"></div>
  <div class="row" style="margin-top:.6vh">
    <div class="col">
      <div class="grid2">
        <div class="card blue"><div style="font-size:min(2.6vw,3.9vh);font-weight:800;color:var(--blue)">2012</div><div class="src-tag" contenteditable="true">公司成立</div></div>
        <div class="card blue"><div style="font-size:min(2.6vw,3.9vh);font-weight:800;color:var(--blue)">14 年</div><div class="src-tag" contenteditable="true">原料研发与客户服务</div></div>
        <div class="card blue"><div style="font-size:min(2.6vw,3.9vh);font-weight:800;color:var(--blue)">{esc(co['facility']['gmp'])}</div><div class="src-tag" contenteditable="true">GMP 车间（总计 {esc(co['facility']['total'])}）</div></div>
        <div class="card blue"><div style="font-size:min(2.6vw,3.9vh);font-weight:800;color:var(--blue)">3 大</div><div class="src-tag" contenteditable="true">业务板块：诊断原料 / 生命科学 / 生物医药</div></div>
      </div>
    </div>
    <div class="col">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.1vh" contenteditable="true">资质与平台</div>
      {ul(co['tags'])}
      <div style="font-size:var(--t-h3);font-weight:800;margin:1.5vh 0 1.1vh" contenteditable="true">六大技术平台</div>
      <div style="display:flex;gap:.5vw;flex-wrap:wrap">
        {''.join(f'<span class="pill" contenteditable="true">{esc(p)}</span>' for p in co['platforms'])}
      </div>
    </div>
  </div>
</div>
{footbar(18)}""", nav='三 · 公司概况')

# --- P19 发展历程（方案 A 时间线动画） ---
milestones = [
  ('2012','公司成立'), ('2015','诊断原料业务线建立'), ('2018','省工程技术研究中心'),
  ('2020','冻干服务平台建成'), ('2022','专精特新企业'), ('2024','生物医药板块拓展'), ('2026','七大场景体系成型'),
]
add('发展历程', f"""{topbar('关于宝锐')}
<div class="grow">
  <div class="kicker" contenteditable="true">MILESTONES</div>
  <h3 class="h3" contenteditable="true">发展历程</h3>
  <div class="rule cy"></div>
  <div class="tl" style="flex:1;margin-top:1.2vh">
    <svg viewBox="0 0 1200 260" preserveAspectRatio="xMidYMid meet">
      <path class="flowline" d="M40,130 L1160,130"/>
      {''.join(f'''<circle class="tlnode{' hi' if i==0 or i==len(milestones)-1 else ''}" cx="{40+i*(1120//(len(milestones)-1))}" cy="130" r="8"/>
      <text class="tlY" x="{40+i*(1120//(len(milestones)-1))}" y="{72 if i%2==0 else 200}" text-anchor="middle">{y}</text>
      <text class="tlT" x="{40+i*(1120//(len(milestones)-1))}" y="{92 if i%2==0 else 220}" text-anchor="middle">{t}</text>''' for i,(y,t) in enumerate(milestones))}
    </svg>
  </div>
</div>
{footbar(19)}""", nav='三 · 发展历程')

# --- P20 荣誉与质量体系 ---
add('荣誉质量', f"""{topbar('关于宝锐')}
<div class="grow">
  <div class="kicker" contenteditable="true">QUALITY</div>
  <h3 class="h3" contenteditable="true">荣誉与质量体系</h3>
  <div class="rule"></div>
  <div class="grid2" style="margin-top:.6vh">
    <div class="card">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.2vh" contenteditable="true">企业资质</div>
      {ul(co['tags'])}
    </div>
    <div class="card">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.2vh" contenteditable="true">质量与工艺</div>
      {ul(['工艺稳定技术先进，有效控制批间差异（PPT2 P19）',
           '全流程质控，覆盖蛋白进化 → 重组表达 → 工艺开发 → 应用验证',
           '产品已完成常温放置 18 个月稳定性验证（冻干平台）',
           '外源核酸酶残留、外源核酸残留纳入酶性能表征项目（PPT1 P26 表征清单）'])}
    </div>
  </div>
</div>
{footbar(20)}""", nav='三 · 荣誉与质量')

# --- P21 制造与研究平台 ---
add('制造研发', f"""{topbar('关于宝锐')}
<div class="grow">
  <div class="kicker" contenteditable="true">CAPACITY</div>
  <h3 class="h3" contenteditable="true">制造 · 研发 · 冻干 · 服务</h3>
  <div class="rule cy"></div>
  <div class="grid4" style="margin-top:1vh">
    <div class="card"><div class="pill" contenteditable="true">制造</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">{esc(co['facility']['gmp'])}</div>
      <div class="led" style="font-size:var(--t-note);color:var(--grey);line-height:1.7" contenteditable="true">GMP 车间；公司总建筑面积 {esc(co['facility']['total'])}</div></div>
    <div class="card"><div class="pill" contenteditable="true">研发</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">6 大平台</div>
      <div style="font-size:var(--t-note);color:var(--grey);line-height:1.7" contenteditable="true">{esc(' · '.join(co['platforms']))}</div></div>
    <div class="card"><div class="pill cy" contenteditable="true">冻干</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">全流程服务</div>
      <div style="font-size:var(--t-note);color:var(--grey);line-height:1.7" contenteditable="true">原料筛选验证 → 工艺优化 → 量产</div></div>
    <div class="card"><div class="pill cy" contenteditable="true">服务</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">应用支持</div>
      <div style="font-size:var(--t-note);color:var(--grey);line-height:1.7" contenteditable="true">按客户体系做平行比对与问题排查</div></div>
  </div>
</div>
{footbar(21)}""", nav='三 · 制造与研发平台')

# --- P22 研发平台 · 酶的进化研究（方案 C 双螺旋） ---
add('研发平台', f"""{topbar('关于宝锐')}
<div class="grow">
  <div class="kicker" contenteditable="true">R & D PLATFORM</div>
  <h3 class="h3" contenteditable="true">研发平台 · 酶的进化研究</h3>
  <div class="rule cy"></div>
  <div class="row" style="margin-top:.4vh;flex:1">
    <div class="col" style="display:flex;flex-direction:column;justify-content:center">
      {ul(['依托蛋白进化、重组表达、抗体开发、适配体筛选、工艺开发、应用研究六大平台',
           '对聚合酶、逆转录酶进行定向进化与理性设计，筛选高温耐受、高抗抑制突变体',
           '酶性能表征覆盖：聚合酶性能 / 反转录酶性能 / 外切酶活性 / 功能性验证 / 外源核酸酶残留 / 外源核酸残留 / 理化性质 / IC50 模型'])}
    </div>
    <div class="col" style="min-height:0;display:flex;align-items:center;justify-content:center">
      <div style="width:100%;height:44vh;position:relative" data-helix data-amp="92" data-turns="3.2">
        <svg viewBox="0 0 1000 340" preserveAspectRatio="xMidYMid meet" style="width:100%;height:100%"></svg>
      </div>
    </div>
  </div>
  <div class="card blue" style="margin-top:1.2vh"><div class="lead" contenteditable="true"><b>十余年只做一件事：把酶的性能指标一项项压到极致。</b>每一项指标都有实测数据支撑（见第四部分七个场景）。</div></div>
</div>
{footbar(22)}""", cls='', nav='三 · 研发平台（双螺旋）')

# --- P23 冻干服务与仪器 ---
add('冻干服务', f"""{topbar('关于宝锐')}
<div class="grow">
  <div class="kicker" contenteditable="true">LYO SERVICE</div>
  <h3 class="h3" contenteditable="true">冻干服务与仪器</h3>
  <div class="rule"></div>
  <div class="grid3" style="margin-top:1vh">
    <div class="card"><div class="pill" contenteditable="true">第一步</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">原料筛选验证</div>
      <div class="lead" contenteditable="true">确认原料可冻干性与配方兼容性，避免后期返工</div></div>
    <div class="card"><div class="pill" contenteditable="true">第二步</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">冻干工艺优化</div>
      <div class="lead" contenteditable="true">保护剂配方 · 冻干曲线 · 复溶性验证</div></div>
    <div class="card"><div class="pill cy" contenteditable="true">第三步</div>
      <div style="font-size:var(--t-h3);font-weight:800;margin:1vh 0 .7vh" contenteditable="true">冻干量产</div>
      <div class="lead" contenteditable="true">批间稳定性 · 常温储运验证 · 量产放大</div></div>
  </div>
  <div class="card" style="margin-top:1.4vh">
    <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.1vh" contenteditable="true">可冻干原料品类齐全</div>
    <div style="display:flex;gap:.6vw;flex-wrap:wrap">
      {''.join(f'<span class="pill" contenteditable="true">{x}</span>' for x in ['Taq 聚合酶','MMLV 逆转录酶','UNG','Tth','探针','单酶','预混液'])}
    </div>
    <div class="lead" style="margin-top:1.2vh" contenteditable="true">产品已完成<b>常温放置 18 个月稳定性验证</b>；全预混 37℃ 7 天稳定；抗吸湿冻干保护剂，冻干环境湿度控制要求低。</div>
  </div>
</div>
{footbar(23)}""", nav='三 · 冻干服务与仪器')

# --- P24 / P25 成功案例（占位骨架，待素材） ---
for k in (1,2):
    add(f'案例 {k}', f"""{topbar('成功案例')}
<div class="grow">
  <div class="kicker" contenteditable="true">CASE STUDY {k}</div>
  <h3 class="h3" contenteditable="true">成功案例 {k}</h3>
  <div class="rule cy"></div>
  <div class="card blue" style="margin-top:1vh">
    <div style="display:flex;align-items:center;gap:.8vw;margin-bottom:1.4vh">
      <span class="pill cy" contenteditable="true">待补充素材</span>
      <span class="src-tag" contenteditable="true">本页为占位骨架 —— 客户信息需授权后方可使用，暂不填写具体客户名</span>
    </div>
    {dtable([['客户类型','【待填：如 某头部 IVD 企业 / 某省疾控 / 某 ICL 集团】'],
             ['应用场景','【待填：如 冻干型呼吸道多联检 / 全血直扩分型】'],
             ['关键指标','【待填：如 批间 CV < 5% / 常温 18 个月活性保留 > 90%】'],
             ['协同方式','【待填：联合开发 / 定制化 / 工艺共建】'],
             ['合作成果','【待填：如 成功注册上市 / 供货份额提升 / 成本下降】']], ['维度','内容'])}
  </div>
  <div class="card" style="margin-top:1.3vh"><div class="lead" contenteditable="true"><b>说明：</b>本页预留给你认为可对外披露的案例。若采用匿名单例，可写为「某头部 IVD 企业 · 冻干体系 · 批间 CV&lt;5%」这类不含客户名的表述。</div></div>
</div>
{footbar(24+k)}""", nav=f'三 · 成功案例 {k}（待素材）')

# ============================================================
#  第四部分 · 七大热点场景（P26–P39）
# ============================================================
add('四 · 承接页', stage_page('04', '七大热点应用场景', '分子诊断流水线 · 抗抑制直扩 · 快速扩增 · 多重溶解曲线 · 单管全预混 · 无核酸残留 · 冻干'),
    cls='stage', nav='四 · 七大场景 · 承接页')

# --- P26 七场景总览 ---
add('七场景总览', f"""{topbar('七大热点场景')}
<div class="grow">
  <div class="kicker" contenteditable="true">SEVEN SCENARIOS</div>
  <h3 class="h3" contenteditable="true">七个场景，逐个用数据对话</h3>
  <div class="rule"></div>
  <div class="scGrid" style="margin-top:1vh">
    {''.join(f'''<div class="scCard">
      <div class="no">0{i}</div>
      <div class="nm" contenteditable="true">{esc(s['name'])}</div>
      <div class="ds" contenteditable="true">{esc(s['need']['subtitle'])}</div>
    </div>''' for i,s in enumerate(DATA['scenarios'],1))}
    <div class="scCard" style="background:linear-gradient(150deg,rgba(0,47,167,.06),rgba(0,212,255,.04))">
      <div class="no">→</div>
      <div class="nm" contenteditable="true">需求分析 + 宝锐数据</div>
      <div class="ds" contenteditable="true">每个场景 2 页：先讲你的卡点，再给实测数据</div>
    </div>
  </div>
</div>
{footbar(26)}""", nav='四 · 七场景总览')

# --- P27–P39 场景页（每场景：需求页 + 数据页） ---
pno = 27
for idx, sc in enumerate(DATA['scenarios'], 1):
    nd = sc['need']; dt = sc['data']
    # 需求页
    add(f'场景{idx} 需求', f"""{topbar('七大热点场景')}
<div class="grow">
  {scene_head('0%d' % idx, sc['name'] + ' · 需求分析', 'SCENARIO %d / 7' % idx)}
  <div class="sub" contenteditable="true">{esc(nd['subtitle'])}</div>
  <div class="rule"></div>
  <div class="row" style="margin-top:.6vh">
    <div class="col">
      <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1.3vh" contenteditable="true">客户端的需求与卡点</div>
      {ul(nd['points'])}
    </div>
    <div class="col" style="display:flex;align-items:center">
      <div class="card blue" style="width:100%">
        <div style="font-size:var(--t-h3);font-weight:800;margin-bottom:1vh;color:var(--blue)" contenteditable="true">这个场景要什么</div>
        <div class="lead" contenteditable="true">需要原料在<b>该场景的关键指标</b>上达到可量产、可注册、批间稳定的水平 —— 下一页给宝锐实测数据。</div>
      </div>
    </div>
  </div>
</div>
{footbar(pno)}""", nav=f'四 · 场景{idx} 需求')
    pno += 1
    # 数据页
    body = ''
    if dt.get('chart'):
        ch = dt['chart']
        if ch['type'] == 'bar':
            body = bartable(ch, h='29vh')
        elif ch['type'] == 'kv':
            body = dtable([list(r) for r in ch['rows']], ['项目','实测结果'])
    blocks = []
    if body: blocks.append(f'<div style="margin-bottom:1.2vh">{body}</div>')
    if dt.get('items'):
        blocks.append(dtable([[i['k'], i['v'], i.get('src','')] for i in dt['items']], ['项目','宝锐实测 / 事实','来源']))
    if dt.get('note'):
        blocks.append(f'<div class="card blue" style="margin-top:1.2vh"><div class="lead" contenteditable="true">{esc(dt["note"])}</div></div>')
    add(f'场景{idx} 数据', f"""{topbar('七大热点场景')}
<div class="grow">
  {scene_head('0%d' % idx, sc['name'] + ' · 宝锐数据', 'SCENARIO %d / 7 · DATA' % idx)}
  <h3 class="h3" style="margin-bottom:.4vh" contenteditable="true">{esc(dt['title'])}</h3>
  <div class="rule cy" style="margin:1vh 0 1.4vh"></div>
  {''.join(blocks)}
</div>
{footbar(pno)}""", nav=f'四 · 场景{idx} 数据')
    pno += 1

# --- P40 七场景货号速查 ---
add('货号速查', f"""{topbar('七大热点场景')}
<div class="grow">
  <div class="kicker" contenteditable="true">SKU MAP</div>
  <h3 class="h3" contenteditable="true">七场景 × 推荐体系货号速查</h3>
  <div class="rule"></div>
  <div style="margin-top:.8vh">
    {dtable([[c['scenario'], c['sku'], c['name'], c['note']] for c in DATA['catalog']], ['场景','推荐货号','产品名称','关键说明'])}
  </div>
  <div class="card blue" style="margin-top:1.3vh"><div class="lead" contenteditable="true">标注 <b>FM / FMD</b> 前缀为可冻干货号，可直接用于冻干体系开发。</div></div>
</div>
{footbar(40)}""", nav='四 · 货号速查')

# --- P41 结语（方案 D + Logo/Slogan） ---
add('结语', f"""<div class="canvasHost" data-mol data-n="46" data-d="165" data-a="0.14"></div>
<div class="st-num">—</div>
<div class="stageTxt">
  <div class="tb-logo" role="img" aria-label="宝锐生物 BIORI BIOTECH" style="width:26vh;height:8.8vh;min-width:230px;min-height:78px;margin-bottom:3.4vh"></div>
  <h2 contenteditable="true">把体系交给我们，把时间留给您的团队</h2>
  <div class="u"></div>
  <div style="margin-top:3.6vh;font-size:min(2.5vw,3.8vh);font-weight:800;letter-spacing:5px;background:linear-gradient(96deg,var(--blue),var(--cyan));-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent" contenteditable="true">守护生命健康<b style="-webkit-text-fill-color:var(--cyan)">·</b>缔造美好生活</div>
  <div class="st-sub" style="margin-top:2.6vh" contenteditable="true">珠海宝锐生物科技有限公司 ｜ BIORI BIOTECH</div>
</div>""", cls='stage', nav='结语')

# ============================================================
#  元素编辑器（Element Editor）—— 增删图片/视频/色块/文本框/动画
# ============================================================
ELEM_CSS = r"""
/* ---------- 元素编辑模式 ---------- */
body.elem .slide.on [data-el]{position:relative}
body.elem *{cursor:default}
body.elem [data-el]:hover{outline:1.5px dashed rgba(0,212,255,.75);outline-offset:2px}
body.elem [data-el].sel{outline:2px solid var(--cyan) !important;outline-offset:2px}
body.elem [contenteditable="true"]{cursor:text}

/* 选中后的小工具条 */
.elbar{
  position:absolute;z-index:120;display:none;gap:3px;
  padding:3px;border-radius:8px;background:rgba(10,16,40,.94);
  box-shadow:0 6px 22px rgba(0,0,0,.28);
}
body.elem .elbar.on{display:flex}
.elbar button{
  padding:.5vh .58vw;border:none;border-radius:6px;cursor:pointer;
  background:transparent;color:#fff;font-family:var(--fnt);
  font-size:1.02vh;font-weight:700;white-space:nowrap;transition:.14s;
}
.elbar button:hover{background:rgba(0,212,255,.28)}
.elbar button.danger:hover{background:rgba(220,60,60,.55)}

/* ---------- 新增元素通用 ---------- */
[data-added]{position:absolute;z-index:40}
[data-added].sel{box-shadow:0 0 0 2px var(--cyan),0 6px 24px rgba(0,0,0,.16)}
[data-added] .elResize{
  position:absolute;right:-6px;bottom:-6px;width:13px;height:13px;
  background:var(--cyan);border:2px solid #fff;border-radius:3px;
  cursor:nwse-resize;display:none;z-index:5;
}
[data-added].sel .elResize{display:block}
[data-added] .elDel{
  position:absolute;right:-9px;top:-9px;width:19px;height:19px;border-radius:50%;
  background:#e5484d;color:#fff;border:2px solid #fff;font-size:11px;line-height:15px;
  text-align:center;cursor:pointer;display:none;z-index:6;font-weight:800;
}
[data-added].sel .elDel{display:block}

/* ---------- 元素工具箱 ---------- */
.elPanel{
  position:fixed;left:0;bottom:0;z-index:215;width:23vw;min-width:290px;
  background:#fff;border-right:1px solid var(--line);
  border-top:1px solid var(--line);border-radius:0 12px 0 0;
  box-shadow:6px -6px 34px rgba(0,47,167,.12);
  transform:translateY(102%);transition:transform .3s cubic-bezier(.2,.8,.3,1);
  max-height:78vh;display:flex;flex-direction:column;
}
.elPanel.on{transform:none}
.elPanel .ehd{padding:1.5vh 1vw 1.1vh;border-bottom:1px solid var(--line)}
.elPanel .ehd h4{font-size:1.5vh;font-weight:800;color:var(--ink)}
.elPanel .ehd p{font-size:1.0vh;color:var(--grey);margin-top:.3vh;line-height:1.55}
.elPanel .ebody{flex:1;overflow-y:auto;padding:.9vh .9vw}
.elPanel .esec{margin-bottom:1.3vh}
.elPanel .esec>label{display:block;font-size:1.02vh;font-weight:800;color:var(--blue);letter-spacing:.5px;margin-bottom:.6vh}
.elPanel .erow{display:flex;gap:.4vw;flex-wrap:wrap}
.elPanel .ebtn{
  flex:1;min-width:calc(33.3% - .3vw);padding:.82vh .4vw;border-radius:8px;cursor:pointer;
  border:1px solid var(--line);background:#fff;color:var(--ink2);
  font-family:var(--fnt);font-size:1.04vh;font-weight:700;transition:.16s;
}
.elPanel .ebtn:hover{border-color:var(--blue);color:var(--blue);background:rgba(0,47,167,.03)}
.elPanel .ebtn.wide{min-width:calc(50% - .3vw)}
.elPanel input[type=color]{width:100%;height:3.2vh;border:1px solid var(--line);border-radius:7px;cursor:pointer;padding:2px}
.elPanel .eswatches{display:flex;gap:.34vw;margin-top:.5vh}
.elPanel .eswatches i{
  flex:1;height:2.4vh;border-radius:6px;cursor:pointer;border:2px solid transparent;
}
.elPanel .eswatches i:hover{border-color:var(--blue)}
.elToggle{
  position:fixed;top:.9vh;right:9.4vw;z-index:151;
  padding:.72vh .88vw;border-radius:8px;cursor:pointer;
  font-family:var(--fnt);font-size:1.16vh;font-weight:800;letter-spacing:.4px;
  border:1px solid var(--line);background:rgba(255,255,255,.95);color:var(--blue);
  box-shadow:0 2px 12px rgba(0,47,167,.1);transition:.18s;
}
.elToggle:hover{background:var(--blue);color:#fff;border-color:var(--blue)}
body.elem .elToggle{background:#0a7f3f;color:#fff;border-color:#0a7f3f}
body.elem .rvToggle{opacity:.45}
body.elem .elHint{
  position:fixed;top:2.1vh;left:50%;transform:translateX(-50%);z-index:160;
  padding:.75vh 1.1vw;border-radius:20px;background:rgba(10,16,40,.9);color:#fff;
  font-size:1.1vh;font-weight:700;letter-spacing:.4px;display:none;
}
body.elem .elHint.on{display:block}
@media print{ .elPanel,.elToggle,.elHint,.elbar{display:none !important} }
"""

ELEM_JS = r"""
/* ============================================================
   元素编辑器：添加/删除/拖动/缩放/动画
   ============================================================ */
var EL = {on:false, sel:null, counter:0};

function elToggle(){
  EL.on = !EL.on;
  document.body.classList.toggle('elem', EL.on);
  document.getElementById('elPanel').classList.toggle('on', EL.on);
  if(!EL.on) elUnselect();
  elHint(EL.on ? '元素编辑模式：点选元素 → 删除/改色/动画；左侧工具箱可添加内容' : '');
}
function elHint(t){
  var h=document.getElementById('elHint'); if(!h) return;
  h.textContent = t; h.classList.toggle('on', !!t);
  clearTimeout(window.__elht); if(t) window.__elht = setTimeout(function(){h.classList.remove('on')}, 3200);
}

/* ---------- 选中 ---------- */
function elSelect(el){
  elUnselect();
  EL.sel = el; el.classList.add('sel');
  var bar = document.getElementById('elbar');
  var r = el.getBoundingClientRect();
  bar.classList.add('on');
  bar.style.left = Math.max(6, r.left + window.scrollX) + 'px';
  bar.style.top  = Math.max(6, r.top + window.scrollY - 34) + 'px';
  // 记录原始样式（撤销用）
  if(el.dataset.added && el.dataset._o === undefined){
    el.dataset._o = JSON.stringify({left:el.style.left,top:el.style.top,w:el.style.width,h:el.style.height,anim:el.dataset.anim||''});
  }
}
function elUnselect(){
  if(EL.sel) EL.sel.classList.remove('sel');
  EL.sel = null;
  var b=document.getElementById('elbar'); if(b) b.classList.remove('on');
}
function elDelete(){
  if(!EL.sel) return;
  var el = EL.sel;
  var slide = el.closest('.slide');
  var i = slide ? (+slide.dataset.i + 1) : 0;
  elUnselect();
  el.remove();
  elPersist(i);
  elHint('已删除元素');
}
function elDup(){
  if(!EL.sel) return;
  var el = EL.sel;
  if(!el.dataset.added){ elHint('仅新增元素可复制'); return; }
  var c = el.cloneNode(true);
  c.style.left = (parseFloat(el.style.left||0) + 18) + 'px';
  c.style.top  = (parseFloat(el.style.top||0) + 18) + 'px';
  el.parentNode.appendChild(c);
  elPersist(+el.closest('.slide').dataset.i + 1);
  elHint('已复制');
}
function elToFront(){ if(EL.sel){ EL.sel.style.zIndex = 90; elPersist(+EL.sel.closest('.slide').dataset.i+1); } }
function elToBack(){  if(EL.sel){ EL.sel.style.zIndex = 1;  elPersist(+EL.sel.closest('.slide').dataset.i+1); } }

/* ---------- 改色 ---------- */
function elSetColor(c){
  if(!EL.sel) return;
  var el = EL.sel;
  if(el.dataset.added === 'block') el.style.background = c;
  else el.style.color = c;
  elPersist(+el.closest('.slide').dataset.i+1);
  elHint('颜色已更新');
}
function elSetBg(c){
  if(!EL.sel) return;
  var el = EL.sel;
  if(el.dataset.added) el.style.background = c;
  else { el.style.background = c; el.style.padding = el.style.padding || '.6vh .8vw'; el.style.borderRadius = el.style.borderRadius || '8px'; }
  elPersist(+el.closest('.slide').dataset.i+1);
  elHint('底色已更新');
}

/* ---------- 动画 ---------- */
var EL_ANIMS = {
  '':            '无动画',
  'fadeIn':      '淡入',
  'slideUp':     '向上滑入',
  'slideLeft':   '向左滑入',
  'zoomIn':      '缩放进入',
  'float':       '持续浮动',
  'pulse':       '呼吸脉动',
  'rotateSlow':  '缓慢旋转',
  'hueShift':    '色相流光'
};
function elSetAnim(a){
  if(!EL.sel) return;
  var el = EL.sel;
  Object.keys(EL_ANIMS).forEach(function(k){ if(k) el.classList.remove('el-'+k); });
  el.classList.remove('el-in');
  if(a){
    if(a==='fadeIn'||a==='slideUp'||a==='slideLeft'||a==='zoomIn'){
      el.classList.add('el-'+a);
      void el.offsetWidth;
      el.classList.add('el-in');
      setTimeout(function(){ el.classList.add('el-done'); }, 1000);
    } else {
      el.classList.add('el-'+a);      // 循环类直接加
    }
    el.dataset.anim = a;
  } else {
    el.dataset.anim = '';
  }
  elPersist(+el.closest('.slide').dataset.i+1);
  elHint('动画：' + EL_ANIMS[a]);
}
function elApplyAnim(){
  if(!EL.sel) return;
  var v = document.getElementById('elAnimSel').value;
  elSetAnim(v);
}

/* ---------- 添加元素 ---------- */
function elNextId(){ return 'el' + (++EL.counter) + '_' + Date.now().toString(36); }
function elNewSlide(){
  var s = document.querySelectorAll('.slide')[CUR];
  if(!s){ elHint('未找到当前页'); return null; }
  return s;
}
function elAddBlock(color, w, h){
  var s = elNewSlide(); if(!s) return;
  var d = document.createElement('div');
  d.setAttribute('data-added','block');
  d.setAttribute('data-el','1');
  d.id = elNextId();
  d.style.cssText = 'left:10%;top:30%;width:'+(w||180)+'px;height:'+(h||90)+'px;'
    + 'background:'+(color||'#002FA7')+';border-radius:8px;cursor:move;';
  d.innerHTML = '<span class="elResize"></span><span class="elDel" onclick="event.stopPropagation();elDelete()">×</span>';
  s.appendChild(d);
  elBindAdded(d);
  elSelect(d);
  elPersist(+s.dataset.i+1);
  elHint('已添加色块（可拖动 / 右下角缩放）');
}
function elAddText(){
  var s = elNewSlide(); if(!s) return;
  var d = document.createElement('div');
  d.setAttribute('data-added','text');
  d.setAttribute('data-el','1');
  d.id = elNextId();
  d.style.cssText = 'left:12%;top:38%;min-width:120px;color:#0a0a0a;'
    + 'font-size:min(1.6vw,2.5vh);font-weight:700;padding:.5vh .6vw;cursor:move;';
  d.innerHTML = '<span class="elResize"></span><span class="elDel" onclick="event.stopPropagation();elDelete()">×</span>';
  var t = document.createElement('div');
  t.setAttribute('contenteditable','true');
  t.textContent = '双击编辑文字';
  d.appendChild(t);
  s.appendChild(d);
  elBindAdded(d);
  elSelect(d);
  elPersist(+s.dataset.i+1);
  elHint('已添加文本框（可拖动 / 点击改字）');
}
function elAddImage(input){
  var f = input.files && input.files[0];
  if(!f){ return; }
  if(f.size > 4.5*1024*1024){ alert('图片过大（>4.5MB），会导致文件膨胀且自动保存失败。请先压缩。'); }
  var rd = new FileReader();
  rd.onload = function(){
    var s = elNewSlide(); if(!s) return;
    var d = document.createElement('div');
    d.setAttribute('data-added','image');
    d.setAttribute('data-el','1');
    d.id = elNextId();
    d.style.cssText = 'left:8%;top:26%;width:340px;cursor:move;';
    d.innerHTML = '<span class="elResize"></span><span class="elDel" onclick="event.stopPropagation();elDelete()">×</span>'
      + '<img src="'+rd.result+'" style="width:100%;display:block;border-radius:6px">';
    s.appendChild(d);
    elBindAdded(d);
    elSelect(d);
    elPersist(+s.dataset.i+1, true);
    elHint('已添加图片：' + f.name);
  };
  rd.readAsDataURL(f);
  input.value = '';
}
function elAddVideo(input){
  var f = input.files && input.files[0];
  if(!f){ return; }
  if(f.size > 8*1024*1024){
    if(!confirm('视频较大（' + (f.size/1048576).toFixed(1) + 'MB），内嵌后 HTML 会显著变大、自动保存可能失败。\n\n继续？（建议改用「视频链接」方式）')) { input.value=''; return; }
  }
  var rd = new FileReader();
  rd.onload = function(){
    var s = elNewSlide(); if(!s) return;
    var d = document.createElement('div');
    d.setAttribute('data-added','video');
    d.setAttribute('data-el','1');
    d.id = elNextId();
    d.style.cssText = 'left:8%;top:26%;width:420px;cursor:move;';
    d.innerHTML = '<span class="elResize"></span><span class="elDel" onclick="event.stopPropagation();elDelete()">×</span>'
      + '<video src="'+rd.result+'" controls style="width:100%;display:block;border-radius:6px;background:#000"></video>';
    s.appendChild(d);
    elBindAdded(d);
    elSelect(d);
    elPersist(+s.dataset.i+1, true);
    elHint('已添加视频：' + f.name + '（' + (f.size/1048576).toFixed(1) + 'MB）');
  };
  rd.readAsDataURL(f);
  input.value = '';
}
function elAddVideoUrl(){
  var s = elSlideOrNull(); if(!s) return;
  var u = prompt('输入视频链接（mp4 / 直链）：');
  if(!u) return;
  var d = document.createElement('div');
  d.setAttribute('data-added','video');
  d.setAttribute('data-el','1');
  d.id = elNextId();
  d.style.cssText = 'left:8%;top:26%;width:420px;cursor:move;';
  d.innerHTML = '<span class="elResize"></span><span class="elDel" onclick="event.stopPropagation();elDelete()">×</span>'
    + '<video src="'+u+'" controls style="width:100%;display:block;border-radius:6px;background:#000"></video>';
  s.appendChild(d); elBindAdded(d); elSelect(d); elPersist(+s.dataset.i+1);
  elHint('已添加外链视频（注意：播放需联网，导出的 HTML 依赖该链接）');
}
function elAddSvg(kind){
  var s = elNewSlide(); if(!s) return;
  var svg = '';
  if(kind==='flow'){
    svg = '<svg viewBox="0 0 400 120" width="400" height="120"><defs><linearGradient id="g1"><stop offset="0" stop-color="#002FA7"/><stop offset="1" stop-color="#00D4FF"/></linearGradient></defs>'
        + '<path d="M10,60 C120,10 280,110 390,60" fill="none" stroke="url(#g1)" stroke-width="3" stroke-dasharray="10 8" class="el-flow"/>'
        + '<circle cx="10" cy="60" r="5" fill="#002FA7"/><circle cx="390" cy="60" r="5" fill="#00D4FF"/></svg>';
  } else if(kind==='molecule'){
    svg = '<svg viewBox="0 0 200 200" width="200" height="200">'
        + '<g stroke="#002FA7" stroke-width="1.6" opacity=".55">'
        + '<line x1="100" y1="100" x2="40" y2="50"/><line x1="100" y1="100" x2="165" y2="60"/>'
        + '<line x1="100" y1="100" x2="60" y2="165"/><line x1="100" y1="100" x2="170" y2="150"/></g>'
        + '<circle cx="100" cy="100" r="18" fill="#002FA7" class="el-pulse"/>'
        + '<circle cx="40" cy="50" r="10" fill="#00D4FF"/><circle cx="165" cy="60" r="10" fill="#00D4FF"/>'
        + '<circle cx="60" cy="165" r="10" fill="#1E5BFF"/><circle cx="170" cy="150" r="10" fill="#1E5BFF"/></svg>';
  } else {
    svg = '<svg viewBox="0 0 240 60" width="240" height="60">'
        + '<rect x="2" y="24" width="60" height="12" rx="6" fill="#002FA7"/>'
        + '<rect x="90" y="24" width="60" height="12" rx="6" fill="#1E5BFF" opacity=".8"/>'
        + '<rect x="178" y="24" width="60" height="12" rx="6" fill="#00D4FF" opacity=".7"/></svg>';
  }
  var d = document.createElement('div');
  d.setAttribute('data-added','svg');
  d.setAttribute('data-el','1');
  d.id = elNextId();
  d.style.cssText = 'left:12%;top:34%;cursor:move;';
  d.innerHTML = '<span class="elResize"></span><span class="elDel" onclick="event.stopPropagation();elDelete()">×</span>' + svg;
  s.appendChild(d); elBindAdded(d); elSelect(d); elPersist(+s.dataset.i+1);
  elHint('已添加图案');
}
function elSlideOrNull(){ return elNewSlide(); }

/* ---------- 拖动 / 缩放 ---------- */
function elBindAdded(el){
  el.setAttribute('draggable','false');
  el.onmousedown = function(e){
    if(EL.on === false) return;
    if(e.target.classList.contains('elResize') || e.target.classList.contains('elDel')) return;
    if(e.target.isContentEditable && document.activeElement === e.target) return;
    if(!EL.sel || EL.sel !== el) { elSelect(el); }
    var sx=e.clientX, sy=e.clientY;
    var ox=parseFloat(el.style.left)||0, oy=parseFloat(el.style.top)||0;
    var par=el.parentNode, pr=par.getBoundingClientRect();
    function mv(ev){
      var nx = ox + (ev.clientX - sx), ny = oy + (ev.clientY - sy);
      el.style.left = Math.max(-pr.width+40, Math.min(pr.width-20, nx)) + 'px';
      el.style.top  = Math.max(-20, Math.min(pr.height-20, ny)) + 'px';
    }
    function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up);
      var sl=el.closest('.slide'); if(sl) elPersist(+sl.dataset.i+1); }
    document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
    e.preventDefault();
  };
  var rz = el.querySelector('.elResize');
  if(rz){
    rz.onmousedown = function(e){
      e.stopPropagation(); e.preventDefault();
      var sx=e.clientX, sy=e.clientY;
      var ow=el.offsetWidth, oh=el.offsetHeight;
      function mv(ev){
        el.style.width  = Math.max(40, ow + (ev.clientX-sx)) + 'px';
        var img = el.querySelector('img,video,svg');
        if(img) img.style.height = 'auto';
        else el.style.minHeight = Math.max(24, oh + (ev.clientY-sy)) + 'px';
      }
      function up(){ document.removeEventListener('mousemove',mv); document.removeEventListener('mouseup',up);
        var sl=el.closest('.slide'); if(sl) elPersist(+sl.dataset.i+1); }
      document.addEventListener('mousemove',mv); document.addEventListener('mouseup',up);
    };
  }
}

/* ---------- 持久化（按页存 DOM 快照） ---------- */
var ELKEY = 'deckElem::' + document.title;
var ELSTORE = {pages:{}, updated:''};
try{ var _e = JSON.parse(localStorage.getItem(ELKEY)||'null'); if(_e && _e.pages) ELSTORE = _e; }catch(e){}

function elPersist(pageNo, big){
  var s = document.querySelectorAll('.slide')[pageNo-1];
  if(!s) return;
  ELSTORE.pages[pageNo] = s.innerHTML;
  ELSTORE.updated = new Date().toISOString();
  try{
    localStorage.setItem(ELKEY, JSON.stringify(ELSTORE));
    elHint('已保存（含元素改动）');
  }catch(err){
    elHint('⚠ 保存失败：内容过大（图片/视频占用存储），请先「导出确稿 HTML」保住成果');
    console.warn('elPersist failed', err);
  }
}
function elRestore(){
  var n = 0;
  document.querySelectorAll('.slide').forEach(function(s){
    var i = +s.dataset.i + 1;
    if(ELSTORE.pages[i]){
      s.innerHTML = ELSTORE.pages[i];
      s.querySelectorAll('[data-added]').forEach(function(el){ el.setAttribute('data-el','1'); elBindAdded(el); });
      n++;
    }
  });
  return n;
}
function elExportDeck(){
  var doc = document.cloneNode(true);
  doc.querySelectorAll('.rvPanel,.rvToggle,.rvbar,.rvBadge,.elPanel,.elToggle,.elbar,.elHint,.editHint,#toc').forEach(function(e){ e.remove(); });
  doc.querySelectorAll('[contenteditable]').forEach(function(e){ e.removeAttribute('contenteditable'); });
  doc.querySelectorAll('.sel').forEach(function(e){ e.classList.remove('sel'); });
  var b = doc.querySelector('body'); if(b) b.className='';
  var html = '<!DOCTYPE html>' + doc.documentElement.outerHTML;
  elDownload(html, document.title.replace(/[\/\\:*?"<>|🐴 ]/g,'') + '_元素版.html', 'text/html');
}
function elResetElems(){
  if(!confirm('清除全部元素改动（新增的图片/视频/色块等），回到初始版本？\n审稿状态与文字改动不受影响。')) return;
  localStorage.removeItem(ELKEY);
  location.reload();
}
function elDownload(content, name, mime){
  var b = new Blob([content], {type: mime + ';charset=utf-8'});
  var a = document.createElement('a');
  a.href = URL.createObjectURL(b); a.download = name;
  document.body.appendChild(a); a.click();
  setTimeout(function(){ URL.revokeObjectURL(a.href); a.remove(); }, 400);
  elHint('已导出 ' + name);
}

/* ---------- 事件委托（一律委托到 document，防 restore 丢绑定） ---------- */
document.addEventListener('click', function(e){
  if(!EL.on) return;
  if(e.target.closest('.elPanel,.elToggle,.elbar,.rvPanel,.rvToggle,.toolbar,#toc')) return;
  if(e.target.closest('[data-added]')){ elSelect(e.target.closest('[data-added]')); return; }
  var sl = e.target.closest('.slide');
  if(sl){
    var t = e.target.closest('h1,h2,h3,div,td,th,li,p,table,svg,img');
    if(t && t.closest('.slide')){ elSelect(t); }
  }
});
document.addEventListener('keydown', function(e){
  if(!EL.on) return;
  if(typeof isTyping === 'function' && isTyping(e)) return;
  if(e.key==='Delete' || e.key==='Backspace'){ if(EL.sel){ e.preventDefault(); elDelete(); } }
  if(e.key==='Escape'){ elUnselect(); }
}, true);
"""

def elem_toolbar():
    return r"""<button class="elToggle" onclick="elToggle()" title="元素编辑（可加图片/视频/色块，可删元素）">▦ 元素编辑</button>
<div class="elHint" id="elHint"></div>
<div class="elbar" id="elbar">
  <button onclick="elDup()">⧉ 复制</button>
  <button onclick="elToFront()">⬆ 前移</button>
  <button onclick="elToBack()">⬇ 后移</button>
  <button class="danger" onclick="elDelete()">🗑 删除</button>
</div>
<aside class="elPanel" id="elPanel">
  <div class="ehd">
    <h4>▦ 元素编辑</h4>
    <p>添加图片/视频/色块/文本框 · 点选元素可删除、改色、加动画 · 新增元素可拖动与缩放</p>
  </div>
  <div class="ebody">
    <div class="esec">
      <label>添加内容</label>
      <div class="erow">
        <button class="ebtn wide" onclick="document.getElementById('elImgFile').click()">🖼 图片</button>
        <button class="ebtn wide" onclick="document.getElementById('elVidFile').click()">🎬 视频（本地）</button>
        <button class="ebtn wide" onclick="elAddVideoUrl()">🔗 视频（链接）</button>
        <button class="ebtn wide" onclick="elAddBlock('#002FA7',180,90)">◼ 色块</button>
        <button class="ebtn wide" onclick="elAddText()">🅣 文本框</button>
        <button class="ebtn wide" onclick="elAddSvg('flow')">〰 流线图案</button>
        <button class="ebtn wide" onclick="elAddSvg('molecule')">⚛ 分子图案</button>
        <button class="ebtn wide" onclick="elAddSvg('bars')">▤ 柱条图案</button>
      </div>
      <input type="file" id="elImgFile" accept="image/*" style="display:none" onchange="elAddImage(this)">
      <input type="file" id="elVidFile" accept="video/*" style="display:none" onchange="elAddVideo(this)">
    </div>
    <div class="esec">
      <label>选中元素的颜色</label>
      <input type="color" value="#002FA7" oninput="elSetColor(this.value)">
      <div class="eswatches">
        <i style="background:#002FA7" onclick="elSetColor('#002FA7')"></i>
        <i style="background:#1E5BFF" onclick="elSetColor('#1E5BFF')"></i>
        <i style="background:#00D4FF" onclick="elSetColor('#00D4FF')"></i>
        <i style="background:#0a0a0a" onclick="elSetColor('#0a0a0a')"></i>
        <i style="background:#6e6e73" onclick="elSetColor('#6e6e73')"></i>
        <i style="background:#ffffff;border-color:#ddd" onclick="elSetColor('#ffffff')"></i>
      </div>
    </div>
    <div class="esec">
      <label>选中元素的底色</label>
      <input type="color" value="#f5f5f7" oninput="elSetBg(this.value)">
    </div>
    <div class="esec">
      <label>选中元素的动画</label>
      <div class="erow">
        <select id="elAnimSel" class="ebtn wide" style="min-width:100%;height:3.4vh">
          <option value="">无动画</option>
          <option value="fadeIn">淡入</option>
          <option value="slideUp">向上滑入</option>
          <option value="slideLeft">向左滑入</option>
          <option value="zoomIn">缩放进入</option>
          <option value="float">持续浮动</option>
          <option value="pulse">呼吸脉动</option>
          <option value="rotateSlow">缓慢旋转</option>
          <option value="hueShift">色相流光</option>
        </select>
        <button class="ebtn wide" onclick="elApplyAnim()">应用动画</button>
      </div>
    </div>
    <div class="esec">
      <label>导出 / 重置</label>
      <div class="erow">
        <button class="ebtn wide" onclick="elExportDeck()">⬇ 导出元素版 HTML</button>
        <button class="ebtn wide" onclick="elResetElems()">⟲ 清除元素改动</button>
      </div>
    </div>
  </div>
</aside>"""

# 动画 keyframes（追加到 CSS）
ELEM_ANIM_CSS = r"""
@keyframes elFadeIn{from{opacity:0}to{opacity:1}}
@keyframes elSlideUp{from{opacity:0;transform:translateY(26px)}to{opacity:1;transform:none}}
@keyframes elSlideLeft{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:none}}
@keyframes elZoomIn{from{opacity:0;transform:scale(.86)}to{opacity:1;transform:none}}
@keyframes elFloat{0%,100%{transform:translateY(0)}50%{transform:translateY(-10px)}}
@keyframes elPulse{0%,100%{transform:scale(1);opacity:1}50%{transform:scale(1.06);opacity:.82}}
@keyframes elRotate{to{transform:rotate(360deg)}}
@keyframes elHue{0%{filter:hue-rotate(0)}100%{filter:hue-rotate(360deg)}}
.el-fadeIn.el-in{animation:elFadeIn .9s ease-out both}
.el-slideUp.el-in{animation:elSlideUp .9s cubic-bezier(.2,.8,.3,1) both}
.el-slideLeft.el-in{animation:elSlideLeft .9s cubic-bezier(.2,.8,.3,1) both}
.el-zoomIn.el-in{animation:elZoomIn .85s cubic-bezier(.2,.8,.3,1) both}
.el-float{animation:elFloat 3.4s ease-in-out infinite}
.el-pulse{animation:elPulse 2.6s ease-in-out infinite}
.el-rotateSlow{animation:elRotate 16s linear infinite}
.el-hueShift{animation:elHue 6s linear infinite}
.el-flow{animation:flowA 2.4s linear infinite}
"""

# ============================================================
#  组装输出
# ============================================================
toc_html = ''.join(
    f'<a href="#" onclick="event.preventDefault();document.getElementById(\'toc\').classList.remove(\'on\');show({i});"><b>{i+1}</b>{esc(p["nav"])}</a>'
    for i, p in enumerate(pages)
)
slides_html = ''.join(
    f'<section class="slide {p["cls"]}" data-i="{i}">{p["body"]}{review_bar(i+1)}</section>'
    for i, p in enumerate(pages)
)
CSS = CSS.replace('LOGO_B64', LOGO_B64) + REVIEW_CSS + ELEM_CSS + ELEM_ANIM_CSS

RV_PANEL = f"""<button class="rvToggle" onclick="rvToggleForce();document.getElementById('rvPanel').classList.toggle('on');rvRefresh();" title="审稿模式（快捷键 R）">✎ 审稿模式</button>
<aside class="rvPanel" id="rvPanel">
  <div class="rvHead">
    <h4>审稿工作台</h4>
    <div class="rvd">逐页确认内容 · 编辑自动保存 · 可导出确稿版与审稿报告</div>
    <div class="rvStat">
      <div class="s-ok"><b id="rvOk">0</b><span>已确认</span></div>
      <div class="s-fix"><b id="rvFix">0</b><span>需修改</span></div>
      <div><b id="rvPend">{len(pages)}</b><span>待确认</span></div>
      <div><b id="rvNotes">0</b><span>有备注</span></div>
      <div><b id="rvEdits">0</b><span>改动处</span></div>
    </div>
  </div>
  <div class="rvList" id="rvList"></div>
  <div class="rvFoot">
    <button class="pri" onclick="rvExportDeck()">⬇ 导出确稿 HTML</button>
    <button onclick="rvExportReport()">⬇ 审稿报告 MD</button>
    <button onclick="rvExportJSON()">⬇ 审稿数据 JSON</button>
    <button onclick="rvImportJSON()">⬆ 导入审稿数据</button>
    <button onclick="rvUndoPage()">↺ 撤销本页改动</button>
    <button class="warn" onclick="rvReset()">✕ 清空审稿状态</button>
    <button onclick="document.getElementById('rvPanel').classList.remove('on')">关闭</button>
  </div>
</aside>"""

HTML = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>宝锐诊断原料 · 客户宣讲（科技风动画版）· 41 页</title>
<style>{CSS}</style>
</head>
<body>
<div class="deck">
{slides_html}
<button class="navbtn prev" id="prevBtn" title="上一页 ←">‹</button>
<button class="navbtn next" id="nextBtn" title="下一页 →">›</button>
<div class="progress" id="prog" style="width:0"></div>
</div>
<div class="toc" id="toc"><h3>目录 · 共 {len(pages)} 页（按 N 开关 / Esc 关闭）</h3><div class="tgrid">{toc_html}</div></div>
{RV_PANEL}
{elem_toolbar()}
<div class="editHint" id="eh">✎ 全部文字可直接点击编辑</div>
<script>{JS}
document.addEventListener('click', function(e){{
  if(e.target.isContentEditable){{
    var h=document.getElementById('eh'); h.classList.add('on');
    clearTimeout(window.__eht); window.__eht=setTimeout(function(){{h.classList.remove('on')}},1600);
  }}
}});
</script>
<script>{ELEM_JS}</script>
<script>{REVIEW_JS}
/* 审稿初始化 */
document.addEventListener('DOMContentLoaded', function(){{
  var _n = elRestore(); if(_n) console.log('[elem] 已恢复 ' + _n + ' 页元素改动');
  rvBuildPanel();
  setTimeout(rvRefresh, 300);
  // 覆盖 show()：翻页后同步审稿状态
  var _show = window.show;
  window.show = function(i){{ _show(i); if(document.body.classList.contains('rv')) rvRefresh(); }};
}});
</script>
</body>
</html>
"""

open(OUT, 'w', encoding='utf-8').write(HTML)
print('✓ 生成完成：%s' % OUT)
print('   页数：%d 页 ｜ 大小：%.1f KB' % (len(pages), len(HTML.encode('utf-8'))/1024))
print('   动画：封面+承接页=%d 处（方案D）｜ 流程页=%d 处（方案A）｜ 技术页=%d 处（方案C）' % (
    sum(1 for p in pages if p['cls']=='stage'),
    sum(1 for p in pages if '<svg' in p['body']),
    sum(1 for p in pages if 'data-helix' in p['body'])))
print()
for i,p in enumerate(pages,1):
    print('  P%-3d %s' % (i, p['nav']))
