<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>__TITLE__</title>
<style>
  :root{
    --brand1:#6D5EFC; --brand2:#28C2C8; --brand3:#8C7BFF;
    --ink:#241F45; --ink2:#5B5776; --ink3:#8B87A6;
    --card:#FFFFFF; --line:#EBEBF5;
    --gold:#FFC53D; --gold-d:#C99A00;
    --green:#34C77B; --green-d:#1F9E5E;
    --coral:#FF6FA5; --coral-d:#D9457E;
    --shadow:0 10px 30px rgba(64,58,140,.10), 0 2px 6px rgba(64,58,140,.06);
  }
  *{box-sizing:border-box;-webkit-tap-highlight-color:transparent}
  html,body{margin:0;padding:0}
  body{font-family:"Microsoft YaHei","PingFang SC",system-ui,sans-serif;color:var(--ink);
    background:radial-gradient(1100px 520px at 12% -10%,#EDE9FF 0,rgba(237,233,255,0) 60%),
      radial-gradient(900px 500px at 100% 0,#E1F7F7 0,rgba(225,247,247,0) 55%),
      linear-gradient(180deg,#F8F9FF 0,#F3F6FB 100%);min-height:100vh}
  .app{max-width:860px;margin:0 auto;padding:22px 16px 40px}
  header.hero{background:linear-gradient(120deg,var(--brand1) 0,var(--brand3) 46%,var(--brand2) 100%);
    border-radius:26px;padding:20px 22px;color:#fff;position:relative;overflow:hidden;
    box-shadow:0 14px 36px rgba(109,94,252,.28)}
  header.hero::after{content:"";position:absolute;right:-40px;top:-60px;width:220px;height:220px;
    background:rgba(255,255,255,.14);border-radius:50%}
  .hero-row{display:flex;align-items:center;gap:14px;position:relative;z-index:1}
  .mascot{width:58px;height:58px;flex:0 0 58px;filter:drop-shadow(0 6px 14px rgba(0,0,0,.18))}
  .hero-txt h1{margin:0;font-size:21px;font-weight:700;letter-spacing:.5px}
  .hero-txt p{margin:4px 0 0;font-size:13px;opacity:.92}
  .tag{display:inline-block;margin-top:10px;font-size:12px;padding:5px 12px;border-radius:999px;
    background:rgba(255,255,255,.20);border:1px solid rgba(255,255,255,.34)}
  .steps{display:flex;align-items:center;gap:8px;margin:18px 2px 14px}
  .dot{flex:1;height:6px;border-radius:999px;background:#E4E5F2;transition:.35s}
  .dot.on{background:linear-gradient(90deg,var(--brand1),var(--brand2))}
  .stepnum{font-size:12px;color:var(--ink3);margin-left:6px;white-space:nowrap}
  .card{background:var(--card);border-radius:22px;box-shadow:var(--shadow);padding:22px;
    border:1px solid var(--line);animation:pop .32s ease both}
  @keyframes pop{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
  h2.q{margin:0 0 4px;font-size:17px}
  .sub{color:var(--ink2);font-size:13px;line-height:1.7;margin:6px 0 0}
  .kbd{background:#F0EEFF;color:var(--brand1);border-radius:6px;padding:1px 7px;font-size:12px}
  .barwrap{margin:18px 0 6px}
  .bar{display:flex;gap:8px;height:74px}
  .grid{display:grid;gap:6px}
  .cell{flex:1 1 0;border-radius:12px;border:3px solid #DADCF0;background:#F7F8FE;transition:.22s;
    display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--ink3);min-height:42px}
  .cell.f-gold{background:linear-gradient(180deg,#FFD977,#FFC53D);border-color:var(--gold-d);color:#7A5A00}
  .cell.f-blue{background:linear-gradient(180deg,#8FD6DA,#28C2C8);border-color:#1B9AA0;color:#04484C}
  .cell.hoverable{cursor:pointer}
  .cell.hoverable:hover{transform:translateY(-3px)}
  /* 格子多时（百格图/数位表/点阵）收紧尺寸，避免撑出巨长一屏 */
  .grid.dense{gap:4px}
  .grid.dense .cell{min-height:24px;border-width:2px;border-radius:6px;font-size:10px}
  .cols{display:flex;gap:12px;align-items:flex-end;height:130px;padding:0 6px}
  .col{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:6px}
  .colbar{border-radius:10px 10px 4px 4px;background:linear-gradient(180deg,#8FD6DA,#28C2C8);width:100%}
  .col .lab{font-size:12px;color:var(--ink2)}

  /* ---------- 新增视觉：几何图形 / 钟面 / 统计表 / 数轴 / 立体堆叠 ---------- */
  .itemgrid{display:flex;gap:14px;align-items:flex-end;justify-content:center;flex-wrap:wrap}
  .pickbox{display:flex;flex-direction:column;align-items:center;gap:6px;padding:8px 10px;
    border-radius:16px;border:2px solid transparent;transition:.2s}
  .pickbox .ilab{font-size:13px;color:var(--ink2);font-weight:600}
  .pickbox.pickable{cursor:pointer;background:#FBFBFE;border-color:#E9E8F6}
  .pickbox.pickable:hover{transform:translateY(-3px);border-color:var(--brand3)}
  .pickbox.sel{border-color:#1B9AA0;background:#EAF9FA}
  .shp{width:112px;height:94px;fill:#F4F3FF;stroke:#9A8CFF;stroke-width:2.4;stroke-linejoin:round}
  .shp.on{fill:#BFEFF1;stroke:#1B9AA0}
  .shp .ln{fill:none}
  .shp.on .ln{fill:none}
  .shp .dot{fill:#5A4BE0;stroke:none}
  .shp .hl{fill:#EAF9FA;stroke:none;opacity:.85}
  .shp .ang{fill:none;stroke:#D9721F;stroke-width:2.6}
  .shp .vt{fill:#3A3560;stroke:none;font-size:9px;font-weight:700}
  /* 文字卡：式子/说法/选项牌（无图形表征的知识点用，避免拿几何图形凑数） */
  .cardgrid{display:flex;gap:12px;align-items:stretch;justify-content:center;flex-wrap:wrap}
  .pcard{min-width:108px;min-height:76px;justify-content:center;background:#fff;border-color:#E9E8F6}
  .pcard .ctext{font-size:15px;font-weight:600;color:var(--ink);text-align:center;line-height:1.35}
  .pcard.on{background:#EAF9FA;border-color:#1B9AA0}
  .ck{width:106px;height:106px}
  .ck .face{fill:#FFFDF7;stroke:#5A4BE0;stroke-width:2.6}
  .ck.on .face{fill:#EAF9FA}
  .ck .tick{stroke:#C3BEE0;stroke-width:2}
  .ck .tick.maj{stroke:#5A4BE0;stroke-width:2.8}
  .ck .hh{stroke:#D9721F;stroke-width:4.4;stroke-linecap:round}
  .ck .mh{stroke:#2F6FE0;stroke-width:3.2;stroke-linecap:round}
  .ck .ctr{fill:#3A3560}
  .tblwrap{overflow-x:auto;-webkit-overflow-scrolling:touch}
  .ntbl{border-collapse:separate;border-spacing:0;width:100%;font-size:13px;min-width:260px}
  .ntbl th,.ntbl td{border:1px solid #DADCF0;padding:8px 10px;text-align:center;white-space:nowrap}
  .ntbl thead th{background:#F4F3FF;color:#3C3489;font-weight:600}
  .ntbl tbody th{background:#F8F8FE;color:#4A4470;font-weight:600}
  .ntbl td{background:#fff;color:var(--ink)}
  .ntbl td.on{background:#D8F5F6;color:#04484C;font-weight:700;box-shadow:inset 0 0 0 2px #28C2C8}
  .ntbl thead th.on{background:#CDEFFF}
  .nl{position:relative;display:flex;padding:8px 4px 4px;min-height:78px}
  .nlrail{position:absolute;left:16px;right:16px;top:14px;height:3px;border-radius:2px;
    background:linear-gradient(90deg,#8C7BFF,#28C2C8)}
  .nlcell{flex:1 1 0;display:flex;flex-direction:column;align-items:center;position:relative;z-index:1}
  .nldot{width:14px;height:14px;border-radius:50%;background:#fff;border:3px solid #C9C4F0;transition:.2s}
  .nldot.on{background:#28C2C8;border-color:#1B9AA0;transform:scale(1.14)}
  .nllab{margin-top:8px;font-size:12px;color:var(--ink2)}
  /* numberline 的非刻度点（如 √2≈1.414）用绝对定位图钉钉在轨上：
     .nlmarks 与 .nl 的内容盒等宽（左右各让掉 4px padding）⇒ left 百分比与刻度同坐标。*/
  .nlmarks{position:absolute;left:4px;right:4px;top:0;bottom:0;pointer-events:none;z-index:2}
  .nlmark{position:absolute;top:8px;transform:translateX(-50%);display:flex;flex-direction:column;align-items:center}
  .nlmarkdot{width:15px;height:15px;border-radius:50%;background:var(--gold);border:3px solid var(--gold-d);
    box-shadow:0 2px 6px rgba(201,154,0,.35)}
  .nlmarklab{margin-top:24px;font-size:11px;font-weight:700;color:#7A5A00;background:#FFF6DA;
    border:1px solid #F0DCA0;border-radius:7px;padding:1px 5px;white-space:nowrap}
  .stackwrap{display:flex;gap:16px;align-items:flex-end;justify-content:center;min-height:150px;padding:6px 0}
  .stackcol{display:flex;flex-direction:column;justify-content:flex-end;gap:3px;align-items:center;height:150px}
  .cube{width:44px;height:22px;border-radius:5px;border:2px solid #1B9AA0;
    background:linear-gradient(180deg,#D9F0F2,#8FD6DA)}
  .stackcol.on .cube{background:linear-gradient(180deg,#FFD977,#FFC53D);border-color:var(--gold-d)}
  .stacklab{margin-top:6px;font-size:12px;color:var(--ink2)}
  /* pick 模式：表格格 / 数轴刻度 / 堆叠列 也要能点选 */
  .pktd{cursor:pointer;transition:.18s}
  td.pktd:hover{background:#EAF9FA}
  td.pktd.sel{background:#CDEFFF;box-shadow:inset 0 0 0 3px #1B9AA0}
  .nlcell.pktd:hover .nldot{border-color:var(--brand3)}
  .nlcell.pktd.sel .nldot{background:#28C2C8;border-color:#1B9AA0;transform:scale(1.14)}
  .stackcol.pktd:hover .cube{filter:brightness(1.06)}
  .stackcol.pktd.sel .cube{background:linear-gradient(180deg,#D9F0F2,#3FC0C6);border-color:#1B9AA0}

  .caption{text-align:center;color:var(--ink2);font-size:13px;margin-top:12px;min-height:20px}
  .opts{display:grid;grid-template-columns:repeat(2,1fr);gap:12px;margin-top:18px}
  .opt{border:2px solid #E4E5F2;background:#FBFBFE;border-radius:16px;padding:16px;font-size:22px;
    font-weight:700;color:var(--ink);cursor:pointer;transition:.2s;text-align:center}
  .opt small{display:block;font-size:12px;font-weight:400;color:var(--ink3);margin-top:6px}
  .opt:hover{border-color:var(--brand3);transform:translateY(-2px);box-shadow:0 8px 18px rgba(109,94,252,.14)}
  .opt.ok{border-color:var(--green-d);background:#EFFBF4;color:var(--green-d)}
  .opt.no{border-color:var(--coral-d);background:#FFF1F6;color:var(--coral-d)}
  .opt[disabled]{pointer-events:none;opacity:.85}
  .actions{display:flex;gap:10px;margin-top:20px;flex-wrap:wrap}
  button.btn{border:0;border-radius:14px;padding:12px 20px;font-size:14px;font-weight:600;
    cursor:pointer;font-family:inherit;transition:.2s}
  .btn.primary{background:linear-gradient(120deg,var(--brand1),var(--brand2));color:#fff;
    box-shadow:0 8px 20px rgba(109,94,252,.26)}
  .btn.primary:hover{transform:translateY(-2px)}
  .btn.ghost{background:#F2F3FB;color:var(--ink2)}
  .btn:disabled{opacity:.5;cursor:not-allowed;transform:none}
  .fb{margin-top:16px;border-radius:16px;padding:14px 16px;font-size:14px;line-height:1.75;display:none}
  .fb.show{display:block;animation:pop .28s ease both}
  .fb.good{background:#EFFBF4;border-left:5px solid var(--green);color:#14603A}
  .fb.bad{background:#FFF1F6;border-left:5px solid var(--coral);color:#8A2E52}
  .sum{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px}
  .mini{background:#F8F8FE;border:1px solid var(--line);border-radius:16px;padding:16px}
  .mini h4{margin:0 0 6px;font-size:14px;color:var(--brand1)}
  .mini p{margin:0;font-size:13px;color:var(--ink2);line-height:1.7}
  .chip{display:inline-flex;align-items:center;gap:8px;margin-top:16px;font-size:13px;
    background:#F0EEFF;color:var(--brand1);border-radius:999px;padding:9px 16px;border:1px solid #E0DBFF}
  .chip i{width:8px;height:8px;border-radius:50%;background:var(--green);display:inline-block}
  .companion{display:flex;gap:12px;align-items:flex-start;margin-top:16px;background:#fff;
    border:1px solid var(--line);border-radius:22px;padding:14px 16px;box-shadow:var(--shadow)}
  .companion .av{width:40px;height:40px;flex:0 0 40px}
  .companion .say{font-size:13px;color:var(--ink2);line-height:1.75}
  .companion .say b{color:var(--ink)}
  .src{margin-top:10px;font-size:11px;color:var(--ink3)}
  @media (max-width:560px){
    .opts{grid-template-columns:1fr}.sum{grid-template-columns:1fr}.hero-txt h1{font-size:18px}
    .shp{width:88px;height:76px}.ck{width:86px;height:86px}.cube{width:34px;height:18px}
  }
</style>
</head>
<body>
<div class="app">
  <header class="hero">
    <div class="hero-row">
      <svg class="mascot" viewBox="0 0 100 100" aria-hidden="true"><defs><linearGradient id="mg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#FFFFFF"/><stop offset="1" stop-color="#E8FBFF"/></linearGradient></defs>
        <circle cx="50" cy="50" r="42" fill="url(#mg)"/><circle cx="36" cy="44" r="7" fill="#3A3560"/><circle cx="64" cy="44" r="7" fill="#3A3560"/>
        <circle cx="38.5" cy="41.5" r="2.4" fill="#fff"/><circle cx="66.5" cy="41.5" r="2.4" fill="#fff"/>
        <path d="M36 62 Q50 74 64 62" stroke="#3A3560" stroke-width="4.5" fill="none" stroke-linecap="round"/>
        <circle cx="26" cy="58" r="5" fill="#FFC0D6" opacity=".85"/><circle cx="74" cy="58" r="5" fill="#FFC0D6" opacity=".85"/></svg>
      <div class="hero-txt"><h1 id="h1"></h1><p id="hp"></p></div>
    </div>
    <span class="tag" id="tag"></span>
  </header>
  <div class="steps"><div class="dot on"></div><div class="dot"></div><div class="dot"></div><div class="dot"></div>
    <span class="stepnum" id="stepnum">第 1 / 4 步</span></div>
  <main id="stage"></main>
  <div class="companion">
    <svg class="av" viewBox="0 0 100 100" aria-hidden="true"><defs><linearGradient id="mg2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#6D5EFC"/><stop offset="1" stop-color="#28C2C8"/></linearGradient></defs>
      <circle cx="50" cy="50" r="40" fill="url(#mg2)"/><circle cx="37" cy="45" r="6.5" fill="#fff"/><circle cx="63" cy="45" r="6.5" fill="#fff"/>
      <circle cx="38.5" cy="43" r="2.2" fill="#3A3560"/><circle cx="64.5" cy="43" r="2.2" fill="#3A3560"/>
      <path d="M37 63 Q50 73 63 63" stroke="#fff" stroke-width="4.2" fill="none" stroke-linecap="round"/></svg>
    <div class="say" id="companion"></div>
  </div>
  <div class="src" id="src"></div>
</div>
<script>
var PACK = __PACK_JSON__;
(function(){
  var step=0, answered=false;
  var stage=document.getElementById('stage');
  var dots=[].slice.call(document.querySelectorAll('.dot'));
  var stepnum=document.getElementById('stepnum');
  var companion=document.getElementById('companion');
  var S=PACK.steps, SAY=PACK.say||['','','',''];

  document.getElementById('h1').textContent=PACK.name;
  document.getElementById('hp').textContent='小伴陪你玩着学 · 小学数学 · '+PACK.grade;
  document.getElementById('tag').textContent='互动课件 · '+PACK.band+' · 可判分';
  document.getElementById('src').textContent='知识点编号 '+PACK.n+' · 原创内容（课标级知识点，自建表述）';

  function setStep(n){step=n;dots.forEach(function(d,i){d.className='dot'+(i<=n?' on':'');});
    stepnum.textContent='第 '+(n+1)+' / 4 步';render();}
  function say(t){companion.innerHTML=t;}
  function rep(s,n){var o='';for(var i=0;i<n;i++)o+=s;return o;}
  function isArr(x){return Object.prototype.toString.call(x)==='[object Array]';}
  function esc(s){return String(s==null?'':s);}

  /* ============ 几何图形库（内联 SVG，离线可用） ============ */
  /* 圆内接四边形（两组对角形态共用底图）：圆心 (50,42)、半径 31，
     A(47.3,72.9) B(19.5,47.4) C(36.9,13.9) D(79.1,31.4) 四点**到圆心距离都是 31**
     ⇒ 它真的是圆内接四边形，对角互补是几何必然而非画上去的装饰
     （实测 ∠A 84.94° + ∠C 94.92° ≈ 180°，∠B 105.09° + ∠D 75.05° ≈ 180°）。*/
  var CYCLIC_QUAD='<circle class="ln" cx="50" cy="42" r="31"/>'
    +'<polygon points="47.3,72.9 19.5,47.4 36.9,13.9 79.1,31.4"/>'
    +'<circle class="dot" cx="47.3" cy="72.9" r="2.4"/>'
    +'<circle class="dot" cx="19.5" cy="47.4" r="2.4"/>'
    +'<circle class="dot" cx="36.9" cy="13.9" r="2.4"/>'
    +'<circle class="dot" cx="79.1" cy="31.4" r="2.4"/>'
    +'<text class="vt" x="47" y="81" text-anchor="middle">A</text>'
    +'<text class="vt" x="13" y="50" text-anchor="middle">B</text>'
    +'<text class="vt" x="37" y="9" text-anchor="middle">C</text>'
    +'<text class="vt" x="87" y="34" text-anchor="middle">D</text>';
  var CYCLIC_ANG_AC='<path class="ang" d="M40.7 66.8 A9 9 0 0 1 52.8 65.8"/>'
    +'<path class="ang" d="M45.2 17.3 A9 9 0 0 1 32.8 21.9"/>';
  var CYCLIC_ANG_BD='<path class="ang" d="M23.6 39.4 A9 9 0 0 1 26.1 53.5"/>'
    +'<path class="ang" d="M73.6 38.5 A9 9 0 0 1 70.8 28.0"/>';
  var SHAPES={
    '长方形':'<rect x="10" y="16" width="80" height="52" rx="3"/>',
    '正方形':'<rect x="22" y="14" width="56" height="56" rx="3"/>',
    '平行四边形':'<polygon points="26,68 46,16 90,16 70,68"/>',
    '梯形':'<polygon points="22,68 44,16 74,16 92,68"/>',
    '等腰梯形':'<polygon points="22,68 44,16 74,16 92,68"/>',
    '直角梯形':'<polygon points="22,68 22,16 70,16 92,68"/>',
    '三角形':'<polygon points="10,70 50,12 90,70"/>',
    '直角三角形':'<polygon points="18,70 18,14 88,70"/>',
    '等边三角形':'<polygon points="50,10 88,70 12,70"/>',
    '圆':'<circle cx="50" cy="42" r="30"/>',
    '半圆':'<path d="M20 62 A30 30 0 0 1 80 62 Z"/>',
    '扇形':'<path d="M50 64 L50 20 A44 44 0 0 1 84 56 Z"/>',
    '角':'<path class="ln" d="M50 72 L14 20 M50 72 L86 20"/><circle class="dot" cx="50" cy="72" r="3.6"/>',
    '直角':'<path class="ln" d="M50 74 L50 20 M50 74 L92 74"/><path class="ln" d="M50 60 L64 60 L64 74"/>',
    '长方体':'<polygon points="18,34 62,34 62,68 18,68"/><polygon points="18,34 32,20 76,20 62,34"/><polygon points="62,34 76,20 76,54 62,68"/>',
    '正方体':'<polygon points="20,38 58,38 58,72 20,72"/><polygon points="20,38 34,24 72,24 58,38"/><polygon points="58,38 72,24 72,58 58,72"/>',
    '圆柱':'<path d="M24 22 V62 A26 9 0 0 0 76 62 V22 Z"/><ellipse class="ln" cx="50" cy="22" rx="26" ry="9"/>',
    '圆锥':'<path d="M50 12 L24 60 A26 9 0 0 0 76 60 Z"/><ellipse class="ln" cx="50" cy="60" rx="26" ry="9"/>',
    '球':'<circle cx="50" cy="42" r="30"/><ellipse class="hl" cx="38" cy="30" rx="12" ry="7"/>',
    /* 圆内接四边形：三种形态共用同一组顶点坐标 ⇒ 切换时圆与四边形逐点不动，
       只有「标哪一对对角」在变（这是《圆内接四边形性质》656 的核心教学点）。*/
    '圆内接四边形':CYCLIC_QUAD,
    '圆内接四边形·对角一':CYCLIC_QUAD+CYCLIC_ANG_AC,
    '圆内接四边形·对角二':CYCLIC_QUAD+CYCLIC_ANG_BD
  };

  /* ============ 高亮策略 ============
     · intro：总是按 highlight 高亮
     · quiz ：方格类(bar/grid)保持旧行为；新视觉默认**不高亮**（防剧透），要亮得显式 reveal:true
     · hands：一律不高亮（由孩子自己选/涂）
     · show/reveal 为 true 时任何阶段都高亮
  */
  var NEWKINDS={shape:1,clock:1,table:1,numberline:1,stack:1,cards:1};
  function isNew(v){return !!(v&&NEWKINDS[v.kind]);}
  function hlOn(v,stage){
    if(!v) return false;
    if(v.show===true||v.reveal===true) return true;
    if(stage==='hands') return false;
    if(stage==='quiz') return !isNew(v);
    return true;
  }
  function hlSet(v,total){
    var h=v.highlight, s={}, i;
    if(h==null) return s;
    if(isArr(h)){ for(i=0;i<h.length;i++) s[h[i]]=1; }
    else { for(i=0;i<Math.min(h,total);i++) s[i]=1; }
    return s;
  }
  function tableHl(v){
    var h=v.highlight, s={}, i;
    if(h==null) return s;
    if(isArr(h)){
      if(h.length&&isArr(h[0])){ for(i=0;i<h.length;i++) s[h[i][0]+','+h[i][1]]=1; }
      else if(h.length>=2) s[h[0]+','+h[1]]=1;
    }
    return s;
  }

  /* numberline 专用高亮：highlight 数组按**数轴上的数值**解释（idx=(值-from)/step）。
     历史坑：旧代码复用 hlSet（按「第几格」索引解释），而作者写的是值 —— 尤其 from<0 的
     数轴上，写 2 想亮刻度 2，实际亮了刻度 from+2；写负值则一律落空。真机实测 671/691/
     716/503/612/534/599/943 等包全部亮错刻度，且 599/612 按下前后一格不变（零反馈）。
     规则：一律按值换算，落在格点上才亮；数字（非数组）分支与 hlSet 完全相同 ⇒ 「前 h 个」
     语义不变；无 step 或值不落在刻度上 ⇒ 丢弃（画不出来）。 */
  function nlSet(v,total,from,stp){
    var h=v.highlight, s={}, i, e, idx;
    if(h==null) return s;
    if(!isArr(h)){ for(i=0;i<Math.min(h,total);i++) s[i]=1; return s; }
    for(i=0;i<h.length;i++){
      e=h[i];
      if(typeof e!=='number'||!isFinite(e)) continue;
      if(!stp) continue;
      idx=(e-from)/stp;
      if(Math.abs(idx-Math.round(idx))<1e-9&&idx>=0&&idx<=total-1) s[Math.round(idx)]=1;
    }
    return s;
  }

  /* numberline 的「非刻度点」标记：highlight 里落在两个刻度之间的值（如 √2≈1.414）。
     历史坑：nlSet 只认落在刻度上的值 ⇒ 1.414 被静默丢弃，547《实数与数轴对应》
     按下按钮前后一格不变（零反馈）。而「无理数也能在数轴上找到对应点」恰恰是这一讲
     的**核心教学点**，画不出来等于自砍知识点，故补渲染能力而不是改例题。
     做法：按 u=(值-from)/step 算出**分数位置**，用绝对定位图钉钉在轨上（pct 与刻度
     同一坐标系）；标签如实写该值本身 —— 数据是 1.414 就标 1.414，不臆造「√2」。*/
  function nlNum(x){ return String(Math.round(x*1e6)/1e6); }
  function nlMarks(v,total,from,stp){
    var h=v.highlight, out=[], i, e, u;
    if(!isArr(h)||!stp||total<2) return out;
    for(i=0;i<h.length;i++){
      e=h[i];
      if(typeof e!=='number'||!isFinite(e)) continue;
      u=(e-from)/stp;
      if(Math.abs(u-Math.round(u))<1e-9) continue;   // 落在刻度上 ⇒ 交给 nlSet 点亮整格
      if(u<0||u>total-1) continue;                   // 越出 [from,to] ⇒ 画不出来，丢弃
      out.push({pct:(u+0.5)/total*100, t:nlNum(e)});
    }
    return out;
  }

  function clockSVG(t,on){
    var h=(t&&t.h)||0, m=(t&&t.m)||0, i;
    var aH=(((h%12)+m/60)/12)*360, aM=(m/60)*360;
    var ticks='';
    for(i=0;i<12;i++){
      var a=i*30-90, maj=(i%3===0), r1=maj?30:33, r2=36;
      ticks+='<line class="tick'+(maj?' maj':'')+'" x1="'+(50+r1*Math.cos(a*Math.PI/180)).toFixed(1)
           +'" y1="'+(50+r1*Math.sin(a*Math.PI/180)).toFixed(1)
           +'" x2="'+(50+r2*Math.cos(a*Math.PI/180)).toFixed(1)
           +'" y2="'+(50+r2*Math.sin(a*Math.PI/180)).toFixed(1)+'"/>';
    }
    function hand(deg,len,cls){
      var r=deg*Math.PI/180;
      return '<line class="'+cls+'" x1="50" y1="50" x2="'+(50+len*Math.sin(r)).toFixed(1)
           +'" y2="'+(50-len*Math.cos(r)).toFixed(1)+'"/>';
    }
    return '<svg class="ck'+(on?' on':'')+'" viewBox="0 0 100 100">'
      +'<circle class="face" cx="50" cy="50" r="40"/>'+ticks
      +hand(aH,20,'hh')+hand(aM,29,'mh')
      +'<circle class="ctr" cx="50" cy="50" r="3.4"/></svg>';
  }

  /* ============ 视觉渲染（highlight 渲染时内联，单次成型） ============ */
  function visual(v,stage){
    if(!v||!v.kind) return '';
    var on=hlOn(v,stage), i, s;
    if(v.kind==='bar'){
      var bcls=(stage==='quiz')?'f-blue':'f-gold';   // 与旧版一致：讲解金色、做题蓝色
      var hb=(on&&typeof v.highlight==='number')?v.highlight:0;
      var sb=(on&&isArr(v.highlight))?hlSet(v,v.cells):null;
      s='<div class="barwrap"><div class="bar" id="viz">';
      for(i=0;i<v.cells;i++){
        var fb=(sb? sb[i] : (i<hb));
        s+='<div class="'+('cell'+(fb?' '+bcls:'')).trim()+'"></div>';
      }
      return s+'</div></div>';
    }
    if(v.kind==='grid'){
      var tot=(v.rows||0)*(v.cols||0);
      var hg=(on&&typeof v.highlight==='number')?v.highlight:0;
      var sg=(on&&isArr(v.highlight))?hlSet(v,tot):null;
      s='<div class="barwrap"><div class="grid'+(tot>30?' dense':'')+'" id="viz" style="grid-template-columns:repeat('+v.cols+',1fr)">';
      for(i=0;i<tot;i++){
        var fg=(sg? sg[i] : (i<hg));
        s+='<div class="'+('cell'+(fg?' f-blue':'')).trim()+'"></div>';
      }
      return s+'</div></div>';
    }
    if(v.kind==='shape'){
      var its=v.items||[], ss=hlSet(v,its.length);
      s='<div class="barwrap"><div class="itemgrid" id="viz" data-kind="shape">';
      for(i=0;i<its.length;i++){
        var it=its[i]||{};
        var svg='<svg class="shp'+(on&&ss[i]?' on':'')+'" viewBox="0 0 100 84">'
              +(SHAPES[it.form]||'<rect x="10" y="16" width="80" height="52" rx="3"/>')+'</svg>';
        s+='<div class="pickbox" data-i="'+i+'">'+svg+(it.label?'<div class="ilab">'+esc(it.label)+'</div>':'')+'</div>';
      }
      return s+'</div></div>';
    }
    if(v.kind==='clock'){
      var ts=v.times||[], cs=hlSet(v,ts.length);
      s='<div class="barwrap"><div class="itemgrid" id="viz" data-kind="clock">';
      for(i=0;i<ts.length;i++){
        s+='<div class="pickbox" data-i="'+i+'">'+clockSVG(ts[i],on&&!!cs[i])
          +(ts[i]&&ts[i].label?'<div class="ilab">'+esc(ts[i].label)+'</div>':'')+'</div>';
      }
      return s+'</div></div>';
    }
    if(v.kind==='table'){
      var cols=v.cols||[], rows=v.rows||[], data=v.data||[], hs=on?tableHl(v):{};
      s='<div class="barwrap"><div class="tblwrap" id="viz" data-kind="table"><table class="ntbl"><thead><tr><th></th>';
      for(i=0;i<cols.length;i++) s+='<th>'+esc(cols[i])+'</th>';
      s+='</tr></thead><tbody>';
      for(var r=0;r<rows.length;r++){
        s+='<tr><th>'+esc(rows[r])+'</th>';
        for(var c=0;c<cols.length;c++){
          var dv=(data[r]&&data[r][c]!=null)?data[r][c]:'';
          var pick=(stage==='hands')?' class="pktd'+(hs[r+','+c]?' on':'')+'" data-i="'+(r*cols.length+c)+'"'
                  :(hs[r+','+c]?' class="on"':'');
          s+='<td'+pick+'>'+esc(dv)+'</td>';
        }
        s+='</tr>';
      }
      return s+'</tbody></table></div></div>';
    }
    if(v.kind==='numberline'){
      var from=(v.from==null?0:v.from), to=(v.to==null?10:v.to), stp=v.step||1;
      var n=Math.floor((to-from)/stp)+1, ns=nlSet(v,n,from,stp);
      var nm=(on&&stp&&n>1)?nlMarks(v,n,from,stp):[];
      s='<div class="barwrap"><div class="nl" id="viz" data-kind="numberline"><div class="nlrail"></div>';
      for(i=0;i<n;i++){
        s+='<div class="nlcell'+(stage==='hands'?' pktd':'')+'"'+(stage==='hands'?' data-i="'+i+'"':'')+'>'
          +'<div class="nldot'+(on&&ns[i]?' on':'')+'"></div>'
          +'<div class="nllab">'+esc(from+stp*i)+'</div></div>';
      }
      if(nm.length){
        s+='<div class="nlmarks">';
        for(i=0;i<nm.length;i++){
          s+='<div class="nlmark" style="left:'+nm[i].pct.toFixed(3)+'%">'
            +'<div class="nlmarkdot"></div>'
            +'<div class="nlmarklab">'+esc(nm[i].t)+'</div></div>';
        }
        s+='</div>';
      }
      return s+'</div></div>';
    }
    if(v.kind==='stack'){
      var hh=v.heights||[], ks=hlSet(v,hh.length);
      s='<div class="barwrap"><div class="stackwrap" id="viz" data-kind="stack">';
      for(i=0;i<hh.length;i++){
        s+='<div class="stackcol'+(on&&ks[i]?' on':'')+(stage==='hands'?' pktd':'')+'"'
          +(stage==='hands'?' data-i="'+i+'"':'')+'>'+rep('<div class="cube"></div>',hh[i])
          +'<div class="stacklab">'+esc(hh[i])+'</div></div>';
      }
      return s+'</div></div>';
    }
    if(v.kind==='cards'){
      var ci2=v.items||[], sc2=hlSet(v,ci2.length);
      s='<div class="barwrap"><div class="cardgrid" id="viz" data-kind="cards">';
      for(i=0;i<ci2.length;i++){
        var ci=ci2[i]||{};
        s+='<div class="pickbox pcard'+(on&&sc2[i]?' on':'')+'" data-i="'+i+'">'
          +'<div class="ctext">'+esc(ci.t)+'</div>'
          +((ci.label!=null&&ci.label!=='')?'<div class="ilab">'+esc(ci.label)+'</div>':'')+'</div>';
      }
      return s+'</div></div>';
    }
    // 兜底：一排占位格（未知 kind 不至于空白）
    return '<div class="barwrap"><div class="bar" id="viz">'+rep('<div class="cell"></div>',6)+'</div></div>';
  }

  function render(){
    say(SAY[step]||'');
    if(step===0) return stepIntro();
    if(step===1) return stepQuiz();
    if(step===2) return stepHands();
    return stepSum();
  }

  /* 讲解的「前后对比」：优先用 visual.after 做任意字段覆盖（如 stack 加一层、
     table 换数据）；没有 after 时退回只切 highlight（旧行为）。 */
  function introVariant(v,toggled){
    var w=JSON.parse(JSON.stringify(v)), k;
    delete w.after;
    if(!toggled) return w;                       // 回到初始态 = 原样
    var a=v.after||{}, has=false;
    /* has 的语义必须是「after 真的改变了渲染输入」：若值与原值一模一样（照抄），
       覆盖等于没覆盖 ⇒ 不能算 has —— 否则会顺手屏蔽掉下面那条唯一有效的 highlight2 回退
       （历史坑：多个包的按钮因此按下后图形零变化）。 */
    for(k in a){
      if(!Object.prototype.hasOwnProperty.call(a,k)) continue;
      if(JSON.stringify(a[k])!==JSON.stringify(w[k])) has=true;
      w[k]=a[k];
    }
    if(!has && w.highlight2!=null) w.highlight=w.highlight2;
    return w;
  }

  function stepIntro(){
    var v=S.intro.visual||{};
    stage.innerHTML='<div class="card"><h2 class="q">'+S.intro.heading+'</h2><p class="sub">'+S.intro.text+'</p>'
      +'<div id="vizwrap">'+visual(v,'intro')+'</div>'
      +'<div class="caption" id="cap">'+(v.caption||'')+'</div>'
      +'<div class="actions">'+(S.intro.action?'<button class="btn primary" id="act">'+S.intro.action+'</button>':'')
      +'<button class="btn ghost" id="next">做一道题试试</button></div></div>';
    var act=document.getElementById('act');
    if(act){
      var toggled=false;
      act.onclick=function(){
        toggled=!toggled;
        document.getElementById('vizwrap').innerHTML=visual(introVariant(v,toggled),'intro');
        document.getElementById('cap').innerHTML=toggled?(v.caption2||''):(v.caption||'');
      };
    }
    document.getElementById('next').onclick=function(){setStep(1);};
  }

  function stepQuiz(){
    answered=false;
    var q=S.quiz;
    stage.innerHTML='<div class="card"><h2 class="q">'+q.heading+'</h2><p class="sub">'+q.prompt+'</p>'
      +visual(q.visual||{},'quiz')
      +'<div class="opts">'+q.options.map(function(o){
        return '<div class="opt" data-v="'+o.v+'">'+o.v+(o.t?'<small>'+o.t+'</small>':'')+'</div>';}).join('')+'</div>'
      +'<div class="fb" id="fb"></div><div class="actions"><button class="btn primary" id="next" disabled>下一步</button></div></div>';
    var fb=document.getElementById('fb'), nx=document.getElementById('next');
    [].forEach.call(document.querySelectorAll('.opt'),function(el){
      el.onclick=function(){
        if(answered) return; answered=true;
        var pick=el.getAttribute('data-v'), right=(pick===q.answer);
        [].forEach.call(document.querySelectorAll('.opt'),function(o){
          o.setAttribute('disabled',''); if(o.getAttribute('data-v')===q.answer) o.classList.add('ok');});
        if(!right) el.classList.add('no');
        fb.className='fb show '+(right?'good':'bad');
        fb.innerHTML=(right?q.why_ok:(q.why_no||'').replace(/\{pick\}/g,pick));
        nx.disabled=false;
      };
    });
    nx.onclick=function(){setStep(2);};
  }

  function handsMode(h,v){
    if(h.mode==='pick'||h.mode==='count') return h.mode;
    return (v&&(v.kind==='bar'||v.kind==='grid'))?'count':'pick';
  }

  function stepHands(){
    var h=S.hands, v=h.visual||{};
    return handsMode(h,v)==='pick'? stepHandsPick(h,v) : stepHandsCount(h,v);
  }

  function stepHandsCount(h,v){
    stage.innerHTML='<div class="card"><h2 class="q">'+h.heading+'</h2><p class="sub">'+h.prompt+'</p>'
      +visual(v,'hands')+'<div class="caption" id="cap"></div><div class="fb" id="fb"></div>'
      +'<div class="actions"><button class="btn primary" id="check">检查一下</button>'
      +'<button class="btn ghost" id="next" disabled>下一步</button></div></div>';
    var viz=document.getElementById('viz'), cls=v.kind==='grid'?'f-blue':'f-gold';
    [].forEach.call(viz.children,function(c){
      c.classList.add('hoverable');
      c.onclick=function(){ if(c.classList.contains(cls)) c.className='cell hoverable';
        else c.className='cell hoverable '+cls; count(); };
    });
    function count(){var n=viz.querySelectorAll('.'+cls).length;
      document.getElementById('cap').innerHTML=(h.count||'已涂：')+'<b>'+n+'</b>'+(h.countTail||'');}
    count();
    var fb=document.getElementById('fb'), nx=document.getElementById('next');
    document.getElementById('check').onclick=function(){
      var n=viz.querySelectorAll('.'+cls).length, right=(n===h.target);
      fb.className='fb show '+(right?'good':'bad');
      fb.innerHTML=right?h.ok:(h.no||'').replace(/\{n\}/g,n);
      if(right) nx.disabled=false;
    };
    nx.onclick=function(){setStep(3);};
  }

  function stepHandsPick(h,v){
    stage.innerHTML='<div class="card"><h2 class="q">'+h.heading+'</h2><p class="sub">'+h.prompt+'</p>'
      +visual(v,'hands')+'<div class="caption" id="cap"></div><div class="fb" id="fb"></div>'
      +'<div class="actions"><button class="btn primary" id="check">检查一下</button>'
      +'<button class="btn ghost" id="next" disabled>下一步</button></div></div>';
    var want={}, sel={}, seq=[];
    if(isArr(h.answer)) h.answer.forEach(function(i){want[i]=1;});
    var boxes=[].slice.call(document.querySelectorAll('#viz .pickbox, #viz .pktd'));
    function refresh(){
      var n=0;
      boxes.forEach(function(b){
        var i=b.getAttribute('data-i');
        if(sel[i]){b.classList.add('sel');n++;} else b.classList.remove('sel');
      });
      document.getElementById('cap').innerHTML=(h.count||'已选：')+'<b>'+n+'</b>'+(h.countTail||'');
    }
    boxes.forEach(function(b){
      b.classList.add('pickable');
      b.onclick=function(){var i=b.getAttribute('data-i');
        if(sel[i]){ delete sel[i]; seq=seq.filter(function(x){return x!==i;}); }
        else { sel[i]=1; seq.push(i); }
        refresh();};
    });
    refresh();
    var fb=document.getElementById('fb'), nx=document.getElementById('next');
    document.getElementById('check').onclick=function(){
      /* ordered:true —— 判「点击顺序」，用于排队/排列类动手任务。
         实测缺口：不判顺序时，「按从短到长依次点选三张卡」这种题乱点也全对（#154）。 */
      var got, exp, right;
      if(h.ordered===true){
        got=seq.map(Number).join(','); exp=(h.answer||[]).map(Number).join(','); right=(got===exp);
      } else {
        got=Object.keys(sel).sort().join(','); exp=Object.keys(want).sort().join(','); right=(got===exp);
      }
      fb.className='fb show '+(right?'good':'bad');
      fb.innerHTML=right?h.ok:(h.no||'').replace(/\{n\}/g,Object.keys(sel).length);
      if(right) nx.disabled=false;
    };
    nx.onclick=function(){setStep(3);};
  }

  function stepSum(){
    stage.innerHTML='<div class="card"><h2 class="q">'+S.summary.heading+'</h2><p class="sub">'+S.summary.text+'</p>'
      +'<div class="sum">'+S.summary.cards.map(function(c){
        return '<div class="mini"><h4>'+c.h+'</h4><p>'+c.p+'</p></div>';}).join('')+'</div>'
      +'<div class="chip"><i></i>已记录到学情：'+S.summary.mastery+'（模拟 kb_mastery 回写）</div>'
      +'<div class="actions"><button class="btn primary" id="restart">再做一遍</button></div></div>';
    document.getElementById('restart').onclick=function(){setStep(0);};
  }

  render();
})();
</script>
</body>
</html>
