<!DOCTYPE html>
<html lang="zh-CN" data-page-node-id="49ioH2H82ADB0skaE43vmI"><head data-page-node-id="yqRDtpRG2epAQiUDprRomx">
<meta charset="UTF-8" data-page-node-id="VU7qPtRQeRxHgKwO589tpd">
<meta name="viewport" content="width=device-width, initial-scale=1.0" data-page-node-id="qlzfLyd6YDrMBPVblq422b">
<!-- PWA：手机/平板「添加到主屏幕」后以独立应用方式启动（桌面图标、全屏、无地址栏） -->
<link rel="manifest" href="./manifest.json">
<meta name="theme-color" content="#2f6bff">
<link rel="icon" type="image/png" sizes="192x192" href="./icons/icon-192.png">
<link rel="apple-touch-icon" href="./icons/icon-192.png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="木匠短线看板">
<title>木匠短线看板 1.0beta</title>
<style>#gateMask{position:fixed;inset:0;background:#f5f6f8;z-index:99999;display:flex;align-items:center;justify-content:center;font-family:'Microsoft YaHei',system-ui,sans-serif;color:#54607a;font-size:13px;letter-spacing:.5px}</style>
<div id="gateMask">正在验证访问权限…</div>
<script>
/* 兜底：无论门禁脚本后续发生什么异常（含标签被破坏/脚本未执行），
   8 秒后必须撤掉遮罩，绝不让用户永久卡在「正在验证访问权限」。 */
(function(){
  var t = setInterval(function(){
    var m = document.getElementById('gateMask');
    if(!m){ clearInterval(t); return; }          /* 门禁已自行处理 */
    if(m.dataset.gateOK === '1'){ clearInterval(t); return; }
    if(Date.now() - (+(m.dataset.gateT0||0)) > 8000){
      clearInterval(t);
      m.textContent = '正在加载看板…（访问校验未响应，可直接刷新重试）';
      m.style.color = '#98a0b3';
    }
  }, 400);
  var mk = document.getElementById('gateMask');
  if(mk){ mk.dataset.gateT0 = String(Date.now()); mk.dataset.gateOK = '0'; }
  window.__gateSettled = function(ok){
    var m = document.getElementById('gateMask');
    if(m){ m.dataset.gateOK = ok ? '1' : '0'; }
  };
})();
</script>
<script>
(function () {
  var mask = document.getElementById('gateMask');
  function pass() { if(window.__gateSettled) window.__gateSettled(1); if (mask && mask.parentNode) mask.parentNode.removeChild(mask); }

  var HASH = '';
  function deny(reason, showCode) {
    document.documentElement.innerHTML = '<head><meta charset="utf-8">'
      + '<meta name="viewport" content="width=device-width,initial-scale=1"></head><body style="margin:0;min-height:100vh;'
      + 'display:flex;align-items:center;justify-content:center;background:#f5f6f8;'
      + "font-family:'Microsoft YaHei',system-ui,sans-serif;color:#1a1f2e\">"
      + '<div style="text-align:center;padding:24px;max-width:440px">'
      + '<div style="font-size:40px;margin-bottom:12px">🔒</div>'
      + '<div style="font-size:17px;font-weight:700;margin-bottom:8px">该设备未授权</div>'
      + '<div style="font-size:13px;color:#7a8194;line-height:1.7">本看板仅限内部使用，不对外分享</div>'
      + (showCode && HASH ? '<div style="margin-top:16px;font-size:12px;color:#54607a">本机识别码（复制发给本人登记，一次即可）：</div>'
        + '<div id="gid" style="margin-top:6px;padding:9px 10px;background:#fff;border:1px solid #e5e8ef;'
        + 'border-radius:8px;font-size:11px;word-break:break-all;font-family:Consolas,monospace;cursor:pointer">' + HASH + '</div>'
        + '<button id="gbtn" style="margin-top:10px;padding:7px 18px;font-size:13px;border:none;border-radius:8px;'
        + 'background:#2f6bff;color:#fff;cursor:pointer">复制识别码</button>' : '')
      + (reason ? '<div style="margin-top:12px;font-size:11px;color:#a8acb5">' + reason + '</div>' : '')
      + '</div></body>';
    var g = document.getElementById('gid'), b = document.getElementById('gbtn');
    function copy() { try { navigator.clipboard.writeText(HASH); } catch (e) { } }
    if (g) g.onclick = copy;
    if (b) b.onclick = copy;
    throw new Error('DEVICE_NOT_AUTHORIZED');
  }

  /* 本机开发零摩擦 */
  if (location.protocol === 'file:' || /^(127\.0\.0\.1|localhost|0\.0\.0\.0|\[::1\])$/.test(location.hostname)) { pass(); return; }

  var host = location.hostname;

  /* ===== 通道 B：第三方静态托管（EdgeOne / Cloudflare Pages）→ 访问密钥（?k=）=====
     这些托管平台上没有 WorkBuddy 云数据库，所以走密钥校验。
     密钥由本人私下分发，未持有者打不开页面（源码可见级别，够内部小范围用）。 */
  var EDGE_KEY = 'a7F3k9Qm2XpR7wL4nB6';
  if (/(^|\.)edgeone\.(cool|app|site|dev)$/.test(host) || /(^|\.)pages\.dev$/.test(host)) {
    var m = location.search.match(/[?&]k=([^&#]+)/);
    var k = m ? decodeURIComponent(m[1]) : '';
    if (k !== EDGE_KEY) { deny('请向本人索取专属访问链接（含访问密钥）', false); }
    else { pass(); }
    return;
  }

  /* ===== 通道 A：WorkBuddy 云端 → 设备白名单 =====
     浏览器生成一枚 256 位随机 token 存 localStorage，取其 SHA-256，
     查 gate_devices 是否登记了该哈希：已登记 → 放行；未登记 → 展示识别码，
     本人把识别码登记进表后即可使用，之后永久免登。 */
  function loadSdk() {
    return new Promise(function (res, rej) {
      if (window.WorkBuddyCloud) return res();
      var s = document.createElement('script');
      s.src = 'https://cdn.jsdelivr.net/npm/@tencent-ai/workbuddy-cloud-sdk@dev/lib/index.global.js';
      s.onload = function () { res(); };
      s.onerror = function () { rej(new Error('SDK 加载失败')); };
      document.head.appendChild(s);
    });
  }

  loadSdk().then(function () {
    var token = null;
    try { token = localStorage.getItem('dash.gate.token'); } catch (e) { }
    if (!token) {
      var buf = new Uint8Array(32);
      (window.crypto || window.msCrypto).getRandomValues(buf);
      token = Array.prototype.map.call(buf, function (b) { return ('0' + b.toString(16)).slice(-2); }).join('');
      try { localStorage.setItem('dash.gate.token', token); } catch (e) { }
    }
    return crypto.subtle.digest('SHA-256', new TextEncoder().encode(token)).then(function (b) {
      var h = ''; new Uint8Array(b).forEach(function (x) { h += ('0' + x.toString(16)).slice(-2); });
      return h;
    }).then(function (hash) {
      HASH = hash;
      var cloud = WorkBuddyCloud.createWorkBuddyCloud({
        endpoint: 'https://auction-sentiment-board.app.workbuddy.host',
        oauthRelayBaseUrl: 'https://www.workbuddy.cn/v2/as/genie-baas/oauth',
        publishableKey: 'wbpk_xaBpPPb0luvGgxIS1z0uIw_LflAm54zgiF4wc27o7SOns0DZLiIWLxh'
      });
      return cloud.database.from('gate_devices').select('token_hash').eq('token_hash', hash).maybeSingle();
    });
  }).then(function (r) {
    if (r && r.data) pass(); else deny('', true);
  }).catch(function () {
    deny('验证服务暂时不可用，请检查网络后刷新重试', true);
  });
})();
</script>
<!-- ECharts：本地优先（离线可用），本地缺失时才回落 CDN -->
<script src="./vendor/echarts.min.js"></script>
<script>
  if (typeof echarts === 'undefined') {
    document.write('<script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.5.0/echarts.min.js"><\/script>');
  }
  if (typeof echarts === 'undefined') {
    document.write('<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"><\/script>');
  }
</script>
<script src="./data.js"></script>
<script src="./cycle.js"></script>
<script src="./skyladder.js"></script>
<script src="./alert.js"></script>
<!-- gbk-table.js：自包含中文码表，让 GBK 解码不依赖浏览器 TextDecoder；版本号用于破除缓存 -->
<script src="./vendor/gbk-table.js?v=20261015b"></script>
<script src="./import.js?v=20261015b"></script>
<script src="./market_calendar.js?v=20261015"></script>
<script src="./cycle_axis.js?v=20261015"></script>
<script src="./breadth.js?v=20261015"></script>
<script src="./hotrank.js?v=20261015"></script>
<script src="./newmod_data.js?v=20261015"></script>
<script src="./mcap_data.js?v=20261016"></script>
<script src="./earn_core.js?v=20261016"></script>
<!-- 复盘计划及交易预警 · 警示节点（节点定义＋判定见 nodes_core.js，数据由 _nodes.js 收盘生成） -->
<script src="./nodes_core.js?v=20261016"></script>
<!-- ===== 首屏瘦身：以下两个大文件改为按需异步加载 ======
     · ladderhist.js ≈ 931KB（连板梯队历史快照，只有「连板梯队」看板与周期主图用）
     · lunar.js     ≈ 409KB（农历/八字库，只有万年历用它）
     首屏不再下载，等用户点进对应看板时再 fetch，避免手机流量浪费与等待。 -->
<script>
window.__lazyData = { ladderhist: false, lunar: false };
function __ensureChunk(name, cb) {
  var M = { ladderhist: 'LADDERHIST', lunar: 'Lunar' };
  var ready = function () {
    if (name === 'ladderhist') return !!window.LADDERHIST;
    if (name === 'lunar') return !!(window.Lunar && window.Lunar.Gan);
    return true;
  };
  if (ready()) { cb && cb(); return; }
  if (window.__lazyData[name]) {
    // 正在加载：等一小会儿再验（配合轮询，够用且不引入额外依赖）
    var t = 0;
    (function w() { if (ready()) { cb && cb(); return; } if (t++ > 60) return; setTimeout(w, 50); })();
    return;
  }
  window.__lazyData[name] = true;
  var s = document.createElement('script');
  s.src = './' + name + '.js';
  s.onload = function () { cb && cb(); };
  s.onerror = function () { window.__lazyData[name] = false; };
  document.head.appendChild(s);
  /* 顺带让 Service Worker 预热（下次/离线可用） */
  try {
    var sw = navigator.serviceWorker && navigator.serviceWorker.controller;
    if (sw) sw.postMessage({ type: 'prefetch', urls: ['./' + name + '.js'] });
  } catch (e) { }
}
window.ensureChunk = __ensureChunk;

/* 需要大文件依赖的模块 → 渲染前自动确保依赖到位（DASH.register 内建包装会读这张表）。
   ladderhist.js ≈ 931KB，首屏不加载，点进相关看板时取一次即常驻。 */
window.__CHUNK_DEP = {
  ladder: 'ladderhist', skyladder: 'ladderhist', volume: 'ladderhist',
  cycle: 'ladderhist', alert: 'ladderhist', earn: 'ladderhist',
  newmod: 'ladderhist', breadth: 'ladderhist', hotrank: 'ladderhist',
  calendar: 'lunar'
};
</script>
<script src="./_cg_render.js?v=20261016"></script>
<script src="./nodes_data.js?v=20261016"></script>
<script src="./earn_data.js?v=20261016"></script>
<script src="./wuxing5.js?v=20261016c"></script>
<script src="./quotes_data.js?v=20261015"></script>
<style>
  /* ===== 字体清晰度优化 =====
     ① 微软雅黑原生字重只有 400/700，站内大量 font-weight:800/900 会触发字体回退/合成加粗，字形发虚发糊。
        注册本地字体别名 BoardUI：100–600 → Light/Regular，700–900 → Bold/半粗，全部落到真实字形，不再合成。
     ② 字体栈把 Microsoft YaHei UI（UI 版字形更清晰）提前，减少中西/符号混排时的字体回退。
     ③ 显式 subpixel-antialiased：Windows 上启用 ClearType 次像素渲染，小字号与细笔画明显更锐利。 */
  @font-face{font-family:'BoardUI';
    src:local('Microsoft YaHei UI Light'),local('Microsoft YaHei Light'),local('Microsoft YaHei UI'),local('Microsoft YaHei'),local('Segoe UI');
    font-weight:100 600;font-style:normal;font-display:swap}
  @font-face{font-family:'BoardUI';
    src:local('Microsoft YaHei UI Bold'),local('Microsoft YaHei-Bold'),local('Microsoft YaHei Bold'),local('Microsoft YaHei UI Semibold'),local('Microsoft YaHei Semibold');
    font-weight:700 900;font-style:normal;font-display:swap}
  :root{
    color-scheme: light;
    --bg:#f5f6f8; --card:#ffffff; --ink:#1a1f2e; --sub:#6b7280;
    --up:#d93a3a; --down:#1a9c6b; --line:#e5e8ef; --accent:#2f6bff;
  }
  html{-webkit-font-smoothing:subpixel-antialiased;text-rendering:optimizeLegibility;
    -webkit-text-size-adjust:100%;text-size-adjust:100%}
  html,body{color-scheme:light;background:var(--bg);color:var(--ink)}
  *{box-sizing:border-box;margin:0;padding:0}
  select,input,button,textarea,input[type=range]{color-scheme:light}
  body{font-family:'Apple Color Emoji','Segoe UI Emoji','BoardUI',-apple-system,"PingFang SC","Microsoft YaHei UI","Microsoft YaHei","Segoe UI",system-ui,"Segoe UI Symbol",sans-serif;background:var(--bg);color:var(--ink);font-size:14px}
  .wrap{max-width:1180px;margin:0 auto;padding:16px}
  header{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:8px;margin-bottom:14px}
  header h1{font-size:20px}
  .ver-badge{display:inline-block;margin-left:9px;padding:2px 9px;border-radius:999px;vertical-align:2px;
    background:#eef3ff;border:1px solid #d6e2ff;color:#2f6bff;font-size:11px;font-weight:700;letter-spacing:.3px}
  header .date{color:var(--sub);font-size:13px}
  .kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:12px;margin-bottom:16px}
  .kpi{background:var(--card);border-radius:12px;padding:14px 16px;border:1px solid var(--line)}
  .kpi .label{color:var(--sub);font-size:12px;margin-bottom:6px}
  .kpi .value{font-size:26px;font-weight:700}
  .kpi .value.up{color:var(--up)} .kpi .value.down{color:var(--down)} .kpi .value.flat{color:#555}
  .kpi .note{color:var(--sub);font-size:12px;margin-top:4px}
  .kpi.hero{background:linear-gradient(135deg,rgba(217,58,58,.06),rgba(26,156,107,.06));border-color:var(--line)}
  .kpi .value.v2{display:flex;align-items:baseline;gap:10px;font-size:30px;line-height:1.12;flex-wrap:wrap}
  .kpi .value.v2 .up{color:var(--up)} .kpi .value.v2 .down{color:var(--down)}
  .kpi .value.v2 .sep{color:var(--sub);font-weight:400;font-size:22px}
  .kpi .label .sub{color:var(--sub);font-size:11px;font-weight:400;margin-left:4px}
  .kpi .note b.up{color:var(--up)} .kpi .note b.down{color:var(--down)}
  /* ===== 周期猜想模块（中周期 M13 / 题材周期 M14）===== */
  .cg-wrap{overflow-x:auto;padding-bottom:4px}
  .cg-now{display:flex;align-items:center;gap:12px;flex-wrap:wrap;padding:9px 12px;margin-bottom:11px;
    background:linear-gradient(135deg,#f8f9fc,#fff);border:1px solid var(--line);border-radius:10px}
  .cg-now .cg-st{font-size:19px;font-weight:900;letter-spacing:.5px}
  .cg-now .cg-metric{font-size:11.5px;color:var(--sub)}
  .cg-now .cg-metric b{color:var(--ink);font-variant-numeric:tabular-nums}
  .cg-legend{display:flex;gap:13px;flex-wrap:wrap;margin-bottom:9px;font-size:11.5px;color:var(--sub)}
  .cg-legend span{display:inline-flex;align-items:center;gap:4px;white-space:nowrap}
  .cg-legend i{font-style:normal;font-size:14px;font-weight:900;line-height:1}
  .cg-st-回暖{color:#c98a1e} .cg-st-低迷{color:#8b93a6} .cg-st-高潮{color:#d93a3a}
  .cg-st-亏钱出现{color:#b06a1e} .cg-st-亏钱炸裂{color:#1a9c6b}
  .cg-th-启动{color:#8b93a6} .cg-th-确认{color:#2f6bff} .cg-th-发酵{color:#d93a3a}
  .cg-th-加速{color:#c026d3} .cg-th-分歧{color:#e08b1a} .cg-th-反包{color:#2e9e5b} .cg-th-龙回头{color:#8b5cf6}
  .cg-bar{display:flex;height:34px;border-radius:9px;overflow:hidden;border:1px solid var(--line);background:#fff;min-width:760px}
  .cg-seg{display:flex;align-items:center;justify-content:center;min-width:0;
    font-size:11px;font-weight:800;color:#fff;white-space:nowrap;overflow:hidden;
    border-right:1px solid rgba(255,255,255,.35);transition:filter .12s}
  .cg-seg:last-child{border-right:none}
  .cg-seg:hover{filter:brightness(1.1)}
  .cg-seg .cg-ds{overflow:hidden;text-overflow:ellipsis;padding:0 3px}
  .cg-seg.s0{background:#d9a441} .cg-seg.s1{background:#aeb6c6} .cg-seg.s2{background:#d93a3a}
  .cg-seg.s3{background:#c98a1e} .cg-seg.s4{background:#1a9c6b}
  .cg-seg.t0{background:#9aa3b4} .cg-seg.t1{background:#2f6bff} .cg-seg.t2{background:#d93a3a}
  .cg-seg.t3{background:#c026d3} .cg-seg.t4{background:#e08b1a} .cg-seg.t5{background:#2e9e5b} .cg-seg.t6{background:#8b5cf6}
  .cg-matrix{overflow-x:auto;margin-top:10px;border:1px solid var(--line);border-radius:9px;background:#fff}
  .cg-mrow{display:flex;align-items:center;border-bottom:1px dashed #f0f3f8;min-width:max-content}
  .cg-mrow:last-child{border-bottom:none}
  .cg-mrow .cg-lab{flex:0 0 92px;padding:6px 9px;font-size:11px;color:var(--sub);
    border-right:1px solid var(--line);position:sticky;left:0;background:#fbfcfe;z-index:1}
  .cg-mrow .cg-cells{padding:6px 8px;font-size:13px;letter-spacing:1.4px;line-height:1.2;white-space:nowrap;font-weight:700}
  .cg-cycles{margin-top:11px;display:grid;gap:7px}
  .cg-cyc{display:flex;align-items:center;gap:9px;flex-wrap:wrap;padding:7px 11px;background:#fbfcfe;
    border:1px solid var(--line);border-radius:9px;font-size:11.5px}
  .cg-cyc .cg-cno{font-weight:900;font-size:13px;color:var(--ink);min-width:56px}
  .cg-cyc .cg-rng{color:var(--sub);font-variant-numeric:tabular-nums}
  .cg-cyc .cg-seq{display:inline-flex;gap:5px;flex-wrap:wrap;align-items:center}
  .cg-cyc .cg-seq em{font-style:normal;font-size:11px;padding:1px 7px;border-radius:999px;color:#fff;font-weight:700}
  .cg-cyc.part{border-style:dashed;background:#fffdf6}
  .cg-desc{font-size:11px;color:var(--sub);line-height:1.65;margin-top:7px;padding:7px 10px;
    background:#fbfcfe;border-left:3px solid var(--accent);border-radius:0 7px 7px 0}
  .cg-note{font-size:11px;color:var(--sub);margin-top:9px;line-height:1.7}
  .cg-note b{color:var(--ink)}

  /* ADJS 模块：市场广度条（红涨 / 绿跌 / 灰平 三段比例条 + 情绪判定） */
  .breadth-strip{margin:2px 0 14px;padding:11px 13px;border:1px solid var(--line);border-radius:12px;
    background:linear-gradient(180deg,#fbfcfe,#f5f8fc)}
  .breadth-strip .bs-bar{display:flex;height:18px;border-radius:9px;overflow:hidden;background:#eef1f6;
    box-shadow:inset 0 1px 2px rgba(0,0,0,.07)}
  .breadth-strip .bs-up{background:linear-gradient(180deg,#ff6b6b,#d93a3a);transition:width .4s ease}
  .breadth-strip .bs-flat{background:#c3c9d6;transition:width .4s ease}
  .breadth-strip .bs-down{background:linear-gradient(180deg,#1fae7d,#1a9c6b);transition:width .4s ease}
  .breadth-strip .bs-legend{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:center;margin-top:9px;font-size:12px}
  .breadth-strip .bs-legend .up{color:var(--up);font-weight:700}
  .breadth-strip .bs-legend .down{color:var(--down);font-weight:700}
  .breadth-strip .bs-legend .flat{color:#8a91a3;font-weight:600}
  .breadth-strip .bs-legend .arr{font-size:10px;margin-right:1px}
  .breadth-strip .bs-verdict{margin-left:auto;font-weight:800;font-size:12.5px;padding:3px 13px;border-radius:999px;letter-spacing:.4px}
  .breadth-strip .bs-verdict.up{color:#a4161a;background:#fff2f2;border:1px solid #f4d6d6}
  .breadth-strip .bs-verdict.down{color:#0f7a52;background:#ecfaf3;border:1px solid #c6e9d8}
  .breadth-strip .bs-verdict.mid{color:#7a6400;background:#fff8e6;border:1px solid #f0e0b0}
  .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;margin-bottom:16px}
  .card h2{font-size:15px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center}
  .card h2 .hint{font-weight:400;color:var(--sub);font-size:12px}
  /* 次日预警·大字结论条：收盘后用前一日数据算「明天买什么」 */
  .al-plan{margin:0 0 14px}
  .al-plan .alp-top{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;
    border-left:4px solid var(--up);padding:7px 12px;margin-bottom:12px;
    background:linear-gradient(90deg,rgba(217,58,58,.07),rgba(217,58,58,0));border-radius:0 10px 10px 0}
  .al-plan .alp-t{font-size:17px;font-weight:800;letter-spacing:.3px}
  .al-plan .alp-s{font-size:12px;color:var(--sub)}
  .al-plan .alp-grid{display:grid;gap:12px}
  .al-plan .alp-m{border:1px solid var(--line);border-radius:12px;padding:12px 14px;
    background:linear-gradient(180deg,#fdfdff,#f7f9fe);box-shadow:0 1px 3px rgba(31,41,55,.05);
    border-left:4px solid var(--pm,#cfd6e3);transition:box-shadow .18s,transform .18s}
  .al-plan .alp-m:hover{box-shadow:0 4px 16px rgba(31,41,55,.09)}
  .al-plan .alp-m.hit{border-color:#f3cfcf;background:linear-gradient(180deg,#fff8f8,#fffdfd);border-left-color:var(--pm,#a4161a)}
  .al-plan .alp-m.none{opacity:.72}
  /* 四模式主题色：换手卡位=橙 / 最高标=红 / 天量弱转强=蓝 / 五虎=紫 */
  .al-plan .alp-m--card{--pm:#e08b1a}
  .al-plan .alp-m--top{--pm:#c0392b}
  .al-plan .alp-m--tianliang{--pm:#2f6bff}
  .al-plan .alp-m--five{--pm:#6b3fa0}
  .al-plan .alp-mt{display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;margin-bottom:8px}
  .al-plan .alp-mn{font-size:15px;font-weight:800}
  .al-plan .alp-mn::before{content:"";display:inline-block;width:8px;height:8px;border-radius:50%;
    background:var(--pm,#cfd6e3);margin-right:7px;vertical-align:1px}
  .al-plan .alp-tag{font-size:11px;font-weight:700;padding:1px 7px;border-radius:20px;background:#eef2ff;color:#3f51a8;border:1px solid #d6ddf7}
  .al-plan .alp-cnt{font-size:12px;font-weight:800;color:#a4161a;background:#fff2f2;
    border:1px solid #f4d6d6;border-radius:999px;padding:2px 10px}
  .al-plan .alp-cnt.zero{color:#7a8194;background:#f4f6fa;border-color:#e4e9f2;font-weight:700}
  .al-plan .alp-rows{display:flex;flex-direction:column;gap:8px}
  .al-plan .alp-row{display:flex;align-items:stretch;gap:12px;background:#fff;border:1px solid #e6eaf3;
    border-radius:11px;padding:9px 14px;min-height:62px;transition:border-color .15s,box-shadow .15s,transform .15s}
  .al-plan .alp-row:hover{border-color:#f0c3c3;box-shadow:0 3px 12px rgba(164,22,26,.10);transform:translateY(-1px)}
  .al-plan .alp-nmw{display:flex;flex-direction:column;justify-content:center;gap:4px;flex:0 0 160px;min-width:0}
  .al-plan .alp-tags{display:inline-flex;gap:5px;flex-wrap:wrap;align-items:center;max-width:100%;overflow:visible}
  .al-plan .alp-tags .al-eg{display:inline-flex;margin:0;align-items:center;flex-wrap:wrap;overflow:visible}
  .al-plan .alp-tags .m5-stars{flex-wrap:wrap;overflow:visible;max-width:none}
  .al-plan .alp-nm{font-size:22px;font-weight:900;letter-spacing:.5px;line-height:1.1;white-space:nowrap}
  .al-plan .alp-meta{display:flex;flex-direction:column;justify-content:center;gap:3px;flex:1 1 auto;min-width:0;
    font-size:11px;line-height:1.5}
  .al-plan .alp-meta-1{font-weight:700;color:#7a8194;white-space:normal;overflow:visible;word-break:break-word}
  .al-plan .alp-meta-2{color:var(--sub);white-space:normal;overflow:visible;word-break:break-word}
  .al-plan .alp-line{display:flex;align-items:center;justify-content:center;flex:0 1 auto;min-width:128px;max-width:200px;
    font-size:14.5px;font-weight:800;color:#a4161a;white-space:normal;padding:5px 10px;
    background:linear-gradient(135deg,#fff4f4,#ffecec);border:1px solid #f6d9d9;border-radius:9px;text-align:center}
  .al-plan .alp-note{font-size:11.5px;color:#a35b00;margin-top:6px}
  .al-plan .alp-how{font-size:12px;color:var(--sub);line-height:1.65;margin-top:8px;padding-top:7px;border-top:1px dashed #e3e8f2}
  .al-plan .alp-how b{color:#3c4a63}
  /* 复盘计划模块：分区样式统一在「交易预警」区块（.al-sec）定义，避免重复覆盖导致对齐错乱 */
  .al-summary{display:flex;flex-wrap:wrap;gap:8px;align-items:center;font-size:13px;color:var(--sub)}
  .al-chip{display:inline-flex;align-items:baseline;gap:6px;background:#fff;border:1px solid #e6eaf3;border-radius:999px;
    padding:5px 13px;font-size:12.5px;color:#54607a;white-space:nowrap}
  .al-chip i{font-style:normal;font-size:11px;color:#9aa2b1}
  .al-chip b{font-size:14px;font-weight:800;color:#232a3a;font-variant-numeric:tabular-nums}
  .al-chip.ok{border-color:#f3cfcf;background:#fff7f7} .al-chip.ok b{color:#a4161a}
  .al-chip.warn{border-color:#f0e0b0;background:#fffaf0} .al-chip.warn b{color:#a35b00}
  .al-chip.sub{background:#f6f8fc;border-color:#e8edf5;color:#7a8194;font-weight:500}
  .al-chip.sub b{color:#54607a;font-size:12.5px}
  /* 四模式总览表 */
  .ab-table{border-collapse:separate;border-spacing:0;width:100%;font-size:12.5px}
  .ab-table thead th{position:sticky;top:0;background:#f7f9fd;color:#6b7384;font-weight:700;
    padding:8px 9px;text-align:center;border-bottom:1.5px solid #e3e8f2;white-space:nowrap;z-index:1}
  .ab-table tbody td{padding:7px 9px;text-align:center;border-bottom:1px solid #f0f2f7;color:#3a4253}
  .ab-table tbody tr:hover td{background:#f8faff}
  .ab-table tbody tr.A td{background:rgba(217,58,58,.05)} .ab-table tbody tr.A:hover td{background:rgba(217,58,58,.09)}
  .ab-nm{text-align:left !important;font-weight:700;white-space:nowrap}
  .ab-nmw{display:inline-flex;align-items:center;gap:4px}
  .ab-cd{font-size:10.5px;color:#a8acb5;margin-left:5px;font-weight:400}
  .ab-concl{text-align:left !important;font-weight:700}
  .ab-dot{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;font-size:11px;font-weight:800}
  .ab-dot.ok{background:#fdeaea;color:#a4161a} .ab-dot.unk{background:#fff4e0;color:#a35b00} .ab-dot.no{background:#eef1f6;color:#9aa2b1}
  .ab-legend{display:flex;align-items:center;gap:12px;flex-wrap:wrap;font-size:11.5px;color:#7a8194;margin-bottom:7px}
  .ab-lg{cursor:default;padding:1px 8px;border-radius:6px;background:#f3f5fa;color:#54607a;font-weight:600}
  /* 结论卡左侧强调线已并入上方四模式主题色（--pm），此处不再重复覆盖 */
  /* 待更新提示（选中今天但竞价未抓时） */
  .al-pend{margin:0 0 10px;padding:9px 13px;background:#fff8e6;border:1px solid #f0e0b0;border-radius:10px;
    font-size:12.5px;color:#7a6400;line-height:1.6}
  /* 折叠参考面板 */
  .al-ref{margin:16px 0 4px;border:1px solid var(--line);border-radius:12px;background:#fbfcfe;overflow:hidden}
  .al-ref summary{cursor:pointer;list-style:none;padding:12px 15px;font-size:14px;font-weight:800;color:#232a3a;
    display:flex;align-items:center;gap:8px;user-select:none}
  .al-ref summary::-webkit-details-marker{display:none}
  .al-ref summary:hover{background:#f4f7fc}
  .al-ref summary::after{content:"▾";margin-left:auto;color:var(--sub);font-size:12px;transition:transform .2s}
  .al-ref:not([open]) summary::after{transform:rotate(-90deg)}
  .al-ref > div{padding:4px 15px 16px}
  .al-note{font-size:12px;color:var(--sub);line-height:1.7;margin-top:12px;padding-top:12px;border-top:1px dashed #e3e8f2}
  /* 四模式说明卡 */
  .al-modes{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:12px;margin-top:6px}
  .al-mode{border:1px solid var(--line);border-radius:11px;padding:12px 14px;background:#fff}
  .al-mode h4{margin:0 0 7px;font-size:13.5px;font-weight:800;color:#2a3346}
  .al-mode .al-desc{font-size:12px;color:#5b6478;line-height:1.7}
  .al-mode .al-desc b{color:#a4161a}
  /* ---------- 警示节点（小周期 / 趋势 / 双重 / 节气 / 江恩） ---------- */
  .al-node{display:inline-block;font-size:10px;font-weight:800;line-height:1.5;padding:1px 6px;border-radius:999px;
    margin-left:5px;white-space:nowrap;vertical-align:1px;border:1px solid transparent}
  .al-node.xiao{background:#fff4e2;color:#b06a00;border-color:#f0d3a2}
  .al-node.trend{background:#eaf0ff;color:#3f51a8;border-color:#cfd9f7}
  .al-node.dual{background:#f3ecff;color:#6b3fa0;border-color:#ddc9f5;box-shadow:inset 0 0 0 1px rgba(107,63,160,.06)}
  .al-node.jieqi{background:#eaf7ef;color:#1a7a4b;border-color:#c6e6d3}
  .al-node.gann{background:#fdf6e3;color:#8a6400;border-color:#ecd9a6}
  /* 面板外壳 */
  .al-nodes{margin:0 0 14px;border:1px solid #efe2c9;border-radius:14px;padding:13px 15px 15px;
    background:linear-gradient(180deg,#fffdf8,#fff8ec)}
  .al-nodes .aln-top{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:10px}
  .al-nodes .aln-t{font-size:16px;font-weight:800;color:#8a5a00;letter-spacing:.4px;white-space:nowrap}
  .al-nodes .aln-s{flex:1 1 340px;min-width:240px;font-size:11.5px;color:#9c8c72;line-height:1.7}
  /* 参考日五类判定 */
  .al-nodes .aln-chips{display:flex;gap:7px;flex-wrap:wrap;align-items:stretch}
  .al-nodes .aln-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:700;
    padding:5px 11px 5px 8px;border-radius:999px;background:#fff;border:1px solid #eceadf;color:#98a0ad;
    transition:box-shadow .15s,transform .15s}
  .al-nodes .aln-chip:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(138,90,0,.12)}
  .al-nodes .aln-chip b{font-weight:800}
  .al-nodes .aln-chip em{font-style:normal;font-weight:500;font-size:11px;color:#a9a294;
    border-left:1px solid #eceadf;padding-left:7px}
  .al-nodes .aln-ic{display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;
    border-radius:50%;font-size:10px;font-weight:800;font-style:normal;background:#f1f2f5;color:#a8acb5}
  .al-nodes .aln-chip.on{background:#fff7f6;border-color:#f2cdcd;color:#a4161a}
  .al-nodes .aln-chip.on .aln-ic{background:rgba(200,60,60,.14);color:#c0392b}
  .al-nodes .aln-chip.on.trend{background:#f5f8ff;border-color:#d3ddf7;color:#33509e}
  .al-nodes .aln-chip.on.trend .aln-ic{background:rgba(63,81,168,.14);color:#3f51a8}
  .al-nodes .aln-chip.on.dual{background:#f9f5ff;border-color:#ddc9f5;color:#6b3fa0}
  .al-nodes .aln-chip.on.dual .aln-ic{background:rgba(107,63,160,.14);color:#6b3fa0}
  .al-nodes .aln-chip.on.jieqi{background:#f2fbf6;border-color:#cbe9d8;color:#1a7a4b}
  .al-nodes .aln-chip.on.jieqi .aln-ic{background:rgba(26,122,75,.14);color:#1a7a4b}
  .al-nodes .aln-chip.on.gann{background:#fffcf0;border-color:#efe0b4;color:#8a6400}
  .al-nodes .aln-chip.on.gann .aln-ic{background:rgba(138,100,0,.14);color:#8a6400}
  /* 小节标题（虚线分隔） */
  .al-nodes .aln-sec{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;margin:14px 0 8px;
    padding-top:11px;border-top:1px dashed #f0e3cc}
  .al-nodes .aln-sec>b{font-size:12.5px;font-weight:800;color:#8a5a00;letter-spacing:.3px}
  .al-nodes .aln-sec>em{font-style:normal;font-size:11px;color:#a2937a}
  /* 最近一次节点：一条结论带 */
  .al-nodes .aln-hero{display:flex;align-items:center;gap:9px;flex-wrap:wrap;background:#fff;
    border:1px solid #f1e3c9;border-left:3px solid #d9a441;border-radius:10px;padding:8px 12px;margin-bottom:9px}
  .al-nodes .aln-hero .al-node{margin-left:0}
  .al-nodes .aln-hero-d{font-size:15px;font-weight:900;color:#7a4f00;letter-spacing:.3px;font-variant-numeric:tabular-nums}
  .al-nodes .aln-hero-w{font-size:11.5px;color:#8b7a5e}
  .al-nodes .aln-hero-c{margin-left:auto;font-size:11.5px;font-weight:700;color:#a4161a;
    background:#fff2f2;border:1px solid #f4d6d6;border-radius:999px;padding:2px 9px}
  /* 节点票卡：统一网格，行高一致 */
  .al-nodes .aln-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(198px,1fr));gap:7px}
  .al-nodes .aln-stk{display:flex;flex-direction:column;gap:4px;background:#fff;border:1px solid #f1e8d8;
    border-radius:10px;padding:7px 10px;transition:border-color .15s,box-shadow .15s}
  .al-nodes .aln-stk:hover{border-color:#e6cd9a;box-shadow:0 2px 10px rgba(138,90,0,.10)}
  .al-nodes .aln-stk.one{flex-direction:row;align-items:baseline;gap:6px;padding:6px 10px}
  .al-nodes .aln-l1{display:flex;align-items:baseline;flex-wrap:wrap}
  .al-nodes .aln-stk b{font-size:13.5px;font-weight:800;color:#1f2430}
  .al-nodes .aln-stk i{font-style:normal;font-size:10.5px;color:#9aa2b1;margin-left:5px}
  .al-nodes .aln-stk .al-node{margin-left:auto}
  .al-nodes .aln-gone{font-size:9.5px;font-weight:700;font-style:normal;color:#a8acb5;background:#f3f4f7;
    border-radius:999px;padding:1px 6px;white-space:nowrap}
  /* 接下来的时间窗 */
  .al-nodes .aln-ups{display:flex;gap:7px;flex-wrap:wrap}
  .al-nodes .aln-win{display:inline-flex;align-items:baseline;gap:5px;font-size:11.5px;color:#6b5a3c;
    background:#fff;border:1px solid #f0e7d6;border-radius:999px;padding:4px 12px}
  .al-nodes .aln-win b{font-size:12.5px;font-weight:800;color:#7a4f00;font-variant-numeric:tabular-nums}
  .al-nodes .aln-win.k-jieqi{background:#f7fdf9;border-color:#cfe8d9}
  .al-nodes .aln-win.k-jieqi b{color:#1a7a4b}
  .al-nodes .aln-win.k-gann{background:#fffdf5;border-color:#efe1bd}
  .al-nodes .aln-empty{font-size:12.5px;color:#a2937a}
  /* ---------- 数字能量小标签：挂在股票名称「下面」（节点标签仍留在名称旁边，两者并存） ----------
     迷你胶囊＝色点(五行) + 星名 + 五行字；完整「数对·星」读法在 title 里 */
  .al-eg{display:block;margin-top:3px}
  .al-eg .m5-stars{display:flex;flex-wrap:wrap;gap:3px;margin:0}
  .al-eg .m5-star.mini{display:inline-flex;align-items:center;gap:4px;margin:0;padding:0 7px;
    border:1px solid #eef0f4;border-radius:999px;background:#fbfcfe;
    font-size:10px;line-height:17px;font-weight:700;color:#4b556b;white-space:nowrap}
  .al-eg .m5-star.mini .m5-dot{width:6px;height:6px;margin:0;flex:0 0 auto}
  .al-eg .m5-star.mini .m5-star-wx{background:none;padding:0;margin:0 0 0 1px;font-size:9px;
    line-height:inherit;font-weight:800}
  .al-nmw{display:inline-flex;flex-direction:column;align-items:flex-start;gap:2px}
  .al-plan .alp-nmw .al-eg{margin-top:1px}
  .al-plan .alp-nmw .m5-star.mini{font-size:11px;line-height:19px;padding:0 9px}
  .ab-nmw{display:inline-block;white-space:nowrap}
  #adjsChart{width:100%;height:clamp(460px,60vh,660px)}   /* 双图（上 ADJS / 下 中位数），两格各自留足高度 */
  .h2right{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
  .zoom-tip{font-size:11px;color:#9aa2b5;font-weight:400}
  .zoom-btns{display:flex;gap:4px}
  .zoom-btns button{padding:3px 10px;font-size:12px;background:#eef1f7;color:var(--ink);border-radius:6px}
  .zoom-btns button.on{background:var(--accent);color:#fff}
  #ladderChart{width:100%;height:260px}
  .tag{display:inline-block;padding:2px 10px;border-radius:999px;font-size:12px;font-weight:600}
  .tag.hot{background:#fdeaea;color:var(--up)} .tag.warm{background:#fff3e0;color:#c26a00}
  .tag.cool{background:#e8f4ff;color:var(--accent)} .tag.cold{background:#e9f7f1;color:var(--down)}
  table{width:100%;border-collapse:collapse;font-size:13px}
  th,td{padding:8px 10px;border-bottom:1px solid var(--line);text-align:left;white-space:nowrap}
  th{color:var(--sub);font-weight:500;background:#fafbfd;position:sticky;top:0}
  td.num{font-variant-numeric:tabular-nums}
  .pos{color:var(--up)} .neg{color:var(--down)}
  .scroll{max-height:320px;overflow:auto;border:1px solid var(--line);border-radius:8px}
  .grid2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
  @media(max-width:900px){.grid2{grid-template-columns:1fr}}
  form{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px}
  form input,form select{padding:7px 10px;border:1px solid var(--line);border-radius:8px;font-size:13px;background:#fff;color:var(--ink)}
  form input{width:110px} form input.wide{width:200px}
  button{padding:7px 16px;border:none;border-radius:8px;background:var(--accent);color:#fff;font-size:13px;cursor:pointer}
  button.ghost{background:#eef1f7;color:var(--ink)}
  button.mini{padding:2px 8px;font-size:12px;background:#f1f2f6;color:#888}
  .stages{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:10px}
  .stage{flex:1;min-width:150px;border-radius:10px;padding:10px;border:1px solid var(--line)}
  .stage h3{font-size:13px;margin-bottom:6px}
  .stage ul{list-style:none;font-size:12px;color:var(--sub)}
  .stage li{display:flex;justify-content:space-between;padding:2px 0}
  .s0{background:#fff8ec;border-color:#f5d9a8} .s0 h3{color:#c26a00}
  .s1{background:#fdeaea;border-color:#f3c1c1} .s1 h3{color:var(--up)}
  .s2{background:#efe9ff;border-color:#d5c8f7} .s2 h3{color:#6a3fd6}
  .s3{background:#e9f7f1;border-color:#bfe4d2} .s3 h3{color:var(--down)}
  .rules label{display:flex;align-items:center;gap:8px;padding:7px 4px;border-bottom:1px dashed var(--line);cursor:pointer}
  .rules label.done span{text-decoration:line-through;color:var(--sub)}
  .auto-rule{background:#f2f6ff;border:1px solid #d6e2ff;border-radius:8px;padding:8px 12px;margin-bottom:10px;font-size:13px;color:#2a4a99}
  .muted{color:var(--sub);font-size:12px}
  .stats-row{display:flex;gap:18px;flex-wrap:wrap;margin-bottom:10px}
  .stats-row b{font-size:18px}
  /* ===== 主界面（万年历欢迎页）+ 三大按钮 + 场景看板 + 模块勾选器 ===== */
  #homeView{max-width:1100px;margin:0 auto}
  .home-hero{background:#fff;border:1px solid var(--line);border-left:4px solid var(--accent);
    border-radius:14px;padding:15px 20px;margin-bottom:16px;box-shadow:0 1px 3px rgba(31,41,55,.05);
    display:flex;align-items:center;gap:14px;flex-wrap:wrap}
  .home-hero .hh-t{font-size:19px;font-weight:800;color:var(--ink);letter-spacing:.3px}
  .home-hero .hh-s{font-size:12.5px;color:var(--sub)}
  .home-hero .hh-date{margin-left:auto;font-size:11.5px;color:#54607a;background:#f6f8fc;
    border:1px solid #e8edf5;border-radius:999px;padding:4px 13px;white-space:nowrap}
  .home-btns{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin:0 0 18px}
  @media(max-width:700px){.home-btns{grid-template-columns:1fr}}
  /* 守护状态条：本地部署时可一键拉起 dashboardd + watchdog */
  .daemon-bar{display:flex;align-items:center;gap:9px;padding:9px 14px;margin-bottom:14px;
    background:#fff;border:1px solid var(--line);border-radius:12px;font-size:12.5px;color:#54607a;
    box-shadow:0 1px 3px rgba(31,41,55,.05)}
  .daemon-bar .db-dot{width:8px;height:8px;border-radius:50%;background:#c3c9d6;flex:0 0 auto}
  .daemon-bar.on .db-dot{background:#1a9c6b;box-shadow:0 0 0 3px rgba(26,156,107,.16)}
  .daemon-bar.off .db-dot{background:#d93a3a;box-shadow:0 0 0 3px rgba(217,58,58,.16)}
  .daemon-bar .db-tx{flex:1 1 auto;min-width:0;line-height:1.5}
  .daemon-bar .db-btn{flex:0 0 auto;padding:6px 14px;font-size:12px;border-radius:8px;min-height:32px;
    background:var(--accent);border:1px solid var(--accent);color:#fff;cursor:pointer;font-weight:600}
  .daemon-bar .db-btn:hover{background:#245fe0}
  .daemon-bar .db-btn:disabled{opacity:.65;cursor:default}
  .home-btn{display:flex;align-items:center;gap:13px;padding:15px 17px;background:#fff;
    border:1px solid var(--line);border-radius:14px;cursor:pointer;transition:all .18s;
    box-shadow:0 1px 3px rgba(31,41,55,.05);text-align:left}
  .home-btn:hover{border-color:var(--accent);transform:translateY(-2px);box-shadow:0 8px 20px rgba(47,107,255,.13)}
  .home-btn .hb-ico{display:flex;align-items:center;justify-content:center;width:44px;height:44px;
    border-radius:12px;background:#f2f6ff;font-size:21px;flex:0 0 auto}
  .home-btn[data-scene="op"] .hb-ico{background:#fff3e9}
  .home-btn[data-scene="re"] .hb-ico{background:#eef3ff}
  .home-btn[data-scene="quick"] .hb-ico{background:#f4efff}
  .home-btn .hb-info{flex:1 1 auto;min-width:0}
  .home-btn .hb-t{display:block;font-size:16.5px;font-weight:800;color:var(--ink);letter-spacing:.3px}
  .home-btn .hb-s{display:block;font-size:11.5px;color:var(--sub);margin-top:2px}
  .home-btn .hb-n{flex:0 0 auto;font-size:11px;font-weight:700;color:var(--accent);background:#f0f5ff;
    border:1px solid #d6e2ff;border-radius:999px;padding:3px 11px}
  #boardView{max-width:1100px;margin:0 auto}
  .board-bar{display:flex;align-items:center;gap:12px;margin-bottom:16px;padding:12px 16px;background:#fff;
    border:1px solid var(--line);border-radius:12px;position:sticky;top:0;z-index:20}
  .board-bar #boardTitle{font-size:20px;font-weight:900}
  .board-bar #boardEdit{margin-left:auto}
  #boardBody{display:flex;flex-direction:column;gap:16px}
  #boardBody>[data-mod]{display:none}
  #boardBody>[data-mod].mod-on{display:block}
  #modPicker{position:fixed;inset:0;background:rgba(20,26,40,.5);display:flex;align-items:center;justify-content:center;z-index:1000;padding:20px}
  #modPicker[hidden]{display:none}
  .picker-box{background:#fff;border-radius:16px;width:min(700px,94vw);max-height:84vh;overflow:auto;padding:22px 24px}
  .picker-box h3{margin:0 0 4px;font-size:19px}
  .picker-box .pk-sub{font-size:12px;color:var(--sub);margin-bottom:14px}
  .pk-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;margin-bottom:16px}
  .pk-item{display:flex;align-items:center;gap:10px;padding:11px 13px;border:2px solid var(--line);
    border-radius:11px;cursor:pointer;font-size:14px;font-weight:600;transition:all .12s;user-select:none}
  .pk-item:hover{border-color:var(--accent)}
  .pk-item.on{border-color:var(--accent);background:#f4f8ff}
  .pk-item .pk-code{display:block;font-size:11px;color:var(--sub);font-weight:400;margin-top:1px}
  .pk-item input{accent-color:var(--accent);width:16px;height:16px;flex:0 0 auto}
  .pk-acts{display:flex;gap:10px;justify-content:flex-end}
  #tradeChart{width:100%;height:220px}
  footer{color:var(--sub);font-size:12px;text-align:center;padding:8px 0 20px}
  /* ===== 万年历 ===== */
  .m5-grid{display:grid;grid-template-columns:384px 1fr;gap:14px;align-items:stretch}
  @media(max-width:860px){.m5-grid{grid-template-columns:1fr}}
  .m5-right{display:flex;flex-direction:column;gap:14px}
  .m5-cal-col{display:flex;flex-direction:column;gap:9px}
  .m5-cal-col>.m5-panel{flex:1}
  .m5-right>.m5-gann{flex:1;display:flex;flex-direction:column}
  .m5-gann .m5-gann-wrap{flex:1;align-items:center}
  .m5-top{display:grid;grid-template-columns:1fr 1fr;gap:14px}
  @media(max-width:560px){.m5-top{grid-template-columns:1fr}}
  .m5-panel{position:relative;background:linear-gradient(180deg,#fdfdff 0%,#f7f9fd 100%);border:1px solid var(--line);border-radius:12px;padding:11px 12px 12px;overflow:hidden;box-shadow:0 1px 3px rgba(31,41,55,.05)}
  /* 游资悟道语录：填日历下方空白，贴底展示；面板改纵向 flex，日历内容在上 */
  .m5-cal-panel{display:flex;flex-direction:column}
  .m5-cal-panel #m5Cal{flex:0 0 auto}
  .m5-quote{margin-top:auto;padding:9px 11px 2px;border-top:1px dashed #dfe6f3;
    display:flex;gap:7px;align-items:flex-start;min-height:0}
  .m5-quote .m5-q-mark{font-size:20px;line-height:1;color:#b9c6de;font-weight:700;flex:0 0 auto;transform:translateY(2px)}
  .m5-quote .m5-q-body{flex:1;min-width:0}
  .m5-quote .m5-q-text{display:block;font-size:12.5px;line-height:1.6;color:#3c4a63;
    font-style:italic;letter-spacing:.2px;overflow-wrap:break-word}
  .m5-quote .m5-q-author{display:block;margin-top:3px;font-size:11px;color:#8a97ad;text-align:right;white-space:nowrap}
  .m5-quote .m5-q-author::before{content:'—— '}
  .m5-quote.swap{animation:m5qIn .45s ease}
  @keyframes m5qIn{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
.m5-panel::before{content:"";position:absolute;left:0;right:0;top:0;height:3px;background:linear-gradient(90deg,#2f6bff 0%,#7ba3ff 35%,#2e9e5b 68%,#d93a3a 100%);opacity:.30}
.m5-panel-t{display:flex;align-items:center;gap:7px;font-size:11.5px;font-weight:600;margin-bottom:8px}
  .m5-panel-t i{width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 3px rgba(47,107,255,.14);flex:0 0 auto}
  .m5-panel-t .m5-pt-sub{font-weight:400;color:var(--sub);font-size:11px;margin-left:auto}
  /* 日历 */
  .m5-cal-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}
  .m5-cal-title{font-size:14px;font-weight:700;letter-spacing:.3px}
  .m5-cal-title small{display:block;font-size:11px;color:var(--sub);font-weight:500;margin-top:1px}
  .m5-cal-nav{display:flex;gap:6px}
  .m5-cal-nav button{width:26px;height:26px;border:1px solid var(--line);background:#fff;border-radius:8px;cursor:pointer;color:var(--ink);font-size:13px;line-height:1;display:inline-flex;align-items:center;justify-content:center;transition:all .15s}
  .m5-cal-nav button:hover{border-color:var(--accent);color:var(--accent);background:#f1f5ff}
  .m5-cal-dow{display:grid;grid-template-columns:repeat(7,1fr);gap:5px;margin-bottom:4px}
  .m5-cal-dow span{text-align:center;font-size:11px;color:var(--sub);padding:3px 0}
  .m5-cal-dow span.wk0{color:#d93a3a;font-weight:600}
  .m5-cal-dow span.wk6{color:#e08a1e;font-weight:600}
  .m5-legend-cal{display:flex;flex-wrap:wrap;gap:10px;margin:0 0 7px}
  .m5-legend-cal span{display:inline-flex;align-items:center;gap:4px;font-size:10px;color:var(--sub)}
  .m5-legend-cal i{width:11px;height:11px;border-radius:3px;border:1px solid var(--line)}
  .m5-cal-grid{display:grid;grid-template-columns:repeat(7,1fr);gap:5px}
  .m5-cal-cell{position:relative;min-height:60px;background:#fff;border:1px solid var(--line);border-radius:9px;padding:5px 6px;display:flex;flex-direction:column;align-items:flex-start;line-height:1.35;transition:transform .12s,box-shadow .12s,border-color .12s}
  .m5-cal-cell:hover{transform:translateY(-1px);border-color:#b9cbff;box-shadow:0 3px 10px rgba(47,107,255,.14);z-index:1}
  .m5-cal-cell.m5-empty{background:transparent;border:none;min-height:0;padding:0}
  .m5-cal-cell b{font-size:14px;font-weight:600}
  .m5-cal-cell em{font-size:12px;color:var(--sub);font-style:normal}
  .m5-cal-cell.m5-hol{background:#fff8f1;border-color:#f3cfa9}
  .m5-cal-cell.m5-hol b{color:#c2560a}
  .m5-cal-cell.m5-legal{background:linear-gradient(180deg,#fff3f3,#ffe8e8);border-color:#f0b6b6}
  .m5-cal-cell.m5-legal b{color:#c0392b}
  .m5-cal-cell.m5-work{background:#f3f6fb;border-color:#c3d0e6;border-style:dashed}
  .m5-cal-cell.m5-work b{color:#54658a}
  .m5-cal-cell.m5-today{background:#eef4ff;border-color:var(--accent);box-shadow:0 0 0 2px rgba(47,107,255,.16)}
  .m5-cal-cell.m5-today b{color:var(--accent);font-weight:800}
  .m5-cal-cell.m5-today::after{content:"今";position:absolute;left:7px;bottom:5px;font-size:10px;color:var(--accent);font-weight:700}
  .m5-tag{position:absolute;right:5px;bottom:5px;font-size:10px;line-height:1.45;background:#fdecec;color:#c0392b;border-radius:4px;padding:0 3px;font-style:normal;font-weight:600}
  .m5-tag-legal{background:#d93a3a;color:#fff}
  .m5-tag-work{background:#e6ecf6;color:#4a5a75}
  /* 月度节假日摘要 */
  .m5-msum{margin-top:9px;font-size:12px;color:var(--sub);background:#fff;border:1px solid var(--line);border-radius:9px;padding:7px 10px;line-height:1.65}
  .m5-msum b{color:var(--ink)}
  .m5-msum .m5-src{display:block;font-size:10px;color:#a2a9b8;margin-top:2px}
  /* 实时时钟 */
.m5-clock{display:flex;align-items:center;justify-content:space-between;gap:10px;background:#fff;border:1px solid var(--line);border-radius:11px;padding:9px 13px;box-shadow:0 1px 3px rgba(31,41,55,.05)}
.m5-clock-time{font-size:22px;font-weight:800;letter-spacing:.5px;font-variant-numeric:tabular-nums;line-height:1;color:var(--ink)}
.m5-clock-time .m5-sec{color:var(--accent);font-size:16px}
  .m5-clock-date{font-size:11px;color:var(--sub);text-align:right;line-height:1.55}
  /* 八字月 / 当日五行 */
.m5-big{font-size:19px;font-weight:800;letter-spacing:.3px;line-height:1.2}
.m5-sub{font-size:11.5px;color:var(--sub);margin-top:5px;line-height:1.6}
  .m5-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}
  .m5-row:first-child{margin-top:0}
  .m5-pill{display:inline-flex;align-items:center;gap:5px;border:1px solid var(--line);border-radius:999px;padding:2px 10px;font-size:12px;background:#fff;color:var(--sub)}
  .m5-pill b{color:var(--ink);font-weight:700}
  .m5-wxchip{display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;border-radius:9px;color:#fff;font-size:16px;font-weight:800;box-shadow:0 1px 4px rgba(26,31,46,.12)}
  .m5-kv{display:flex;align-items:center;justify-content:space-between;font-size:11.5px;padding:5px 0;border-bottom:1px dashed var(--line)}
  .m5-kv:last-child{border-bottom:none}
  .m5-kv>span{color:var(--sub)}
  .m5-dot{display:inline-block;width:7px;height:7px;border-radius:50%;margin-right:5px;vertical-align:1px}
  /* 江恩九方图 */
  .m5-gann-wrap{display:flex;gap:16px;flex-wrap:wrap;align-items:center}
#m5Gann{flex:0 0 auto;max-width:100%;border:1px solid var(--line);border-radius:12px;background:linear-gradient(180deg,#fcfdff,#f4f7ff);box-shadow:0 1px 3px rgba(31,41,55,.05)}
#m5GannInfo{flex:1 1 210px;min-width:200px}
.m5-gann-num{font-size:28px;font-weight:800;line-height:1.05;letter-spacing:.3px}
  .m5-legend{display:flex;flex-wrap:wrap;gap:4px;margin-top:10px}
  .m5-legend span{display:inline-flex;align-items:center;gap:4px;font-size:11px;color:var(--sub);border:1px solid var(--line);border-radius:6px;padding:1px 6px;background:#fff}
  .m5-legend i{width:8px;height:8px;border-radius:2px}
  /* 模块二数字能量列复用 */
  .m5-star{display:inline-block;border:1px solid #ccc;border-radius:6px;padding:1px 6px;font-size:12px;margin:2px 3px 0 0}
  .m5-star.sm{font-size:11px;padding:0 4px;margin:1px 2px 0 0;line-height:1.5}
  /* 加强组合：由虚数 0/5 按相邻数补位派生，虚线框 + 淡底，与基础读法区分 */
  .m5-star.sm.aug{border-style:dashed;background:rgba(0,0,0,.018);opacity:.92}
  /* 标签内的五行属性：实心小色块（颜色＝该星五行） */
  .m5-star-wx{display:inline-block;font-style:normal;font-weight:700;font-size:9.5px;line-height:1.5;
    color:#fff;border-radius:3px;padding:0 3px;margin-left:3px;vertical-align:1px;letter-spacing:0}
  #ladderTable td .m5-stars{display:flex;flex-wrap:wrap;gap:2px;max-width:255px}
  .m5-stars{margin-top:2px}

  /* ===== 手动刷新按钮（全模块通用，触控友好） ===== */
  .rf-btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:40px;padding:9px 14px;
    border:1px solid var(--line);border-radius:10px;background:#fff;color:var(--ink);
    font-size:13px;font-weight:600;line-height:1;cursor:pointer;flex:0 0 auto;
    touch-action:manipulation;-webkit-tap-highlight-color:transparent;
    transition:border-color .15s,color .15s,background .15s,transform .1s}
  .rf-btn:hover{border-color:var(--accent);color:var(--accent);background:#f4f8ff}
  .rf-btn:active{transform:scale(.96)}
  .rf-btn .rf-ico{display:inline-block;font-size:15px;line-height:1;font-weight:700}
  .rf-btn.busy{opacity:.7;pointer-events:none}
  .rf-btn.busy .rf-ico{animation:rfspin .8s linear infinite}
  .rf-btn.ok{border-color:#bfe4d2;color:var(--down);background:#f2fbf6}
  @keyframes rfspin{to{transform:rotate(360deg)}}
  .rf-btn.rf-all{background:var(--accent);border-color:var(--accent);color:#fff}
  .rf-btn.rf-all:hover{background:#245fe0;color:#fff}
  header .hdr-right{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
  #dashToast{position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(14px);
    background:rgba(26,31,46,.93);color:#fff;font-size:13px;font-weight:500;
    padding:10px 18px;border-radius:999px;z-index:300;opacity:0;pointer-events:none;
    transition:opacity .25s,transform .25s;max-width:86vw;text-align:center;
    box-shadow:0 6px 20px rgba(0,0,0,.22)}
  #dashToast.show{opacity:1;transform:translateX(-50%) translateY(0)}

  /* ===== 自动运行引擎（Autopilot）：今日徽标 + 右下角状态胶囊 ===== */
  .today-badge{display:inline-flex;align-items:center;gap:7px;padding:4px 11px;border-radius:999px;
    background:#eef1f7;color:var(--ink);font-size:12.5px;font-weight:600;border:1px solid var(--line)}
  .today-badge .tb-d{font-variant-numeric:tabular-nums}
  .today-badge .tb-w{color:var(--sub);font-weight:500}
  .today-badge .tb-s{padding:1px 8px;border-radius:999px;font-size:11.5px;font-weight:700}
  .today-badge .tb-s.on{background:#eaf3ff;color:var(--accent)}
  .today-badge .tb-s.off{background:#f0f1f4;color:#8b93a6}
  .today-badge .tb-i{font-size:11px;line-height:1}
  .ap-pill{position:fixed;right:14px;bottom:14px;z-index:290;display:flex;align-items:center;gap:8px;
    background:rgba(255,255,255,.97);border:1px solid var(--line);border-radius:999px;
    padding:7px 10px;box-shadow:0 6px 18px rgba(20,30,60,.12);max-width:min(92vw,520px)}
  .ap-pill.off{opacity:.62;filter:grayscale(.4)}
  .ap-pill[hidden],.ap-panel[hidden]{display:none !important}
  .ap-pill .ap-tx{display:flex;flex-direction:column;line-height:1.25;font-size:12px;min-width:0}
  .ap-pill .ap-tx b{font-size:12.5px;color:var(--ink)}
  .ap-pill .ap-sub{color:var(--sub);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .ap-btn{border:1px solid var(--line);background:#f7f9fc;color:var(--ink);border-radius:8px;
    width:26px;height:26px;line-height:1;font-size:13px;cursor:pointer;display:inline-flex;
    align-items:center;justify-content:center;flex:0 0 auto}
  .ap-btn:hover{background:#eaf0fb;border-color:#c9d8f5}
  .ap-panel{position:fixed;right:14px;bottom:56px;z-index:291;width:300px;max-height:56vh;overflow:auto;
    background:#fff;border:1px solid var(--line);border-radius:12px;padding:12px 13px;
    box-shadow:0 10px 28px rgba(20,30,60,.16);font-size:12.5px}
  .ap-panel .ap-row{display:flex;justify-content:space-between;gap:10px;padding:4px 0;
    border-bottom:1px dotted #eceef3;color:var(--sub)}
  .ap-panel .ap-row b{color:var(--ink);font-weight:600}
  .ap-panel .ap-log{margin-top:8px;color:var(--sub);font-size:11.5px;line-height:1.7;
    max-height:22vh;overflow:auto;white-space:pre-wrap;word-break:break-all}
  @media(max-width:640px){ .ap-pill{right:8px;bottom:8px} .ap-panel{right:8px;width:min(92vw,300px)} }

  /* ===== 响应式：窄屏 / 触屏适配 ===== */
  .card h2{flex-wrap:wrap;gap:6px 10px}
  @media(max-width:640px){
    .wrap{padding:12px}
    .card{padding:13px;margin-bottom:13px}
    .card h2{font-size:14px}
    .card h2 .hint{font-size:11px}
    th,td{padding:6px 8px;font-size:12px}
    #adjsChart{height:clamp(340px,58vh,480px)}
    #ladderChart{height:300px !important}
    .kpis{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:9px}
    .kpi{padding:11px 12px}
    .kpi .value{font-size:20px}
    .kpi .value.v2{font-size:22px;gap:7px}
    .kpi .value.v2 .sep{font-size:17px}
    .m5-clock-time{font-size:20px}
    .m5-clock-time .m5-sec{font-size:14px}
    .m5-big{font-size:17px}
    .m5-gann-num{font-size:24px}
    .m5-wxchip{width:26px;height:26px;font-size:14px;border-radius:8px}
    .scroll{max-height:280px}
    /* 主界面（万年历欢迎页 + 三按钮） */
    .home-hero{padding:12px 14px;border-radius:12px;gap:10px}
    .home-hero .hh-t{font-size:16px}
    .home-hero .hh-s{font-size:11.5px}
    .home-hero .hh-date{font-size:11px;padding:3px 10px}
    .home-btns{gap:10px}
    .home-btn{padding:13px 14px;gap:11px}
    .home-btn .hb-ico{width:38px;height:38px;font-size:18px;border-radius:10px}
    .home-btn .hb-t{font-size:15px}
    .home-btn .hb-s{font-size:11px}
    .home-btn .hb-n{font-size:10px;padding:2px 9px}
    /* 看板标题栏 */
    .board-bar{padding:10px 12px;gap:9px}
    .board-bar #boardTitle{font-size:17px}
    /* 复盘出手名单：窄屏下股票行换行堆叠，不横向溢出 */
    .al-plan .alp-row{flex-wrap:wrap;gap:7px}
    .al-plan .alp-nmw{flex:0 0 124px}
    .al-plan .alp-nm{font-size:19px}
    .al-plan .alp-line{min-width:108px;font-size:13px;padding:4px 8px}
    /* 勾选弹层单列 */
    .pk-grid{grid-template-columns:1fr}
  }
  @media(max-width:560px){
    .m5-cal-cell{min-height:56px;padding:5px 6px;border-radius:9px}
    .m5-cal-cell b{font-size:14px}
    .m5-cal-cell em{font-size:11px}
    .m5-tag{font-size:9px;right:4px;bottom:4px}
    .m5-cal-cell.m5-today::after{font-size:9px;left:6px;bottom:4px}
    .m5-cal-dow span{font-size:10px}
    .m5-legend-cal{gap:6px}
    .m5-legend-cal span{font-size:9px}
  }
  /* 触屏设备：所有可点击元素命中区 ≥44px */
  @media(pointer:coarse){
    .rf-btn{min-height:44px;padding:11px 15px}
    .zoom-btns button{min-height:36px;padding:6px 13px}
    .m5-cal-nav button{width:36px;height:36px}
    button.mini{min-height:32px;padding:5px 11px}
    .cycfilters button,.cyc-matrix-toolbar button{min-height:34px;padding:6px 12px}
    form input,form select{min-height:40px}
    .m5-cal-nav button{font-size:15px}
  }
  /* 手机端：卡片副标题单行省略 + 隐藏鼠标操作提示，标题不再占 4 行 */
  @media(max-width:640px){
    .card h2 .hint{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;flex-basis:100%}
    .card h2 .h2right{gap:6px}
    .zoom-tip{display:none}
    .hdr-right .date{font-size:12px}
  }
</style>
<style id="__dm_no_drag_style__">* { -webkit-user-drag: none !important; }</style><style id="__dm_bindable_badge_style__">[data-sp-bindable="database"]{outline:1px dashed #0088ff;outline-offset:2px;border-radius:4px;}[data-sp-bindable="database"] *{pointer-events:none !important;transition:none !important;animation:none !important;}[data-sp-bindable="database"]{transition:none !important;animation:none !important;}</style></head>
<body data-page-node-id="AxeFDhuzNRhmjUwtE4iUaB" style="">
<div id="dashToast"></div>
<!-- 自动运行引擎：0 点日切 / 数据守护 / 卡死自愈 -->
<div class="ap-pill" id="apPill" hidden>
  <button type="button" class="ap-btn" id="apToggle" title="暂停 / 开启自动运行">⏸</button>
  <span class="ap-tx" id="apTx"></span>
  <button type="button" class="ap-btn" id="apNow" title="立刻检查并刷新">↻</button>
  <button type="button" class="ap-btn" id="apMore" title="展开运行详情">⋯</button>
</div>
<div class="ap-panel" id="apPanel" hidden></div>
<div class="wrap" data-page-node-id="n55Km4b35eYi5q6WK1nQfW">
  <header data-page-node-id="6Egk0UU0QOpr88UxAkED8K">
    <h1 data-page-node-id="6z3h53XYGPV7W5IkugfluL">📈 木匠短线看板<span class="ver-badge" id="verBadge">1.0beta</span></h1>
    <div class="hdr-right">
      <div class="date" id="updateDate" data-page-node-id="xxdNswObPfRPDyXCLomncA">数据更新于 2026-10-15T02:45:00+08:00</div>
      <span class="today-badge" id="todayBadge" title="今天"></span>
      <button type="button" class="rf-btn" data-rf="kpi" title="刷新情绪概览 KPI"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      <button type="button" class="rf-btn rf-all" data-rf="__all" title="重新拉取 data.js / cycle.js 并重绘全部模块"><span class="rf-ico">↻</span><span class="rf-tx">全部刷新</span></button>
      <a href="./mobile.html" class="rf-btn" style="text-decoration:none" title="手机端极简竞价播报（含三模式预警）"><span class="rf-ico">📱</span><span class="rf-tx">手机播报</span></a>
    </div>
  </header>

  <!-- ===== 主界面：万年历欢迎页 ===== -->
  <section id="homeView">
    <div class="home-hero">
      <div class="hh-t" id="hhTitle">你好，交易者</div>
      <div class="hh-s">万年历 · 从下面选一个看板，开始今天的盯盘与复盘</div>
      <div class="hh-date" id="hhDate"></div>
    </div>
    <div class="daemon-bar" id="daemonBar">
      <span class="db-dot"></span>
      <span class="db-tx">检测本地守护…</span>
      <button type="button" class="db-btn" id="dbStart" hidden>启动守护</button>
    </div>
    <div class="home-btns">
      <button type="button" class="home-btn" data-scene="op">
        <span class="hb-ico">🎯</span>
        <span class="hb-info"><span class="hb-t">操作面板</span><span class="hb-s">盯盘决策 · 情绪 · 梯队 · 天量 · 计划</span></span>
        <span class="hb-n" data-n="op"></span>
      </button>
      <button type="button" class="home-btn" data-scene="re">
        <span class="hb-ico">📊</span>
        <span class="hb-info"><span class="hb-t">复盘面板</span><span class="hb-s">复盘研究 · 天梯 · 题材 · 涨幅 · 人气</span></span>
        <span class="hb-n" data-n="re"></span>
      </button>
      <button type="button" class="home-btn" data-scene="quick">
        <span class="hb-ico">🗺</span>
        <span class="hb-info"><span class="hb-t">全景预览</span><span class="hb-s">全部模块 · 一页总览</span></span>
        <span class="hb-n" data-n="quick"></span>
      </button>
    </div>
    <div id="homeM01"></div>
  </section>

  <!-- ===== 场景看板视图（默认隐藏） ===== -->
  <section id="boardView" hidden>
    <div class="board-bar">
      <button type="button" class="rf-btn" id="boardBack">← 主界面</button>
      <span id="boardTitle">看板</span>
      <button type="button" class="rf-btn" id="boardEdit">⚙ 选模块</button>
    </div>
    <div id="boardBody"></div>
  </section>

  <!-- ===== 模块勾选弹层 ===== -->
  <div id="modPicker" hidden>
    <div class="picker-box">
      <h3 id="pkTitle">选择模块</h3>
      <div class="pk-sub">勾选后点「生成看板」，所选模块会单独组成一个看板</div>
      <div class="pk-grid" id="pkGrid"></div>
      <div class="pk-acts">
        <button type="button" class="ghost" id="pkAll">全选</button>
        <button type="button" class="ghost" id="pkNone">清空</button>
        <button type="button" id="pkSave">生成看板</button>
      </div>
    </div>
  </div>

  <!-- ============ 万年历 ============ -->
  <div class="card" data-mod="M01" data-page-node-id="m5card">
    <h2>🌗 万年历
      <div class="h2right">
        <span class="hint">农历 · 中国节假日 · 八字月 · 江恩九方图 · 当日五行</span>
        <button type="button" class="rf-btn" data-rf="calendar" title="刷新万年历（日历回到今天、江恩图重绘）"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div class="m5-grid">
      <div class="m5-cal-col">
        <div class="m5-panel m5-cal-panel">
          <div class="m5-panel-t"><i></i>农历日历<span class="m5-pt-sub">公历 + 农历 + 节假日</span></div>
          <div id="m5Cal"></div>
          <div id="m5Quote" class="m5-quote" title="每次刷新换一条 · 洗牌袋轮换不重复">
            <span class="m5-q-mark">「</span>
            <span class="m5-q-body"><span class="m5-q-text" id="m5QuoteText"></span><span class="m5-q-author" id="m5QuoteAuthor"></span></span>
          </div>
        </div>
        <div id="m5Clock" class="m5-clock"></div>
      </div>
      <div class="m5-right">
        <div class="m5-top">
          <div class="m5-panel">
            <div class="m5-panel-t"><i style="background:#c08a3e;box-shadow:0 0 0 3px rgba(192,138,62,.14)"></i>八字月（地支月）</div>
            <div id="m5Bazi"></div>
          </div>
          <div class="m5-panel">
            <div class="m5-panel-t"><i style="background:#2f6bff;box-shadow:0 0 0 3px rgba(47,107,255,.14)"></i>当日五行</div>
            <div id="m5Day"></div>
          </div>
        </div>
        <div class="m5-panel m5-gann">
          <div class="m5-panel-t"><i style="background:#2e9e5b;box-shadow:0 0 0 3px rgba(46,158,91,.14)"></i>江恩九方图 · 运行位置<span class="m5-pt-sub">以上证近3年高点为起点</span></div>
          <div class="m5-gann-wrap">
            <canvas id="m5Gann" width="440" height="440"></canvas>
            <div id="m5GannInfo"></div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- KPI -->
  <div class="kpis" id="kpis" data-mod="M02" data-page-node-id="fwcBzjAxIyFfOFTBe10IE4">
    <div class="kpi"><div class="label">最新交易日</div><div class="value" style="font-size:18px">2026-10-14</div><div class="note">情绪阶段：混沌/分歧</div></div>
    <div class="kpi"><div class="label">竞价涨幅中位数(%)</div><div class="value up">0.66</div><div class="note">昨日涨停股今开 vs 昨收</div></div>
    <div class="kpi"><div class="label">总体标准差(%)</div><div class="value">5.05</div><div class="note">分歧度：越大越分化</div></div>
    <div class="kpi"><div class="label">近20日中位数均值(%)</div><div class="value up">0.89</div><div class="note">样本 20 个交易日</div></div></div>

  <!-- 市场情绪双图：上图 竞价中位数/总体标准差，中带 ADJS 涨跌家数柱（+涨停家数线），时间轴在最底
       —— 三段共用同一条时间轴，拖动/滚轮/按钮缩放完全同步 -->
  <div class="card" id="breadthCard" data-mod="M04">
    <h2>市场情绪双图 · 上：竞价中位数 / 总体标准差　下带：ADJS 涨跌家数
      <div class="h2right">
        <span class="hint" id="breadthHint">数据来源：ADJS＝东方财富/新浪全A（15:20 自动抓取）· 中位数＝通达信（交易日 9:25 自动更新）</span>
        <span class="zoom-tip">🖐 上下两图同拖同缩 / 滚轮缩放</span>
        <div class="zoom-btns" id="zoomBtns">
          <button type="button" data-w="0">全部</button>
          <button type="button" data-w="60">近60日</button>
          <button type="button" data-w="20" class="on">近20日</button>
          <button type="button" data-w="10">近10日</button>
        </div>
        <button type="button" class="rf-btn" data-rf="breadth" title="重新拉取 breadth.js / data.js 并重绘双图"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div class="kpis" id="breadthKpis"></div>
    <div id="breadthStrip" class="breadth-strip"></div>
    <div id="adjsChart" style="user-select:none;-webkit-tap-highlight-color:rgba(0,0,0,0)"></div>
    <div class="cyc-toolbar-row" style="margin:6px 0 0">
      <span>上图 中位数＝昨日涨停股今开 vs 昨收涨幅的中位数，灰色虚线为总体标准差（分歧度）；下带 ADJS ＝ 上涨家数 − 下跌家数（通达信 <code>ADJS:"999999$ADVANCE"-"999999$DECLINE"</code>／同花顺 <code>ADJS:上涨家数-下跌家数</code> 同一算法）· 红柱涨方占优、绿柱跌方占优 · ±1000 / ±2500 为公式自带参考线 · 两图共用一条时间轴（在最底部），拖动 / 滚轮 / 按钮缩放同步</span>
    </div>
  </div>

  <!-- 连板梯队排序图 -->
  <div class="card" data-mod="M05" data-page-node-id="nXW9tkH0hJsG4vyTCX8VrB">
    <h2 data-page-node-id="8q2iA2Q6HyjfQALAwcZIUd">连板梯队 · 竞价实际换手率排序
      <div class="h2right">
        <span class="hint" data-page-node-id="dwWX90ZCs2VdULjRwJvzCk">昨日涨停 + 主板 + 二板及以上 · 竞价换手＝竞价量(手)×100 ÷ 实际流通股（＝自由流通股），条形，＝通达信 CAKPHSZ），右侧标注＝竞价涨幅</span>
        <button type="button" class="rf-btn" data-rf="ladder" title="重新拉取数据并重绘连板梯队"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div style="margin-bottom:8px" data-page-node-id="hrsZXWHV5Nm7u55fUYzFld"><label class="muted" data-page-node-id="ugnuPumuBx8myt4XnWo5Y7">查看日期：</label><select id="ladderDate" style="padding:5px 10px;border:1px solid var(--line);border-radius:8px;font-size:13px" data-page-node-id="Rnnbm5y9mrGGJCLGBq45XS"><option value="2026-10-14">2026-10-14</option></select></div>
    <div id="ladderNote" class="muted" style="font-size:12px;margin-bottom:8px;line-height:1.5" data-page-node-id="ladnote001"></div>
    <div id="ladderChart" data-page-node-id="BBD35UORJQT55Tn1JTCW6H" style="user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); height: 372px; position: relative;"></div>
    <div class="scroll" style="margin-top:8px" data-page-node-id="aq0QCiqNTxY9ly9XR9a8af">
      <table id="ladderTable" data-page-node-id="8CzmF8MS2d50ULXsILCYBN">
        <thead data-page-node-id="EKHvh7GOw7tJhlObeo9VJL"><tr data-page-node-id="VKMHcZwabwTto9By4RfbZg"><th data-page-node-id="3X7W9MocjUNdM7kT51geK5">代码</th><th data-page-node-id="m5e0col" title="八星＝相邻两位数字；相隔的数字不配对。六位代码前两位（市场前缀 60/00/30…）不参与统计。虚数 0/5 为「加强数」，只取紧邻非虚数补位（201 可读作 221/211）。实线＝基础读法，虚线＝加强组合">数字能量</th><th data-page-node-id="ArCtlJ5WyLUzrQPsZpWf5W">名称</th><th data-page-node-id="xqUPlN9KzxOgD4IalZsxOZ">连板</th><th data-page-node-id="DdI1CA4WZsv8glhAtjMTIb">竞价涨幅%</th><th data-page-node-id="aVSSwPe00ZP3Xng4pmqWE6">竞价换手%</th><th data-page-node-id="nin5ci7jUFDNb1PSFwI2jD">竞价量(手)</th><th data-page-node-id="xTdVN1PFONsGofYNOAhDCp">昨日成交量(手)</th><th data-page-node-id="u6rHOevtCxIirCBB3hVZSe">板型</th><th data-page-node-id="u6rHOevtCxIirCBB3hVZSf">题材</th></tr></thead>
        <tbody data-page-node-id="cBKpUDylkeoRCb9eQDKY3D"></tbody>
      </table>
    </div>
  </div>

  <!-- 连板天梯 -->
  <div class="card" data-mod="M06" data-page-node-id="skycard01">
    <style>
      #skyTable th:last-child, #skyTable td:last-child{white-space:normal;min-width:150px;max-width:360px;line-height:1.45}
      /* 身位徽标：板数越高颜色越深（涨=红） */
      #skyTable .sky-bd{display:inline-block;min-width:36px;text-align:center;padding:2px 9px;border-radius:999px;font-size:12px;font-weight:700;letter-spacing:.5px}
      #skyTable .sky-bd.b4{background:#fdeaea;color:#a4161a;border:1px solid #f0bfbf}
      #skyTable .sky-bd.b3{background:#fdeeee;color:#c22626;border:1px solid #f5cfcf}
      #skyTable .sky-bd.b2{background:#fdf3e3;color:#b3620a;border:1px solid #f5ddba}
      #skyTable .sky-bd.bx{background:#f2f4f8;color:#6b7280;border:1px solid var(--line)}
      /* 梯队分组行 */
      #skyTable tr.sky-tier>td{background:#f5f6fa;font-weight:700;font-size:12px;color:#4a5163;padding:7px 10px;border-top:2px solid #dfe3ec;border-bottom:1px solid var(--line)}
      #skyTable tr.sky-tier .sky-tier-b{font-size:13px;letter-spacing:1px}
      .rf-btn.on{border-color:var(--accent);color:var(--accent);background:#f4f8ff}
      /* 交易预警 */
      .al-modes{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:12px}
      @media (max-width:760px){.al-modes{grid-template-columns:1fr}}
      .al-mode{border:1px solid var(--line);border-radius:10px;padding:10px 12px;background:var(--bg-soft,#fafbfd)}
      .al-mode h4{margin:0 0 6px;font-size:13px;letter-spacing:.5px}
      .al-mode .al-desc{font-size:11.5px;color:var(--sub);line-height:1.6}
      .al-card{border:1px solid var(--line);border-radius:10px;margin-bottom:9px;overflow:hidden}
      .al-card.lvA{border-left:4px solid var(--up)}
      .al-card.lvB{border-left:4px solid #d9a441}
      .al-card.lvC{border-left:4px solid #c8ccd6;opacity:.72}
      .al-h{display:flex;align-items:center;gap:9px;padding:8px 11px;background:var(--bg-soft,#fafbfd);flex-wrap:wrap}
      .al-h .al-name{font-size:14px;font-weight:700}
      .al-tag{font-size:11px;padding:1px 7px;border-radius:9px;border:1px solid var(--line)}
      .al-tag.t-card{border-color:#e2a03f;color:#c07a12;background:#fff8ec}
      .al-tag.t-top{border-color:#c0392b;color:#c0392b;background:#fdeeec}
      .al-tag.lv{border-width:1px}
      .al-tag.lvA{border-color:var(--up);color:var(--up)}
      .al-tag.lvB{border-color:#d9a441;color:#b8860b}
      .al-tag.lvC{border-color:#c8ccd6;color:#8a8f99}
      .al-metrics{font-size:11.5px;color:var(--sub);margin-left:auto}
      .al-body{padding:8px 11px;display:grid;grid-template-columns:1fr 1fr;gap:10px}
      @media (max-width:760px){.al-body{grid-template-columns:1fr}}
      .al-col{border:1px dashed var(--line);border-radius:8px;padding:7px 9px}
      .al-col .al-ct{font-size:12px;font-weight:700;margin-bottom:5px}
      .al-cond{font-size:11.5px;line-height:1.85;color:var(--sub)}
      .al-cond b{font-weight:600}
      .mk-ok{color:var(--up);font-weight:700}
      .mk-no{color:var(--down);font-weight:700}
      .mk-unk{color:#b0b4bd;font-weight:700}
      .al-foot{font-size:11px;color:var(--sub);padding:5px 11px;border-top:1px dashed var(--line)}
      /* 模式分区 */
      .al-sec{margin:18px 0 8px;border-radius:12px;overflow:hidden;border:1px solid var(--line);background:#fff}
      .al-sec-card{border-left:4px solid #e2a03f}
      .al-sec-top{border-left:4px solid #c0392b}
      .al-sec-tianliang{border-left:4px solid #2f6bff}
      .al-tag.t-tian{border-color:#2f6bff;color:#2f6bff;background:#eef3ff}
      .al-sec-h{display:flex;align-items:center;gap:10px;padding:11px 14px;flex-wrap:wrap;
        position:relative;border-bottom:1px solid #eef1f6;margin-bottom:10px;
        background:linear-gradient(90deg,rgba(47,107,255,.05),rgba(47,107,255,0))}
      .al-sec-h::after{content:"";position:absolute;left:14px;bottom:-1px;width:54px;height:2px;
        background:linear-gradient(90deg,#2f6bff,#7aa0ff);border-radius:2px}
      .al-sec-t{font-size:15px;font-weight:800;letter-spacing:.5px}
      .al-sec-s{font-size:12px;color:var(--sub);margin-left:auto}
      .al-sec-d{font-size:11.5px;color:var(--sub);line-height:1.6;padding:0 14px 12px}
      .al-sec .al-card{margin:0 14px 12px}
      .al-sec-empty{padding:0 14px 14px;font-size:12.5px;color:#a2937a}
      .al-sec-ico{font-size:16px;line-height:1}
      .al-sec-sub{font-size:11.5px;font-weight:500;color:var(--sub);margin-left:auto}
      /* 三模式总览表 */
      .ab-table{width:100%;border-collapse:collapse;font-size:12.5px}
      .ab-table th{position:sticky;top:0;background:var(--bg-soft,#fafbfd);z-index:2;
        font-size:11.5px;color:var(--sub);font-weight:600;padding:6px 8px;text-align:center;border-bottom:1px solid var(--line)}
      .ab-table th:first-child{text-align:left}
      .ab-table td{padding:5px 8px;border-bottom:1px solid #f0f2f6;text-align:center}
      .ab-table td.ab-nm{text-align:left;font-weight:600;white-space:normal}
      .ab-cd{font-weight:400;color:var(--sub);font-size:10.5px;margin-left:5px}
      .ab-table tr.A{background:rgba(200,60,60,.045)}
      .ab-table tr.B{background:rgba(217,164,65,.05)}
      .ab-table tr.C{opacity:.55}
      .ab-table td.up{color:var(--up);font-weight:600}
      .ab-table td.dn{color:var(--down);font-weight:600}
      .ab-concl{text-align:left;font-size:11.5px}
      .ab-dot{display:inline-block;width:17px;height:17px;line-height:17px;border-radius:50%;
        text-align:center;font-size:11px;font-weight:800;cursor:default}
      .ab-dot.ok{background:rgba(200,60,60,.13);color:var(--up)}
      .ab-dot.unk{background:#fdf3dd;color:#b8860b}
      .ab-dot.no{background:#f1f2f5;color:#a8acb5}
      .ab-h-card{color:#c07a12!important}
      .ab-h-top{color:#c0392b!important}
      .ab-h-tianliang{color:#2f6bff!important}
      .ab-legend{font-size:11.5px;color:var(--sub);margin:0 0 6px;display:flex;align-items:center;gap:5px;flex-wrap:wrap}
      .ab-lg{font-size:11px;padding:1px 7px;border-radius:8px;border:1px solid var(--line);margin-left:4px}
      .ab-lg[data-k="card"]{border-color:#e2a03f;color:#c07a12}
      .ab-lg[data-k="top"]{border-color:#c0392b;color:#c0392b}
      .ab-lg[data-k="tianliang"]{border-color:#2f6bff;color:#2f6bff}
    </style>
    <h2 data-page-node-id="skytitle01">连板天梯 · 身位梯队划分
      <div class="h2right">
        <span class="hint" data-page-node-id="skyhint001">默认只看封板（在连股票），断板可切换显示 · 按身位分组 · 快照日期 <b id="skyDate">--</b></span>
        <button type="button" class="rf-btn on" id="skyOnlySealBtn" title="切换是否显示断板股"><span class="rf-ico">✓</span><span class="rf-tx">只看封板</span></button>
        <button type="button" class="rf-btn" data-rf="skyladder" title="重新拉取连板天梯快照并重绘"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div style="margin-bottom:8px" data-page-node-id="skydate001"><label class="muted">查看日期：</label><select id="skyDateSel" style="padding:5px 10px;border:1px solid var(--line);border-radius:8px;font-size:13px"><option value="">加载中…</option></select></div>
    <div id="skyNote" class="muted" style="font-size:12px;margin-bottom:8px;line-height:1.5" data-page-node-id="skynote001"></div>
    <div id="skySummary" style="font-size:13px;color:var(--sub);margin-bottom:8px" data-page-node-id="skysum0001">加载中…</div>
    <div class="scroll" data-page-node-id="skyscroll1">
      <table id="skyTable" data-page-node-id="skytable01">
        <thead data-page-node-id="skythead01"><tr data-page-node-id="skytr00001"><th data-page-node-id="skyth00001">代码</th><th data-page-node-id="skyth00002" title="八星＝相邻两位数字；相隔的数字不配对。六位代码前两位（市场前缀 60/00/30…）不参与统计。虚数 0/5 为「加强数」，只取紧邻非虚数补位（201 可读作 221/211）。实线＝基础读法，虚线＝加强组合">数字能量</th><th data-page-node-id="skyth00003">名称</th><th data-page-node-id="skyth00004">连板</th><th data-page-node-id="skyth00005">状态</th><th data-page-node-id="skyth00006">涨幅%</th><th data-page-node-id="skyth00007">价格</th><th data-page-node-id="skyth00008" title="实际流通市值＝自由流通股本×股价（同花顺自由流通口径，扣除限售股与大股东等非自由流通筹码），单位亿元">实际流通市值(亿)</th><th data-page-node-id="skyth00009">题材概念涨停原因</th></tr></thead>
        <tbody data-page-node-id="skytbody01"></tbody>
      </table>
    </div>
  </div>

  <!-- 赚钱效应排行（连板天梯下方）：拆解 30 日涨幅榜的赚钱效应来源 -->
  <div class="card" id="earnCard" data-mod="M10">
    <h2>赚钱效应排行 · 30 日涨幅榜拆解
      <div class="h2right">
        <span class="hint" id="earnHint">样本＝30 日涨幅榜 52 只 · 形态由连板梯队快照推导 · 五行＝数字能量主星</span>
        <button type="button" class="rf-btn" data-rf="earn" title="重新计算赚钱效应排行"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div id="earnOverview" class="ea-ov"></div>
    <div class="ea-grid">
      <div class="ea-box" id="boxForm"><h3><span class="no">1</span>形态榜 <small id="eaSpecForm">按平均涨幅＝赚钱效应强度</small></h3><div id="earnForm"></div></div>
      <div class="ea-side">
        <div class="ea-box" id="boxWx"><h3><span class="no">2</span>五行榜 <small id="eaSpecWx">按入榜家数＝最旺五行</small></h3><div id="earnWx"></div></div>
        <div class="ea-box" id="boxCap"><h3><span class="no">3</span>市值档榜 <small id="eaSpecCap">按平均涨幅＝哪一档最赚钱</small></h3><div id="earnCap"></div></div>
      </div>
      <div class="ea-box" id="boxTheme"><h3><span class="no">4</span>题材榜 <small id="eaSpecTheme">按入榜家数＝资金集中度</small></h3><div id="earnTheme"></div></div>
      <div class="ea-box" id="boxRegion"><h3><span class="no">5</span>地域榜 <small id="eaSpecRegion">按入榜家数＝炒地图热度</small></h3><div id="earnRegion"></div></div>
      <div class="ea-box" id="boxTop"><h3><span class="no">6</span>龙头画像 <small>Top 15 各维度标签</small></h3><div id="earnTop"></div></div>
    </div>
  </div>

  <!-- 复盘计划及交易预警 -->
  <div class="card" id="alertCard" data-mod="M09">
    <h2>复盘计划及交易预警 · 换手卡位 / 最高标 / 天量弱转强
      <div class="h2right">
        <span class="hint">规则来源《提取自卡位统计》· 天量弱转强＝天量的次日做 · 收盘后自动生成次日名单</span>
        <button type="button" class="rf-btn" data-rf="alert" title="重新拉取数据并重跑预警筛选"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div style="margin-bottom:8px;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
      <label class="muted">查看日期：</label><select id="alertDateSel" style="padding:5px 10px;border:1px solid var(--line);border-radius:8px;font-size:13px"><option value="">加载中…</option></select>
      <button type="button" class="rf-btn on" id="alertStrictBtn" title="严格：前一日首封时间 ≤09:25 即视为一字板排除；放宽：T字板/换手板都算换手板"><span class="rf-ico">◆</span><span class="rf-tx">严格口径</span></button>
      <button type="button" class="rf-btn" id="alertShowCBtn" title="是否显示被硬条件排除的标的（便于查看未通过原因）"><span class="rf-ico">☰</span><span class="rf-tx">含被排除</span></button>
      <button type="button" class="rf-btn on" id="alertViewBtn" title="分区：两个模式各自独立成块；对照：同一只股票并排显示两套模式判定"><span class="rf-ico">▤</span><span class="rf-tx">分区视图</span></button>
      <span class="muted" style="font-size:12px">前一日必须是换手板（首封≠9:25）· 竞价红开 · 卡位≥4板 / 最高标≥5板</span>
    </div>
    <div class="al-sec">
      <div class="al-sec-h"><span class="al-sec-ico">📌</span>复盘计划<span class="al-sec-sub">换手卡位 / 最高标 / 天量弱转强 / 五虎</span></div>
      <div id="alertPlan" class="al-plan"></div>
    </div>
    <div id="alertNodes" class="al-nodes"></div>
    <details class="al-ref">
      <summary><span class="al-sec-ico">📖</span>模式规则与判定说明（点击展开）</summary>
      <div id="alertModes"></div>
      <div id="alertNote"></div>
    </details>
  </div>

  <!-- 天量扫描 -->
  <div class="card" data-mod="M07" data-page-node-id="volcard01">
    <h2 data-page-node-id="arAuueuOSk4zOWmQnpQz9U">主板涨停 · 历史天量扫描
      <div class="h2right">
        <span class="hint" data-page-node-id="75WdG5oOHMALhHPFEwmKUc">今日涨停的主板股中，成交量进入上市以来前三大者（创上市新高／历史第2大量／历史第3大量）· 收盘后 15:05 自动更新</span>
        <button type="button" class="rf-btn" data-rf="volume" title="重新拉取数据并重绘天量扫描"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <style>
      #volTable th,#volTable td{padding:5px 6px}
      #volTable td.v-nm{font-weight:700;white-space:nowrap;min-width:128px}
      #volTable td.v-nm .ea-eg{margin-top:4px}
      #volTable td.v-nm .ea-eg .m5-stars{max-width:168px}
      #volTable td.v-theme{white-space:normal;word-break:break-all;line-height:1.5;max-width:168px;min-width:120px}
      #volTable .v-note{display:inline-block;border-radius:5px;padding:1px 7px;font-size:10.5px;
        background:#f4f6fa;border:1px solid #e6eaf3;color:#4b556b;white-space:nowrap}
      #volTable .v-note.r1{background:#fdecec;border-color:#f2c6c6;color:#a4161a;font-weight:700}
      #volTable .v-note.r2{background:#fff3e0;border-color:#f0d5a8;color:#a35b00;font-weight:600}
      #volTable .v-note.r3{background:#eef2ff;border-color:#cdd6f5;color:#3f51a8;font-weight:600}
    </style>
    <div id="volSummary" style="font-size:13px;color:var(--sub);margin-bottom:8px" data-page-node-id="lvzLL20uMBFi4F4aiNdQFS">2026-10-14 · 今日主板涨停 <b>36</b> 只，其中 <b style="color:var(--up)">2</b> 只创上市以来成交量新高</div>
    <div class="scroll" data-page-node-id="CDrh9mRePxpd13jeWGJRJe">
      <table id="volTable" data-page-node-id="X4EbgEUmHvRjDA05t92IBL">
        <thead data-page-node-id="kG8iwSK9WEI6iBgZnj5EDD"><tr data-page-node-id="ejMa9seYwXcGsxbqCfJaIY"><th data-page-node-id="HZoGt2J1RnK7pnFAJ1UhYL">代码</th><th data-page-node-id="WgdLxiAbjmJG6KMbAO3PER">名称</th><th data-page-node-id="vzg14aXkF5tAtoAsgnXLXk">连板</th><th data-page-node-id="ljgafjWelhwU9MczDyFwVm">现价</th><th data-page-node-id="MwodlFDLbGEKUlRGZqrs30">涨幅%</th><th data-page-node-id="CPpa4XuaIzW5p73Zvc01xI">换手%</th><th data-page-node-id="1XT8PPE7SERuEmKTXaV8Ck">成交量(手)</th><th data-page-node-id="JZVMER9BrAAW2jcLWasBkB">前历史峰值(手)</th><th data-page-node-id="F2Rj5xe5ziQZv7Ryr0p2qh" title="实际流通市值＝自由流通股本×股价（同花顺自由流通口径），单位亿元">实际流通市值(亿)</th><th data-page-node-id="kmBOcwIyypV2B1jNQjwzL2">板型</th><th data-page-node-id="3AnuEtrs9thwdE8cEQejUf">题材</th><th data-page-node-id="volNoteTh01">备注</th></tr></thead>
        <tbody data-page-node-id="NSoMrmVeDqZsQW9RWtFiLT"></tbody>
      </table>
    </div>
  </div>

  <!-- 题材周期主图（模块四） -->
  <div class="card" data-mod="M08" data-page-node-id="LaksDFcjkasGR0LxyfXVHG">
    <h2 data-page-node-id="uNZOldPnujtkEe4UWZ4qIP">题材周期主图 · 连板梯队时间轴
      <div class="h2right">
        <span class="hint" data-page-node-id="V30Uwj0UODVu1qSdtemsSQ">来源：贯穿.xlsx「周期主图」近 4 年逐日标注 · 收盘后自动更新</span>
        <button type="button" class="rf-btn" data-rf="cycle" title="重新拉取 cycle.js 并重绘题材周期主图"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <style>
      #cycLegend{display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 10px;align-items:center}
      #cycLegend .chip{display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:999px;font-size:12px;background:#fff;border:1px solid var(--line)}
      #cycLegend .chip b{font-size:14px;letter-spacing:1px}
      #cycLegend .chip-zt b{color:#d93a3a}
      #cycLegend .chip-zb b{color:#e08b1a}
      #cycLegend .chip-dt b{color:#1a9c6b}
      #cycLegend .chip-db b{color:#98a0b3}
      .cyc-sw{display:inline-block;width:17px;height:17px;border-radius:4px;vertical-align:-4px;
        font:600 10px/17px ui-monospace,Consolas,monospace;text-align:center;color:#fff}
      .cyc-lg-sep{display:inline-block;width:1px;height:16px;background:var(--line);margin:0 3px}
      #cycChart{width:100%;height:clamp(340px,52vh,520px)}
      #adjsChart{width:100%;height:clamp(300px,42vh,420px)}
      #breadthKpis{margin:2px 0 10px}
      #hotCols table{width:100%;border-collapse:collapse;font-size:12.5px}
      #hotCols th{font-weight:600;color:var(--sub);text-align:left;padding:4px 6px;border-bottom:1px solid var(--line);font-size:11.5px}
      #hotCols td{padding:3px 6px;border-bottom:1px dashed #eef0f5;font-variant-numeric:tabular-nums}
      #hotCols tr:hover td{background:#f6f8fc}
      #hotCols .rk{display:inline-block;min-width:20px;text-align:center;border-radius:5px;background:#eef1f6;color:#5a6274;font-size:11px;padding:1px 4px}
      #hotCols .rk.t3{background:#fde8c8;color:#a35a00}
      #hotCols .src{font-size:11px;color:var(--sub);margin:0 0 6px}
      .cyc-chart-note{font-size:11px;color:var(--sub);margin:2px 0 10px;line-height:1.7}
      .cycbar{display:flex;flex-wrap:nowrap;gap:1px;font-size:0}
      .cycbar i{display:inline-block;min-width:13px;height:15px;border-radius:3px;
        font:600 9px/15px ui-monospace,Consolas,monospace;text-align:center;font-style:normal}
      .cycbar i.z{background:#f96a6a;color:#fff}
      .cycbar i.z1{background:#ffd9d9;color:#a33}
      .cycbar i.z2{background:#ff9b9b;color:#8a2020}
      .cycbar i.b{background:#ffe6c7;color:#b35c00}
      .cycbar i.d{background:#d6f0e3;color:#12734e}
      .cycbar i.s{background:#f1f3f7;color:#a8b0c0}
      .cycbar i.n{background:#fbf7e8;color:#9a8b5a}
  .cycfilters{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:8px;font-size:13px;color:var(--ink)}
  .cycfilters input,.cycfilters select{padding:5px 9px;border:1px solid var(--line);border-radius:8px;font-size:13px;background:#fff;color:var(--ink)}
  .cycfilters button{padding:4px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer;font-size:12px}
  .cycfilters button:hover{background:#f5f6fa}
      .cyc-fix-pop{position:fixed;z-index:60;display:none;background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px;box-shadow:0 8px 24px rgba(0,0,0,.15);font-size:13px;color:var(--ink)}
  .cyc-fix-pop select{width:100%;margin:6px 0;padding:5px;background:#fff;color:var(--ink)}
  .cyc-fix-pop button{padding:4px 12px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer;margin-right:4px}
      .cyc-h3{margin:14px 0 6px;font-size:14px;color:var(--sub)}
      .cyc-matrix-block{margin-top:14px}
  .cyc-matrix-toolbar{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:6px 0;font-size:13px;color:var(--ink)}
  .cyc-matrix-toolbar button{padding:4px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);cursor:pointer;font-size:12px}
      .cyc-matrix-toolbar button:hover{background:#f5f6fa}
      .cyc-matrix-toolbar button.primary{background:#2f6bff;border-color:#2f6bff;color:#fff;font-weight:600}
      .cyc-matrix-toolbar button.primary:hover{background:#2459dc}
      .cyc-matrix-toolbar select{padding:4px 8px;border:1px solid var(--line);border-radius:8px;font-size:12px;background:#fff;color:var(--ink);max-width:112px}
      .cyc-nav-sep{display:inline-block;width:1px;height:18px;background:var(--line);margin:0 2px}
      .cyc-chk{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--ink);cursor:pointer}
      .cyc-chk input{vertical-align:middle}
      .cyc-matrix-toolbar input[type=range]{vertical-align:middle}
      .cyc-matrix-stage{border:1px solid var(--line);border-radius:8px;overflow:hidden;background:#fff;position:relative}
      #cycMatrixScroll{height:clamp(380px,60vh,560px);overflow:auto;position:relative}
      #cycMatrixCanvas{position:sticky;top:0;left:0;display:block;cursor:crosshair;background:#fff}
      .cyc-tip{position:absolute;z-index:30;display:none;pointer-events:none;
        background:rgba(26,29,36,.94);color:#fff;font-size:12px;padding:7px 10px;border-radius:7px;
        box-shadow:0 8px 22px rgba(0,0,0,.24);white-space:nowrap;line-height:1.6}
      .cyc-tip b{color:#ffd9d9}
      .cyc-tip .t-sub{color:#a8b0c0;font-size:11px}
      .cyc-toolbar-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin:6px 0 8px;font-size:12px;color:var(--sub)}
      .cyc-toolbar-row .cyc-chk{font-size:12px}
    </style>
    <div id="cycLegend" data-page-node-id="mbuyog5GFrEh0nt86KZBXJ"><span class="chip chip-zt"><b>⭕◯⊖θⓉⓁ┱®©℗Ⅱ</b> 涨停</span><span class="chip chip-zb"><b>∅</b> 炸板</span><span class="chip chip-dt"><b>┻┴</b> 跌停</span><span class="chip chip-db"><b>/</b> 断板 / 非涨停</span></div>
    <div class="cycfilters" data-page-node-id="k6ooQy37Tf5Ucdbsy64ZZh">
      <label data-page-node-id="SskGXJQP1U1nuWG7bObecy">题材包含：<input id="cycTheme" placeholder="如：医药 / 数字经济 / 机器人" size="22" data-page-node-id="CHSrl2yyl0b7O8ZrrCxjVn"></label>
      <label data-page-node-id="T9lzwjP6G8esHDCM9Rpp7B">最低连板：
        <select id="cycMin" data-page-node-id="GW5k08iwlLRJuwY1ivmKC9">
          <option value="2" data-page-node-id="9JVi6emwMOE7eh3xixCodY">2板以上</option>
          <option value="3" data-page-node-id="2wRh8HJkwj2joTbD99yYKb">3板以上</option>
          <option value="4" data-page-node-id="XZOHKjCsOnQtVTuOBA1yQ1">4板以上</option>
          <option value="5" data-page-node-id="9tByFnA6bSJguVHrVe6tfE">5板以上</option>
          <option value="6" data-page-node-id="FB6NkZydTLb27a3cyxjUmN">6板以上</option>
          <option value="7" data-page-node-id="3uFXXOQxqHVqqOEkJYl0jb">7板以上</option>
        </select>
      </label>
      <label data-page-node-id="03CHTxH3BwIi44nQwoBaIi">年份：
        <select id="cycYear" data-page-node-id="t84jsBex3eWuoFUcecOlKD"><option value="">全部</option></select>
      </label>
      <label title="只看该时间窗内还在连板的龙头（仅影响上方折线，表格/矩阵仍是全量）">梯队范围：
        <select id="cycScope">
          <option value="20">近 20 日</option>
          <option value="40">近 40 日</option>
          <option value="60" selected>近 60 日</option>
          <option value="120">近 120 日</option>
          <option value="0">全部（按历史最高板）</option>
        </select>
      </label>
      <label class="cyc-chk" title="每个交易日的点旁竖排标注「股票名＋板数」，同日并列龙头挨个标"><input type="checkbox" id="cycNameLbl" checked> 名称标注</label>
      <label class="cyc-chk" title="情绪空间公式 X1：把「当日涨停家数」按连板高度轴归一化后叠加（HLB×涨停数/HZT）"><input type="checkbox" id="cycX1"> 涨停数</label>
      <label class="cyc-chk" title="情绪空间公式 X2：把「当日连板家数(≥2板)」按连板高度轴归一化后叠加（HLB×连板个数/HLBGS）"><input type="checkbox" id="cycX2"> 连板个数</label>
      <span id="cycCount" class="muted" data-page-node-id="zoSW21sQeuyhUEHPgqQwug">命中 0 只，展示前 0 只</span>
    </div>
    <div class="cyc-chart-note"><b style="color:var(--ink)">连板梯队时间轴 · 纯连板高度图（模块顶部图表）</b><br>数据<b>直接取自下方「全量还原矩阵」那张表</b>，逐日汇总而成，两图同源。<br>横轴＝交易日，纵轴＝连板高度（<b>一格就是一板</b>）。<b>一条线 = 一段纯连板</b>：从首板起逐日爬升；中途有一天不涨停这条线当场结束，<b>反包另起一条线从第 1 板重数</b>。粗线＝高度更高，颜色深浅＝板数档位，名字尾带 <b>●</b> ＝当前仍在连板。<br>红线＝每日最高板接力线（高标断板当天，高度落到次高票，几板就是几板），点旁竖排「股票名＋板数」。<br><b style="color:#5E409B">紫色虚线＝涨停家数</b>、<b style="color:#808080">灰色点线＝连板个数(≥2板)</b>：照搬通达信「情绪空间」公式，按连板高度轴归一化（HLB×数值/HHV）后叠加在同一张图上，开关在上方「涨停数 / 连板个数」。<br><b style="color:#2f6bff">两图联动</b>：点这里任意一天，下方矩阵直接定位到该日并筛出当天有动作的票；反过来在矩阵里用「当日 / 年月日 / ◀▶」换一天，这里会竖一条蓝线标出来。</div>
    <div id="cycChart" data-page-node-id="W4v2Oz8o9Ah4PkbdPXmWA1" style="user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);"></div>
    <div class="scroll" style="margin-top:8px" data-page-node-id="mwqtOqCV5hdGN0Li0prMyW">
      <table id="cycTable" data-page-node-id="fPDg232oNKpVwwYLK60MEH">
        <thead data-page-node-id="aSzX7tBmZ0Z5VNWtfCWnMB"><tr data-page-node-id="ABzMcY3KsWEnzVI5GGh9dI"><th data-page-node-id="Gj43RFfvSnus5Bz0rRtIDQ">龙头</th><th data-page-node-id="2NVHoNwzdmJGzQ0YVA7TH0">题材</th><th data-page-node-id="6zCJ0OP7qeMtuyp7oEb5r4">最高板</th><th data-page-node-id="Qo06ov9czd9Y2kjnr9COfJ">起始日</th><th data-page-node-id="PYCvq6bbUxSn22nRJgimgL">结束日</th><th data-page-node-id="EXkMic4drhKGr9uBj8eysj">逐日图例</th></tr></thead>
        <tbody data-page-node-id="MuBzc3eZV33CiwIGX9yjI3"></tbody>
      </table>
    </div>
    <h3 class="cyc-h3">周期主图 · 全量还原（原始单元格 · 可滑动/缩放/点击修正）</h3>
    <div class="cyc-matrix-block" data-page-node-id="cycMatrixBlock">
      <div class="cyc-matrix-toolbar" data-page-node-id="cycMatrixToolbar">
        <button id="cycGoToday" type="button" class="primary" title="跳到今天所在列并高亮；今日休市/无数据则停在最近一个交易日">当日</button>
        <span class="cyc-nav-sep"></span>
        <label>定位到
          <select id="cycNavY" title="选择年份"></select>
          <select id="cycNavM" title="选择月份"></select>
          <select id="cycNavD" title="选择日期（仅列出数据里的交易日）"></select>
        </label>
        <button id="cycGoDate" type="button" title="跳转到所选日期所在列并高亮">定位</button>
        <button id="cycPrevDay" type="button" title="前一个交易日">◀</button>
        <button id="cycNextDay" type="button" title="后一个交易日">▶</button>
        <label class="cyc-chk" title="只保留在定位日有标注的龙头，纵向不再拖 1600 多行"><input type="checkbox" id="cycOnlyDay"> 只看当日有标注</label>
        <span id="cycNavStat" class="muted" data-page-node-id="cycNavStat"></span>
        <span class="cyc-nav-sep"></span>
        <label class="cyc-chk" title="色块深浅＋连板数字表示涨停高度；取消则显示 Excel 原始符号"><input type="checkbox" id="cycNumMode" checked> 连板数字</label>
        <label>单元格缩放 <input type="range" id="cycZoom" min="12" max="44" value="22" data-page-node-id="cycZoom"></label>
        <button id="cycExportFix" type="button" data-page-node-id="cycExportFix">导出修正</button>
        <button id="cycClearFix" type="button" data-page-node-id="cycClearFix">清除全部修正</button>
        <span id="cycFixStat" class="muted" data-page-node-id="cycFixStat"></span>
      </div>
      <div class="cyc-matrix-stage" data-page-node-id="cycMatrixStage">
        <div id="cycMatrixScroll" data-page-node-id="cycMatrixScroll">
          <div id="cycMatrixSpacer" data-page-node-id="cycMatrixSpacer">
            <canvas id="cycMatrixCanvas" data-page-node-id="cycMatrixCanvas"></canvas>
          </div>
        </div>
        <div id="cycTip" class="cyc-tip"></div>
      </div>
      <div class="cyc-toolbar-row">
        <span>滚轮滚动 · Shift+滚轮横向 · 拖动平移 · 悬停看详情 · 点击单元格可手动修正分类</span>
        <span id="cycAxisStat" class="muted" style="margin-left:auto"></span>
      </div>
      <div id="cycFixPop" class="cyc-fix-pop" data-page-node-id="cycFixPop">
        <div id="cycFixTitle" data-page-node-id="cycFixTitle"></div>
        <select id="cycFixSel" data-page-node-id="cycFixSel">
          <option value="zt">涨停</option>
          <option value="zb">炸板</option>
          <option value="dt">跌停</option>
          <option value="db">断板/非涨停</option>
          <option value="up">趋势↑</option>
          <option value="down">趋势↓</option>
          <option value="note">注解(忽略)</option>
          <option value="__clear">清除修正</option>
        </select>
        <div><button id="cycFixOk" type="button" data-page-node-id="cycFixOk">确定</button> <button id="cycFixCancel" type="button" data-page-node-id="cycFixCancel">取消</button></div>
      </div>
    </div>
    <h3 style="margin:14px 0 6px;font-size:14px;color:var(--sub)" data-page-node-id="lbHB3d9z5UFlqPu7Kv96wO">每日自动追踪（收盘后写入）</h3>
    <div class="scroll" data-page-node-id="oTmJwqpu4HdSVxiUwDiVDS">
      <table id="cycAutoTable" data-page-node-id="MR0JvOAGV0Qb7nwZsD8QGr">
        <thead data-page-node-id="BWWaHO8aL0gFmXziyMic0g"><tr data-page-node-id="4DndO8kuKp8tczDgA2KxUc"><th data-page-node-id="tRKhZYF7AFKljwzT5eV5sc">日期</th><th data-page-node-id="iDblfCFuclmoXzHjNT0jHw">龙头</th><th data-page-node-id="0vGcYd5IFFTSeFHBWAjq4t">连板</th><th data-page-node-id="1X7Wg0wF5HWgdWow0EOtgE">当日状态</th><th data-page-node-id="ZwoMBmVoQEXoGNyo0t06pY">题材</th></tr></thead>
        <tbody data-page-node-id="mISVWDSG2EqAywMMGl1rpY"></tbody>
      </table>
    </div>
  </div>

  <!-- 30 日涨幅榜 / 异动监控（逼近监管红线）/ 监管票列表 —— 数据：同花顺 iFinD -->
  <style>
    .nm-grid{display:grid;grid-template-columns:1.12fr .88fr;gap:14px}
    @media(max-width:980px){.nm-grid{grid-template-columns:1fr}}
    .nm-box{border:1px solid var(--line);border-radius:9px;padding:10px 12px;background:#fcfdff}
    .nm-box h3{margin:0 0 8px;font-size:13px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
    .nm-box h3 small{font-weight:400;color:var(--sub);font-size:11px}
    .nm-table{width:100%;border-collapse:collapse;font-size:12.5px}
    .nm-table th{font-weight:600;color:var(--sub);text-align:left;padding:4px 6px;border-bottom:1px solid var(--line);font-size:11.5px;white-space:nowrap}
    .nm-table td{padding:4px 6px;border-bottom:1px dashed #eef0f5;vertical-align:top}
    .nm-table tr:hover td{background:#f6f8fc}
    .nm-table .num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
    .nm-table .code{font-family:Consolas,Menlo,monospace;color:var(--sub);white-space:nowrap}
    .nm-table .nm-name{font-weight:600;white-space:nowrap}
    .nm-scroll{max-height:440px;overflow:auto;border:1px solid var(--line);border-radius:8px}
    .nm-scroll .nm-table th{position:sticky;top:0;background:#f7f9fc;z-index:2}
    .nm-bar{position:relative;height:7px;border-radius:4px;background:#eef1f7;min-width:92px;overflow:hidden;margin:2px 0 1px}
    .nm-bar i{position:absolute;left:0;top:0;bottom:0;border-radius:4px}
    .nm-bar i.r{background:#d93a3a} .nm-bar i.o{background:#e08b1a}
    .nm-bar-tx{font-size:10.5px;color:var(--sub)}
    .nm-warn{color:#d93a3a;font-weight:700}
    .nm-up{color:#d93a3a}
    /* ===== 赚钱效应排行（连板天梯下方） ===== */
    /* 概览统计卡：大字允许自动另起一行，绝不裁切、也不允许把格子撑跑 */
    .ea-ov{display:grid;grid-template-columns:repeat(auto-fit,minmax(112px,1fr));gap:9px;margin-bottom:14px}
    .ea-st{position:relative;background:linear-gradient(180deg,#fff,#fafbfd);border:1px solid #e8ecf4;border-radius:11px;
      padding:8px 12px 7px;overflow:hidden;box-shadow:0 1px 2px rgba(16,24,40,.04);min-width:0}
    .ea-st::before{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--ac,#d93a3a)}
    .ea-st em{display:block;font-style:normal;font-size:10.5px;color:var(--sub);letter-spacing:.3px;margin-bottom:1px;
      white-space:normal;overflow-wrap:anywhere;line-height:1.3}
    .ea-st em i{font-style:normal;font-size:9.5px;color:#9aa3b5;margin-left:3px;white-space:nowrap}
    .ea-st b{display:block;font-size:15.5px;font-weight:800;color:var(--ink);font-variant-numeric:tabular-nums;
      line-height:1.28;white-space:normal;word-break:break-word;overflow-wrap:anywhere}
    .ea-st b.up{color:#d93a3a}
    .ea-st small{font-size:10.5px;color:var(--sub);font-weight:500;margin-left:2px;white-space:nowrap}
    .ea-grid{display:grid;grid-template-columns:1fr 1fr;grid-template-areas:"form side" "theme region" "top top";gap:14px}
    @media(max-width:980px){.ea-grid{grid-template-columns:1fr;grid-template-areas:"form" "side" "theme" "region" "top"}}
    .ea-box{border:1px solid #e8ecf4;border-radius:12px;padding:12px 14px 8px;background:#fff;position:relative;overflow:hidden;
      box-shadow:0 1px 3px rgba(16,24,40,.05)}
    .ea-box::before{content:'';position:absolute;left:0;top:0;right:0;height:3px;background:var(--ac,#d93a3a)}
    #boxForm{grid-area:form;--ac:#d93a3a;--bar1:#f3b0a5}
    .ea-side{grid-area:side;display:grid;gap:14px;align-content:start}
    #boxWx{--ac:#2f6bff;--bar1:#9db9f5}
    #boxCap{--ac:#8b5cf6;--bar1:#c4b0f0}
    #boxTheme{grid-area:theme;--ac:#e08b1a;--bar1:#f2c58a}
    #boxRegion{grid-area:region;--ac:#2e9e5b;--bar1:#96cfab}
    #boxTop{grid-area:top;--ac:#334155;--bar1:#94a3b8}
    .ea-box h3{margin:0 0 9px;font-size:13.5px;display:flex;align-items:center;gap:8px;flex-wrap:wrap}
    .ea-box h3 .no{display:inline-flex;flex:0 0 auto;width:21px;height:21px;border-radius:7px;background:var(--ac,#d93a3a);
      color:#fff;font-size:11.5px;font-weight:800;align-items:center;justify-content:center;box-shadow:0 2px 4px -1px rgba(16,24,40,.25)}
    .ea-box h3 small{font-weight:400;color:var(--sub);font-size:11px}
    .ea-row{padding:7px 4px 6px;border-bottom:1px dashed #f0f3f8}
    .ea-row:last-child{border-bottom:none}
    .ea-row.r1{background:linear-gradient(90deg,#fff6f4,rgba(255,246,244,0) 70%);border-radius:8px}
    .ea-line{display:flex;align-items:center;gap:8px}
    .ea-rank{flex:0 0 18px;height:18px;border-radius:50%;font-size:10.5px;font-weight:800;display:inline-flex;
      align-items:center;justify-content:center;background:#eef1f7;color:#8b93a6;font-variant-numeric:tabular-nums}
    .ea-rank.r1{background:linear-gradient(135deg,#f7b733,#e08b1a);color:#fff;box-shadow:0 1px 3px rgba(224,139,26,.4)}
    .ea-rank.r2{background:linear-gradient(135deg,#c3cad6,#98a1b3);color:#fff}
    .ea-rank.r3{background:linear-gradient(135deg,#e2a679,#c0763e);color:#fff}
    .ea-k{flex:0 0 auto;min-width:80px;font-size:12.5px;font-weight:700;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
    .ea-k small{color:var(--sub);font-weight:400;font-size:11px}
    /* 档位名两行显示：主名 + 括号区间（如 大盘 / （>100亿）），避免挤压、截断 */
    .ea-k.ea-k2{white-space:normal;overflow:visible;line-height:1.3;min-width:96px}
    .ea-k.ea-k2 em{display:block;font-style:normal;font-size:10.5px;font-weight:500;color:var(--sub);
      font-variant-numeric:tabular-nums;margin-top:1px}
    .ea-st b small.sub2{display:block;font-size:11px;color:var(--sub);font-weight:600;margin:1px 0 0;white-space:nowrap}
    .ea-bar{flex:1 1 auto;position:relative;height:9px;border-radius:6px;background:#eef1f7;overflow:hidden;min-width:56px;
      box-shadow:inset 0 1px 2px rgba(16,24,40,.06)}
    .ea-bar i{position:absolute;left:0;top:0;bottom:0;border-radius:6px;background:linear-gradient(90deg,var(--bar1,#f3b0a5),var(--ac,#d93a3a))}
    .ea-v{flex:0 0 auto;font-size:12.5px;font-variant-numeric:tabular-nums;color:var(--ink);white-space:nowrap}
    .ea-v b{color:#c22626;font-weight:800;font-size:13px}
    .ea-v em{font-style:normal;color:var(--sub);font-size:11px}
    .ea-v em.ea-med{color:#8b93a6;font-size:10.5px}
    /* 强度榜样本不足标记：均涨幅易被单只拉高，不作为「最赚钱/最强」冠军 */
    .ea-thin{flex:0 0 auto;margin-left:6px;font-size:10px;font-weight:600;white-space:nowrap;
      padding:1px 5px;border-radius:4px;background:#fff4e5;border:1px solid #f0d5a8;color:#a35b00}
    .ea-row[data-thin] .ea-k,.ea-row[data-thin] .ea-v b{color:#8b93a6}
    .ea-ex{font-size:11.5px;color:var(--sub);line-height:1.65;margin:2px 0 0 106px;word-break:break-all}
    .ea-ex b{color:#c22626;font-weight:800}
    /* 举例股票：一只一行，右边挂该股的数字能量小标签（与上方各表同口径） */
    .ea-exrow{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:3px 0 0}
    .ea-exrow .ea-tag{flex:0 0 auto;margin:0}
    .ea-exrow .ea-tag b{margin-left:3px}
    .ea-exeg{display:inline-flex;flex-wrap:wrap;gap:2px}
    .ea-exeg .m5-stars{display:flex;flex-wrap:wrap;gap:2px;margin:0}
    .ea-exeg .m5-star.sm{font-size:9.5px;line-height:15px;padding:0 4px;margin:0;border-radius:4px;background:rgba(0,0,0,.012)}
    .ea-exeg .m5-star.sm .m5-star-wx{font-size:8.5px;line-height:11px;padding:0 2px;margin-left:2px;border-radius:2px}
    .ea-days-row{display:block;margin:4px 0 0}
    .ea-row.ea-empty{background:#fafbfd;border:1px dashed #e0e6f0;border-radius:9px;opacity:.8}
    .ea-empty .ea-bar{background:#f1f3f8;box-shadow:none}
    .ea-empty .ea-k{color:#8b93a6;font-weight:600}
    .ea-days{display:inline-flex;align-items:center;margin-left:8px;border:1px solid #e8edf6;background:#f6f8fc;border-radius:6px;
      overflow:hidden;font-size:10.5px;color:#8b93a6;white-space:nowrap;vertical-align:1px}
    .ea-days i{font-style:normal;padding:1px 7px;border-right:1px solid #e8edf6}
    .ea-days i:last-child{border-right:none}
    .ea-days b{color:#a4161a;font-weight:800}
    .ea-table{width:100%;border-collapse:collapse;font-size:12px}
    .ea-table th{font-weight:600;color:var(--sub);text-align:left;padding:5px 7px;border-bottom:1px solid var(--line);
      font-size:11px;white-space:nowrap;background:#f8fafd;position:sticky;top:0;z-index:2}
    .ea-table td{padding:4px 7px;border-bottom:1px dashed #eef0f5;vertical-align:top}
    .ea-table tbody tr:nth-child(even){background:#fafbfd}
    .ea-table tbody tr:hover{background:#fff6f4}
    .ea-table .num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
    .ea-table .code{font-family:Consolas,Menlo,monospace;color:var(--sub);white-space:nowrap}
    .ea-table .ea-nm{font-weight:700;white-space:nowrap;min-width:150px}
    /* 龙头画像：股票名下方挂数字能量标签（与上方各表同口径，尺寸收紧以适配表格） */
    .ea-eg{margin-top:4px}
    .ea-eg .m5-stars{display:flex;flex-wrap:wrap;gap:2px;max-width:212px;margin-top:0}
    .ea-eg .m5-star.sm{font-size:10px;line-height:16px;padding:0 5px;margin:0;border-radius:5px;
      background:rgba(0,0,0,.012);font-variant-numeric:tabular-nums}
    .ea-eg .m5-star.sm .m5-star-wx{font-size:9px;line-height:12px;padding:0 3px;margin-left:3px;border-radius:3px}
    .ea-tag{display:inline-block;border-radius:5px;padding:1px 6px;margin:1px 4px 1px 0;font-size:10.5px;
      background:#f4f6fa;border:1px solid #e6eaf3;color:#4b556b;white-space:nowrap}
    .ea-tag.hot{background:#fdecec;border-color:#f2c6c6;color:#a4161a;font-weight:600}
    .ea-tag.wx{color:#fff;border:none;font-weight:700}
  </style>
  <div class="card" id="newmodCard" data-mod="M11">
    <h2>30 日涨幅榜 · 异动监控 · 监管票
      <div class="h2right">
        <span class="hint" id="newmodHint">数据来源：同花顺 iFinD（选股 / 指标 / 公告检索）· 30 日与 10 日均为前复权区间涨幅 · 15:20 自动更新</span>
        <button type="button" class="rf-btn" data-rf="newmod" title="重新拉取 newmod_data.js 并重绘"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div class="nm-grid">
      <div class="nm-box" id="nmMove"></div>
      <div class="nm-box" id="nmReg"></div>
    </div>
    <div class="nm-box" id="nmR30Box" style="margin-top:14px"></div>
  </div>

  <!-- 24 小时股票人气榜（三平台，放在最底端） -->
  <div class="card" id="hotCard" data-mod="M12">
    <h2>24 小时股票人气榜 · 同花顺 / 东方财富股吧 / 淘股吧
      <div class="h2right">
        <span class="hint" id="hotHint">三平台各自独立抓取，互不阻塞；淘股吧未开放个股榜接口，用其题材榜的人气龙头聚合</span>
        <button type="button" class="rf-btn" data-rf="hotrank" title="重新拉取 hotrank.js 并重绘"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div id="hotCols" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:14px"></div>
  </div>

  <div class="grid2" data-page-node-id="g8Bdh4fSk6vS5dBbozPoCZ">
    <!-- 今日竞价明细 -->
    

    <!-- 纪律清单 -->
    
  </div>

  <!-- ============ 中周期猜想（M13）· 依据《龙头股情绪周期教程》中级周期五阶段 ============ -->
  <div class="card" id="cgMidCard" data-mod="M13">
    <h2>🌀 中周期猜想 · 2022→今天 中级周期五阶段演绎
      <div class="h2right">
        <span class="hint">口径：《龙头股情绪周期教程》第四章 · 赚钱效应回暖→低迷→高潮→亏钱出现→亏钱炸裂（约3个月一轮，可反复轮回）</span>
        <button type="button" class="rf-btn" data-rf="cgmid" title="重新拉取 cycle_guess.js 并重绘中周期猜想"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div id="cgMidBox"><div class="muted">加载中…</div></div>
  </div>

  <!-- ============ 题材周期猜想（M14）· 依据教程题材周期七阶段 ============ -->
  <div class="card" id="cgThemeCard" data-mod="M14">
    <h2>🎭 题材周期猜想 · 2022→今天 题材七阶段演绎
      <div class="h2right">
        <span class="hint">口径：《龙头股情绪周期教程》第三章 · 启动→确认→发酵→加速→分歧→反包→龙回头（约1周一轮，阶段可跳过）</span>
        <button type="button" class="rf-btn" data-rf="cgtheme" title="重新拉取 cycle_guess.js 并重绘题材周期猜想"><span class="rf-ico">↻</span><span class="rf-tx">刷新</span></button>
      </div>
    </h2>
    <div id="cgThemeBox"><div class="muted">加载中…</div></div>
  </div>

    <!-- 交易记录 -->
    

    <!-- 题材周期 -->
    
  </div>

  <footer data-page-node-id="yVh3PTguw8sk4bNDEU8TXa">数据仅供参考，不构成投资建议 · 情绪数据由自动化每日更新 · 本地数据（交易记录/题材/纪律）保存在浏览器中</footer>
</div>

<script>
/* ============================================================
   手动刷新框架
   - 每个模块向 DASH 注册一个「可重复调用」的 render 函数；
   - 点「刷新」→ 先重新拉取 data.js / cycle.js（带缓存破除参数），
     再重跑该模块；网络不可用时自动降级：script 注入 → 本地缓存。
   ============================================================ */
const fmt = v => (v==null||v===undefined||isNaN(v)) ? '--' : Number(v).toFixed(2);
function getRecs(){
  const S = window.SENTIMENT;
  return (S && S.records) ? S.records.filter(r=>r.median!==null&&r.median!==undefined&&r.median!=="") : [];
}

window.DASH = {
  modules:{}, _loading:null, _toastT:null,
  /* 依赖大文件的模块（ladderhist.js≈931KB / lunar.js≈409KB 已改为按需加载）：
     注册时自动包一层，先确保依赖到位再渲染，避免竞态漏包。 */
  register(id,name,fn){
    var dep = window.__CHUNK_DEP && window.__CHUNK_DEP[id];
    var f = (dep && typeof fn==='function')
      ? function(){ window.ensureChunk(dep, fn); }
      : fn;
    this.modules[id]={name:name,render:f,btn:null};
  },
  /* 按模块 id 触发渲染（走 register 包过的懒加载路径） */
  renderNow(id){ const m=this.modules[id]; if(m && m.render){ try{ m.render(); }catch(e){ console.warn('[renderNow] '+id+':', e); } } },
  toast(msg){
    const t=document.getElementById('dashToast'); if(!t) return;
    t.textContent=msg; t.classList.add('show');
    clearTimeout(this._toastT);
    this._toastT=setTimeout(()=>t.classList.remove('show'), 2200);
  },
  /* 重新加载数据文件；返回 'net'（已取到最新）或 'cache'（沿用内存数据） */
  loadData(){
    if(this._loading) return this._loading;
    const bust='v='+Date.now();
    const loadScript=src=>new Promise((res,rej)=>{
      const s=document.createElement('script'); s.src=src; s.async=false;
      s.onload=()=>res(); s.onerror=()=>rej(new Error('script load fail'));
      document.head.appendChild(s);
    });
    this._loading=(async function(){
      // ① http(s)：fetch + no-store，拿到的一定是最新
      try{
        const core=['./data.js','./cycle.js','./skyladder.js','./ladderhist.js','./mcap_data.js','./earn_core.js','./nodes_core.js','./nodes_data.js'];
        const opt =['./breadth.js','./hotrank.js','./newmod_data.js','./earn_data.js'];      // 新模块的数据，可缺
        const rs=await Promise.all(core.map(u=>fetch(u+'?'+bust,{cache:'no-store'})));
        if(rs.every(r=>r.ok)){
          const txt=await Promise.all(rs.map(r=>r.text()));
          txt.forEach(t=>{ try{ (new Function(t))(); }catch(e0){ console.error('[loadData] 核心文件执行失败', e0); } });
          try{
            const ro=await Promise.all(opt.map(u=>fetch(u+'?'+bust,{cache:'no-store'})));
            for(let i=0;i<ro.length;i++){ if(ro[i].ok) (new Function(await ro[i].text()))(); }
          }catch(e2){}
          return 'net';
        }
      }catch(e){}
      // ② file:// 等 fetch 受限：改用 script 注入（同目录脚本可直接加载）
      try{
        await loadScript('./data.js?'+bust);
        await loadScript('./cycle.js?'+bust);
        await loadScript('./skyladder.js?'+bust);
        await loadScript('./ladderhist.js?'+bust);
        try{ await loadScript('./mcap_data.js?'+bust); }catch(e0a){}
        try{ await loadScript('./earn_core.js?'+bust); }catch(e0b){}
        try{ await loadScript('./breadth.js?'+bust); }catch(e2){}
        try{ await loadScript('./hotrank.js?'+bust); }catch(e3){}
        try{ await loadScript('./newmod_data.js?'+bust); }catch(e4){}
        try{ await loadScript('./earn_data.js?'+bust); }catch(e4b){}
        return 'net';
      }catch(e){}
      // ③ 都失败：沿用当前内存中的数据
      return 'cache';
    })();
    const self=this;
    this._loading.then(()=>{ setTimeout(()=>{ self._loading=null; }, 300); });
    return this._loading;
  },
  _setBusy(btn,on){ if(btn) btn.classList.toggle('busy',!!on); },
  _flash(btn){ if(!btn) return; btn.classList.add('ok'); setTimeout(()=>btn.classList.remove('ok'),1200); },
  _btnOf(id){
    const m=this.modules[id]; if(!m) return null;
    if(!m.btn) m.btn=document.querySelector('.rf-btn[data-rf="'+id+'"]');
    return m.btn;
  },
  refresh(id){
    const m=this.modules[id]; if(!m) return Promise.resolve();
    const btn=this._btnOf(id), self=this;
    this._setBusy(btn,true);
    return this.loadData().then(mode=>{
      m.render();
      self._setBusy(btn,false); self._flash(btn);
      self.toast(m.name+' 已刷新'+(mode==='cache'?'（网络不可用，已用本地缓存）':''));
    }).catch(e=>{
      self._setBusy(btn,false);
      self.toast(m.name+' 刷新失败：'+((e&&e.message)||e));
      console.error('[刷新] '+id, e);
    });
  },
  /* skipLoad=true：数据已由调用方拉好（自动运行引擎），这里只重绘 */
  refreshAll(skipLoad){
    const ids=Object.keys(this.modules), self=this;
    const all=document.querySelectorAll('.rf-btn[data-rf]');
    all.forEach(b=>b.classList.add('busy'));
    const go=mode=>{
      let ok=0;
      ids.forEach(id=>{
        // 只重绘「当前页面上有刷新按钮」的模块（已删除的卡片自动跳过）
        if(!self.modules[id].btn) return;
        try{ self.modules[id].render(); ok++; }catch(e){ console.error('[刷新] '+id+' 失败',e); }
      });
      all.forEach(b=>{ b.classList.remove('busy'); self._flash(b); });
      self.toast('已刷新 '+ok+' 个模块'+(mode==='cache'?'（网络不可用，已用本地缓存）':''));
      return ok;
    };
    if(skipLoad) return Promise.resolve(go('net'));
    return this.loadData().then(go).catch(e=>{
      all.forEach(b=>b.classList.remove('busy'));
      self.toast('刷新失败：'+((e&&e.message)||e));
      console.error('[刷新] 全部刷新失败', e);
    });
  },
  bindButtons(){
    const self=this;
    document.querySelectorAll('.rf-btn[data-rf]').forEach(b=>{
      const id=b.dataset.rf;
      // 记录模块 ↔ 按钮的对应关系（「全部刷新」据此只重绘当前可见的模块）
      if(id!=='__all' && this.modules[id]) this.modules[id].btn=b;
      b.addEventListener('click', function(ev){
        ev.preventDefault();
        if(id==='__all') self.refreshAll(); else self.refresh(id);
      });
    });
  }
};

/* ---------- KPI ---------- */
function stageOf(m){
  if(m==null) return {t:'待更新',c:''};
  if(m>=2) return {t:'情绪高潮 🔥',c:'hot'};
  if(m>=0.8) return {t:'发酵期',c:'warm'};
  if(m>=0) return {t:'混沌/分歧',c:'cool'};
  return {t:'退潮期 ❄️',c:'cold'};
}
function renderKpis(){
  const box = document.getElementById('kpis'); if(!box) return;
  const recs = getRecs();
  const last = recs.length ? recs[recs.length-1] : null;
  const med20 = recs.slice(-20).map(r=>r.median);
  const avg = med20.length ? med20.reduce((a,b)=>a+b,0)/med20.length : null;
  const st = stageOf(last?last.median:null);
  const cls = last ? (last.median>0.02?'up':last.median<-0.02?'down':'flat') : '';
  box.innerHTML = `
    <div class="kpi"><div class="label">最新交易日</div><div class="value" style="font-size:18px">${last?last.date:'--'}</div><div class="note">情绪阶段：${st.t}</div></div>
    <div class="kpi"><div class="label">竞价涨幅中位数(%)</div><div class="value ${cls}">${last?fmt(last.median):'--'}</div><div class="note">昨日涨停股今开 vs 昨收</div></div>
    <div class="kpi"><div class="label">总体标准差(%)</div><div class="value">${last?fmt(last.std):'--'}</div><div class="note">分歧度：越大越分化</div></div>
    <div class="kpi"><div class="label">近20日中位数均值(%)</div><div class="value ${avg>0?'up':'down'}">${fmt(avg)}</div><div class="note">样本 ${med20.length} 个交易日</div></div>`;
  const ud = document.getElementById('updateDate');
  if(ud) ud.textContent = '数据更新于 ' + ((window.SENTIMENT&&window.SENTIMENT.updatedAt)||'--');
}
window.DASH.register('kpi','情绪概览 KPI', renderKpis);
renderKpis();

/* ---------- 主图：竞价中位数 / 总体标准差已并入下方「市场情绪双图」
   （见 renderMood：上格 ADJS 柱、下格 中位数线，共用一条时间轴与缩放）---------- */

/* ---------- 竞价明细 ---------- */
function renderDetail(){
  const d = window.SENTIMENT ? window.SENTIMENT.lastDetail : null;
  const tb = document.querySelector('#detailTable tbody');
  const hint = document.getElementById('detailHint');   // 卡片若被删除则为 null，直接跳过
  if(!tb || !hint) return;
  if(!d || !d.detail || !d.detail.length){
    hint.textContent = '（交易日 9:25 后自动写入）';
    tb.innerHTML = '<tr><td colspan="3" style="color:var(--sub)">暂无明细，等每日自动化写入</td></tr>';
    return;
  }
  hint.textContent = d.date + ' · 样本 ' + d.detail.length + ' 只';
  tb.innerHTML = d.detail.map(s=>{
    const c = s.gap>0.02?'pos':s.gap<-0.02?'neg':'';
    return `<tr><td>${s.code}</td><td>${s.name}</td><td class="num ${c}">${fmt(s.gap)}</td></tr>`;
  }).join('');
}
window.DASH.register('detail','今日竞价明细', renderDetail);
renderDetail();

/* ---------- 连板梯队排序图（按「竞价实际换手率」降序） ---------- */
function renderLadder(){
  const el = document.getElementById('ladderChart'); if(!el) return;
  const tb = document.querySelector('#ladderTable tbody');
  const sel = document.getElementById('ladderDate');
  if(!el) return;

  const fmtInt = v => (v==null || v===undefined || isNaN(v))
    ? '--' : Number(v).toLocaleString('zh-CN',{maximumFractionDigits:0});
  const sgn = v => (v>0?'+':'') + fmt(v);

  // 历史快照 = 回填历史(LADDERHIST，优先) + ladderHistory + 当前 ladder
  const H = (window.LADDERHIST && window.LADDERHIST.days) || {};
  const snaps = Object.keys(H).map(k=>H[k]).filter(s=>s && s.items && s.items.length);
  const seen = {};
  snaps.forEach(s=>{ seen[s.date]=1; });
  (window.SENTIMENT.ladderHistory || []).forEach(s=>{
    if (s && s.items && s.items.length && !seen[s.date]){ snaps.push(s); seen[s.date]=1; }
  });
  const cur0  = window.SENTIMENT.ladder;
  if (cur0 && cur0.items && cur0.items.length && !seen[cur0.date]) snaps.push(cur0);

  /* 今天若是交易日、但 9:25 竞价还没抓（此刻多半如此），补一个「待更新」占位：
     这样日期下拉里就有今天可选；选中后给出明确提示，而不是整个模块空着或下拉里根本没有这一项。
     收盘后 data.js 写入当天数据，占位会被真实快照自动顶替。 */
  const MC = window.MarketCal;
  const today = MC ? MC.ymd(new Date()) : '';
  if (today && MC && MC.isTradingDay(today) && !seen[today]) snaps.push({date:today, items:[], pending:true});
  snaps.sort((a,b)=> a.date < b.date ? -1 : 1);

  // 回填覆盖度说明
  const note = document.getElementById('ladderNote');
  if (note){
    const hk = Object.keys(H).sort();
    const nb = snaps.filter(s=>H[s.date]).length;
    note.innerHTML = hk.length
      ? ('已回填历史 <b>' + nb + '</b> 个交易日（' + hk[0] + ' ~ ' + hk[hk.length-1] + '）'
         + ' · 回填口径：当日收盘封板且连板≥2 的主板股及其当日竞价快照；10-14 起为 9:25 实时快照口径')
      : '暂无回填历史数据，下拉仅显示实时快照日期';
  }

  // 复用/重建实例，避免 _echarts_instance_ 残留导致空白
  let chart = echarts.getInstanceByDom(el);
  if (chart) chart.dispose();
  el.removeAttribute('_echarts_instance_');
  el.innerHTML = '';
  chart = echarts.init(el);

  // 默认展示最新「有数据」的快照；今天若尚未抓取就落到昨天，避免开屏就是一张空图
  const withData0 = snaps.filter(s=>s.items && s.items.length);
  let cur = withData0.length ? withData0[withData0.length-1] : (snaps.length ? snaps[snaps.length-1] : null);

  function draw(){
    if (!cur || !cur.items || !cur.items.length){
      chart.clear();
      const dTxt = (cur && cur.date) ? cur.date : '';
      const tip  = dTxt ? (dTxt + ' 的竞价数据将在 09:25 开盘后自动抓取更新')
                        : '暂无数据，交易日 9:25 后自动更新';
      chart.setOption({title:{text:tip,
        left:'center',top:'middle',
        textStyle:{color:'#7a8194',fontSize:13,fontWeight:'normal'}}});
      if (tb) tb.innerHTML = '<tr><td colspan="10" style="color:var(--sub)">'
        + (dTxt ? dTxt + ' 待更新 · 交易日 09:25 开盘后自动写入（当前非交易时段或尚未到竞价）'
                : '暂无数据，交易日 9:25 后自动写入') + '</td></tr>';
      return;
    }

    // ---- 排序：竞价实际换手率 降序；缺数据排最后 ----
    const items = cur.items.slice()
      .filter(x => x && x.code)
      .sort((a,b)=>{
        const av = (a.turnover==null ? -1 : a.turnover);
        const bv = (b.turnover==null ? -1 : b.turnover);
        return bv - av;
      });

    const hasTO = items.some(x => x.turnover != null);
    const maxTO = items.reduce((m,x)=>Math.max(m, x.turnover||0), 0);
    const longName = Math.max(0, ...items.map(x => (x.name||'').length));

    // ---- 自适应高度 + 纵向缩放/滑动（窄屏少显示几行） ----
    const ROW = 26, VIS = (el.clientWidth||600) < 560 ? 10 : 14;
    el.style.height = (Math.min(items.length, VIS) * ROW + 78) + 'px';
    chart.resize();

    const endPct = items.length <= VIS ? 100 : (VIS / items.length * 100);

    chart.setOption({
      backgroundColor:'transparent',
      title:{text: cur.date + ' · 连板梯队（' + items.length + ' 只）',
             subtext: hasTO ? '条形＝竞价实际换手率（竞价量×100 ÷ 实际流通股＝自由流通股，＝通达信 CAKPHSZ）　标签＝「换手率 / 竞价涨幅」　悬停可见股数换算与金额口径对照' : '（暂无竞价换手数据）',
             left:'center', top:0,
             textStyle:{fontSize:12,color:'#7a8194',fontWeight:'normal',overflow:'truncate',width:'90%'},
             subtextStyle:{fontSize:11,color:'#9aa1b1',overflow:'truncate',width:'90%'}},
      tooltip:{trigger:'item', formatter:p=>{
        const it = items[p.dataIndex];
        if(!it) return '';
        const col = it.gap>0.02 ? '#d93a3a' : it.gap<-0.02 ? '#1a9c6b' : '#98a0b3';
        return '<div style="font-size:13px">'
          + '<b>' + it.name + '</b>（' + it.code + '） <b>' + it.boards + '</b> 连板<br>'
          + '竞价实际换手 <b>' + (it.turnover!=null ? fmt(it.turnover)+'%' : '--') + '</b>'
          + '<span style="opacity:.7">＝竞价量(手)×100 ÷ 实际流通股(股数)　<span style="opacity:.8">（＝通达信 CAKPHSZ）</span></span><br>'
          + '竞价量 ' + fmtInt(it.volAuc) + ' 手　→　' + fmtInt((it.volAuc||0)*100) + ' 股<br>'
          + '实际流通股 ' + fmtInt(it.freeShares) + ' 股<br>'
          + '竞价成交金额 ' + (it.amount!=null ? fmt((it.amount/1e4).toFixed(0))+' 万元' : '--') + '<br>'
          + '实际流通市值 ' + (it.freeMv!=null ? fmt(it.freeMv)+' 亿元' : '--') + '<br>'
          + '竞价涨幅 <b style="color:' + col + '">' + sgn(it.gap) + '%</b><br>'
          + '昨日成交量 ' + fmtInt(it.prevVol) + ' 手<br>'
          + (it.turnoverAmt!=null ? '<span style="opacity:.75">对照·金额口径 ' + fmt(it.turnoverAmt) + '%（竞价金额÷实际流通市值）</span><br>' : '')
          + '板型 ' + (it.boardType || '-')
          + (it.theme ? '<br>题材：' + it.theme : '')
          + '</div>';
      }},
      grid:(function(){
        const cw = el.clientWidth || 600, narrow = cw < 560;
        return { left:  Math.min(150, Math.max(56, Math.min(cw*0.34, 46 + longName*13))),
                 right: narrow ? 96 : 168, top: 46, bottom: 30 };
      })(),
      xAxis:{type:'value', name:'竞价换手%', nameLocation:'middle', nameGap:25,
             nameTextStyle:{fontSize:11,color:'#7a8194'},
             axisLabel:{formatter:'{value}%'},
             splitLine:{lineStyle:{color:'#eef0f5'}},
             max: maxTO ? Math.ceil(maxTO*1.28*10)/10 : 1},
      yAxis:{type:'category', inverse:true,
             data: items.map(x => x.name + ' ' + x.boards + '板'),
             axisLabel:{fontSize:12},
             axisTick:{show:false}},
      dataZoom: items.length > VIS ? [
        {type:'inside', yAxisIndex:0, start:0, end:endPct,
         zoomOnMouseWheel:true, moveOnMouseMove:true, moveOnMouseWheel:false},
        {type:'slider', yAxisIndex:0, start:0, end:endPct,
         width:12, right:6, top:56, bottom:44,
         borderColor:'transparent', backgroundColor:'#f2f4f8',
         fillerColor:'rgba(47,107,255,.16)',
         handleStyle:{color:'#2f6bff'}, showDetail:false}
      ] : [],
      series:[{
        type:'bar',
        barMaxWidth:20,
        data: items.map(x=>({
          value: x.turnover!=null ? x.turnover : 0,
          itemStyle:{color: x.gap>0.02 ? '#d93a3a' : x.gap<-0.02 ? '#1a9c6b' : '#98a0b3',
                    borderRadius:[0,4,4,0]}
        })),
        label:{show:true, position:'right', fontSize:11, color:'#4a5163',
               formatter:p=>{
                 const it=items[p.dataIndex]; if(!it) return '';
                 const to = it.turnover!=null ? fmt(it.turnover)+'%' : '--';
                 // 窄屏缩短标签，避免挤出绘图区
                 return (el.clientWidth||600) < 560 ? (to+' ·'+sgn(it.gap)+'%') : (to + '  /  涨幅 ' + sgn(it.gap) + '%');
               }}
      }]
    }, true);

    // ---- 表格 ----
    if (tb){
      tb.innerHTML = items.map(x=>{
        const c = x.gap>0.02 ? 'pos' : x.gap<-0.02 ? 'neg' : '';
        const eg = (window.M5 && window.M5.energyHtml) ? window.M5.energyHtml(x.code) : '';
        return '<tr><td>' + x.code + '</td><td>' + eg + '</td><td>' + x.name + '</td><td>' + x.boards + '板</td>'
          + '<td class="num ' + c + '">' + fmt(x.gap) + '</td>'
          + '<td class="num">' + (x.turnover!=null ? fmt(x.turnover) : '--') + '</td>'
          + '<td class="num">' + fmtInt(x.volAuc) + '</td>'
          + '<td class="num">' + fmtInt(x.prevVol) + '</td>'
          + '<td>' + (x.boardType||'') + '</td>'
          + '<td>' + (x.theme||'') + '</td></tr>';
      }).join('');
    }
  }

  if (snaps.length && sel){
    sel.innerHTML = snaps.map(s=>'<option value="'+s.date+'">'+s.date+(s.pending?'（待更新）':'')+'</option>').join('');
    sel.value = cur ? cur.date : snaps[snaps.length-1].date;
    sel.onchange = e => { cur = snaps.find(s=>s.date===e.target.value); draw(); };
  }
  draw();
  // resize 只绑一次（刷新时重复绑定会叠加监听）
  if(!el._wbResize){ el._wbResize=1; window.addEventListener('resize', ()=>{ const c=echarts.getInstanceByDom(el); if(c) c.resize(); }); }
}
window.DASH.register('ladder','连板梯队排序', renderLadder);
/* 立即调用走同一套懒加载路径（依赖 ladderhist.js，首屏未加载） */
if(window.DASH && DASH.renderNow) { try{ DASH.renderNow('ladder'); }catch(e){} }

/* ---------- 天量扫描 ---------- */
function renderVolume(){
  const V = window.SENTIMENT ? window.SENTIMENT.volume : null;
  const actualF = v => actualFloat({ code:v.code, freeMv:v.freeMv, floatMv:v.floatMv,
    vol:v.vol, turnover:v.turnover, close:v.close, date:(V&&V.date)||null });
  const sum = document.getElementById('volSummary');
  const tb = document.querySelector('#volTable tbody');
  if(!sum || !tb) return;
  const fmtInt = v => (v==null||isNaN(v)) ? '--' : Number(v).toLocaleString('zh-CN',{maximumFractionDigits:0});
  if(!V || !V.hits){
    sum.textContent = '暂无数据，交易日收盘后 15:05 自动扫描';
    tb.innerHTML = '<tr><td colspan="12" style="color:var(--sub)">暂无数据</td></tr>';
    return;
  }
  /* 放宽口径后按「历史排名」优先，同排名内按放量倍数（旧数据无 volRank 时视为创新高） */
  const gr = x => (x.volRank==null ? 1 : x.volRank);
  const hits = V.hits.slice().sort((a,b)=> (gr(a)-gr(b)) || (b.ratio-a.ratio));
  const n1 = hits.filter(x=>gr(x)===1).length,
        n2 = hits.filter(x=>gr(x)===2).length,
        n3 = hits.filter(x=>gr(x)===3).length;
  const mix = '（<b style="color:var(--up)">创上市新高 '+n1+'</b>'
    + (n2?' · <b style="color:#a35b00">历史第2大量 '+n2+'</b>':'')
    + (n3?' · <b style="color:#3f51a8">历史第3大量 '+n3+'</b>':'') + '）';
  sum.innerHTML = V.date + ' · 今日主板涨停 <b>' + V.total + '</b> 只，其中 <b style="color:var(--up)">'
    + hits.length + '</b> 只放量进入上市以来前三大成交量 ' + mix;
  if(!hits.length){
    tb.innerHTML = '<tr><td colspan="12" style="color:var(--sub)">今日无主板涨停股放量进入上市以来前三大成交量</td></tr>';
    return;
  }
  tb.innerHTML = hits.map(x=>{
    const eg = (window.M5 && window.M5.energyHtml) ? window.M5.energyHtml(x.code) : '';
    const rk = gr(x);
    const note = x.volNote || (rk===1?'历史天量·创上市新高':rk===2?'历史第2大量':'历史第3大量');
    const bd = x.daysBoards || (x.boards!=null ? x.boards+'板' : '--');
    return `<tr>
    <td>${x.code}</td>
    <td class="v-nm">${x.name}${eg?'<div class="ea-eg">'+eg+'</div>':''}</td>
    <td>${bd}</td>
    <td class="num">${x.close!=null?x.close:'--'}</td>
    <td class="num ${x.chg>0?'pos':x.chg<0?'neg':''}">${fmt(x.chg)}</td>
    <td class="num">${x.turnover!=null?fmt(x.turnover):'--'}</td>
    <td class="num">${fmtInt(x.vol)}</td>
    <td class="num">${fmtInt(x.prevMaxVol)}</td>
    <td class="num">${actualF(x)!=null?fmt(actualF(x)):'--'}</td>
    <td>${x.boardType||''}</td><td class="v-theme">${x.theme||''}</td>
    <td><span class="v-note r${rk}">${note}</span></td>
  </tr>`;}).join('');
}
window.DASH.register('volume','天量扫描', renderVolume);
renderVolume();

/* ---------- 市值取值唯一入口：实际流通市值（口径唯一定义在 mcap_data.js）----------
   优先级：① iFinD 实际流通市值 → ② 连板梯队快照自带的同口径市值（freeMv）
          → ③ 派生：成交量÷换手率×价（与 freeMv 同口径）→ ④ 当日快照兜底 → 空
   注意：连板天梯看的是「历史某一天」，此时快照值与该日价格同源，故 prefer='snapshot'。 */
function actualFloat(o){
  o = o || {};
  const code = String(o.code||'');
  const ff   = (window.MCAP && window.MCAP.ff) ? window.MCAP.ff(code) : null;
  const snap = (o.freeMv!=null) ? Number(o.freeMv) : (o.floatMv!=null ? Number(o.floatMv) : null);
  if(o.prefer==='snapshot' && snap!=null) return snap;
  if(ff!=null) return ff;
  if(snap!=null) return snap;
  if(window.MCAP && window.MCAP.fromTurnover){
    const d = window.MCAP.fromTurnover(o.vol, o.turnover, o.close);
    if(d!=null) return d;
  }
  const LH = window.LADDERHIST, dd = o.date;
  if(LH && dd && LH.days && LH.days[dd]){
    const it = (LH.days[dd].items||[]).find(i=>i.code===code.slice(0,6));
    if(it && it.freeMv!=null) return it.freeMv;
  }
  return null;
}

/* ---------- 连板天梯（收盘快照：身位分组 + 封板筛选 + 涨停原因） ---------- */
var skyOnlySealed = true;   // 默认只看封板（在连股票），去掉断板
function skyTierName(b){ return b>=4 ? '高身位' : (b===3 ? '中身位' : '低身位'); }
function skyBdCls(b){ return b>=4 ? 'b4' : (b===3 ? 'b3' : (b===2 ? 'b2' : 'bx')); }
function skyRowHtml(x){
  const c = x.chg>0.02 ? 'pos' : x.chg<-0.02 ? 'neg' : '';
  const actualF = v => actualFloat({ code:v.code, freeMv:v.freeMv, date:skyDate, prefer:'snapshot' });
  const eg = (window.M5 && window.M5.energyHtml) ? window.M5.energyHtml(x.code) : '';
  let badge;
  if(x.sealed)         badge = '<span style="color:var(--up);font-weight:700">封板</span>';
  else if(x.chg<=-9.8) badge = '<span style="color:var(--down);font-weight:700">跌停</span>';
  else                 badge = '<span style="color:#98a0b3">断板</span>';
  const b = x.boards||0;
  return '<tr><td>' + x.code + '</td><td>' + eg + '</td><td>' + x.name + '</td>'
    + '<td class="num"><span class="sky-bd ' + skyBdCls(b) + '">' + (x.boards||'--') + '板</span></td>'
    + '<td>' + badge + '</td>'
    + '<td class="num ' + c + '">' + fmt(x.chg) + '</td>'
    + '<td class="num">' + (x.close!=null ? x.close : '--') + '</td>'
    + '<td class="num">' + (actualF(x)!=null ? fmt(actualF(x)) : '--') + '</td>'
    + '<td style="white-space:normal">' + (x.reason||'') + '</td></tr>';
}
var skyDate = null;     // 当前查看日期，null=最新
function renderSkyLadder(){
  const sum = document.getElementById('skySummary');
  const dt  = document.getElementById('skyDate');
  const tb  = document.querySelector('#skyTable tbody');
  const tgl = document.getElementById('skyOnlySealBtn');
  const sel = document.getElementById('skyDateSel');
  const nte = document.getElementById('skyNote');
  if(!tb) return;
  // 切换按钮：绑定一次 + 标签同步
  if(tgl){
    const tx = tgl.querySelector('.rf-tx');
    if(tx) tx.textContent = skyOnlySealed ? '只看封板' : '含断板';
    tgl.classList.toggle('on', skyOnlySealed);
    tgl.title = skyOnlySealed ? '当前只显示封板股，点击切换为含断板' : '当前含断板股，点击切换为只看封板';
    if(!tgl._wbBound){ tgl._wbBound=1; tgl.addEventListener('click', function(ev){
      ev.preventDefault(); skyOnlySealed=!skyOnlySealed; renderSkyLadder(); }); }
  }
  const S = window.SKYLADDER;
  const H = (window.LADDERHIST && window.LADDERHIST.days) || {};
  const hDates = Object.keys(H).filter(k=>H[k] && H[k].items && H[k].items.length).sort();
  // 可选日期 = 回填历史 + 手工快照日期（去重）
  let allDates = (S && S.date && hDates.indexOf(S.date) < 0) ? hDates.concat([S.date]).sort() : hDates;
  /* 天梯是收盘快照：今天若是交易日但尚未收盘（15:01 才生成），补一个「待更新」占位，
     让下拉能选到今天并给出明确提示，而不是缺这一项或选中后一片空白 */
  const MC = window.MarketCal;
  const today = MC ? MC.ymd(new Date()) : '';
  const hasData = d => !!((H[d] && H[d].items && H[d].items.length) || (S && S.date===d && S.items && S.items.length));
  if (today && MC && MC.isTradingDay(today) && allDates.indexOf(today) < 0) allDates = allDates.concat([today]).sort();

  if(!allDates.length){
    if(sum) sum.textContent = '暂无数据';
    if(dt) dt.textContent = '--';
    if(nte) nte.textContent = '';
    tb.innerHTML = '<tr><td colspan="9" style="color:var(--sub)">暂无连板天梯数据</td></tr>';
    return;
  }

  // 下拉框：日期数量变化时重建；默认选最新日期
  if(sel){
    /* 重建条件用「日期+该日是否有数据」的签名，而不只是日期个数：
       今天以待更新占位已在列表里，数据到达后日期个数不变，只看个数会导致「（待更新）」标记残留不消失 */
    const selSig = allDates.map(d=>d+':'+(hasData(d)?1:0)).join('|');
    if(!sel._wbSig || sel._wbSig !== selSig){
      sel.innerHTML = allDates.slice().reverse().map(d=>'<option value="'+d+'">'+d+((today&&d===today&&!hasData(d))?'（待更新）':'')+'</option>').join('');
      sel._wbSig = selSig;
    }
    if(!skyDate || allDates.indexOf(skyDate) < 0) skyDate = allDates[allDates.length-1];
    sel.value = skyDate;
    if(!sel._wbBound){ sel._wbBound=1; sel.addEventListener('change', function(e){
        skyDate = e.target.value; renderSkyLadder(); }); }
  }
  if(dt) dt.textContent = skyDate + (S && S.date===skyDate && S.updated ? '（'+S.updated.slice(5)+'）' : '');
  if(nte) nte.innerHTML = '回填覆盖 <b>' + hDates.length + '</b> 个交易日'
    + (hDates.length ? '（' + hDates[0] + ' ~ ' + hDates[hDates.length-1] + '）' : '')
    + ' · 回填口径：当日收盘封板且连板≥2 的主板股（天然不含断板）';

  // 当日数据：回填历史优先，回退手工快照 SKYLADDER
  let items = [];
  if(H[skyDate])                       items = H[skyDate].items.map(x=>Object.assign({},x));
  else if(S && S.date===skyDate && S.items) items = S.items.map(x=>Object.assign({},x));
  // 手工快照里人工整理过的涨停原因优先（按代码覆盖）
  if(S && S.items && S.items.length){
    const m = {}; S.items.forEach(x=>{ if(x && x.code) m[x.code]=x; });
    items.forEach(x=>{ const c = m[x.code]; if(c){ if(c.reason) x.reason=c.reason; if(c.theme) x.theme=c.theme; } });
  }
  if(!items.length){
    var skyPend = (today && skyDate===today);
    if(sum) sum.innerHTML = skyPend
      ? (skyDate + ' · <b>待更新</b> · 收盘后 15:01 自动生成连板天梯')
      : (skyDate + ' · 当日无连板封板股');
    tb.innerHTML = '<tr><td colspan="9" style="color:var(--sub)">' + (skyPend
      ? (skyDate + ' 收盘快照待生成 · 交易日 15:01 收盘后自动写入，无需手动刷新')
      : (skyDate + ' 无符合条件的连板股')) + '</td></tr>';
    return;
  }

  // 排序：连板数降序 → 涨幅降序
  items.sort((a,b)=>{
    if((b.boards||0) !== (a.boards||0)) return (b.boards||0) - (a.boards||0);
    return (b.chg||0) - (a.chg||0);
  });
  const nSeal = items.filter(x=>x.sealed).length;
  const nBrk = items.length - nSeal;

  // 筛选：默认去掉断板，只留在连股票
  const shown = skyOnlySealed ? items.filter(x=>x.sealed) : items;

  // 汇总：按身位梯队统计
  const tiers = [];
  shown.forEach(x=>{ const b=x.boards||0;
    let t=tiers.find(t=>t.b===b); if(!t){ t={b:b,n:0,brk:0}; tiers.push(t); }
    t.n++; if(!x.sealed) t.brk++; });
  tiers.sort((a,b)=>b.b-a.b);
  if(sum) sum.innerHTML = skyDate + ' · 在连封板 <b style="color:var(--up)">' + nSeal + '</b> 只'
    + (tiers.length ? '（' + tiers.map(t=>'<b>'+t.b+'板×'+t.n+'</b>').join(' · ') + '）' : '')
    + (skyOnlySealed && nBrk ? ' · 已隐藏断板 <b>'+nBrk+'</b> 只' : (nBrk ? ' · 含断板 <b>'+nBrk+'</b> 只' : ''));

  // 按身位分组渲染，组间用分组行隔开
  let html = '';
  tiers.forEach(t=>{
    const list = shown.filter(x=>(x.boards||0)===t.b);
    html += '<tr class="sky-tier"><td colspan="9"><span class="sky-tier-b">'
      + skyTierName(t.b) + ' · ' + t.b + ' 板</span> · ' + t.n + ' 只'
      + (t.brk ? '（含断板 '+t.brk+'）' : '（全部封板）') + '</td></tr>';
    html += list.map(skyRowHtml).join('');
  });
  tb.innerHTML = html || '<tr><td colspan="9" style="color:var(--sub)">当前无封板连板股'
    + (nBrk ? '（可点「含断板」查看 '+nBrk+' 只断板股）' : '') + '</td></tr>';
}
window.DASH.register('skyladder','连板天梯', renderSkyLadder);

/* ============================================================
   复盘计划及交易预警：按《提取自卡位统计》的两套模式筛选
   —— 换手卡位模式 / 最高标模式
   ============================================================ */
var alertDate = null, alertStrict = true, alertShowC = false, alertView = 'split';

function alertItemsOf(d){
  var H=(window.LADDERHIST&&window.LADDERHIST.days)||{};
  if(H[d]&&H[d].items&&H[d].items.length) return H[d].items;
  var S=window.SENTIMENT;
  if(S&&S.ladder&&S.ladder.date===d&&S.ladder.items&&S.ladder.items.length) return S.ladder.items;
  return [];
}
/* 用「天量扫描」结果补充 天量弱转强 模式所需字段：
   volume.date 与 ladder.date 同日时，volume 是该日收盘后的天量命中表，正好是次日竞价要参考的「前一日天量」 */
function alEnrich(items, alertDate){
  var S=window.SENTIMENT, A=window.ALERT;
  if(!S) return items;
  /* ★ 天量弱转强是「天量的次日」做：第 D 日的预警，必须取【D 之前】那一份天量扫描。
     取同日扫描＝把「次日做」错做成「当日做」，2026-10-15 用户纠偏后固化为唯一口径。 */
  var V=null;
  if(A&&A.volumeBefore) V=A.volumeBefore(S.volumeHistory, alertDate, S.volume);
  else V=S.volume;
  if(!V||!V.hits||!V.hits.length) return items;
  var m={}; V.hits.forEach(function(h){ m[h.code]=h; });
  items.forEach(function(it){
    var h=m[it.code];
    if(h){
      /* 天量窗口：命中「天量扫描」（上市以来前三大成交量）即算天量；保留 volRank/备注供展示 */
      if(h.volRank!=null) it.isTianLiang = true;
      else if(h.vol!=null&&h.prevMaxVol!=null) it.isTianLiang = (h.vol>=h.prevMaxVol);
      if(h.volRank!=null) it.volRank = h.volRank;
      if(h.volNote) it.tianLiangNote = h.volNote;
      if(h.ratio!=null) it.volRatio = h.ratio;
      it.prevMaxVol = h.prevMaxVol;
      it.tianLiangVol = h.vol;
      if(h.close!=null) it.tianLiangClose = h.close;      /* 博弈量能平台 */
      it.tianLiangDate = V.date;
    }
  });
  return items;
}
function esc2(s){ return String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }

/* ---------- 警示节点（小周期节点 / 趋势节点 / 双重节点）----------
   定义与判定口径全部固化在 nodes_core.js（Node 端 _nodes.js 收盘生成 nodes_data.js），
   视图层只负责「取标签 + 贴到股票名称旁边」，绝不在这里重算一遍。 */
function nodeData(){ return window.NODES || null; }
/* 取某只票「仍在跟踪」的节点标签数组（大跌过的不返回 → 标签已被摘掉） */
function nodeTagsOf(code){
  var N=nodeData(); if(!N||!code) return [];
  var s=N.stocks&&N.stocks[code];
  return (s&&s.alive)?(s.tags||[]):[];
}
function nodeCls(tags){
  if(!tags||!tags.length) return '';
  if(window.NodesCore&&window.NodesCore.classOf) return window.NodesCore.classOf(tags);
  return tags.indexOf('双重节点')>=0?'dual':(tags.indexOf('小周期节点')>=0?'xiao':(tags.indexOf('趋势节点')>=0?'trend':(tags.indexOf('节气节点')>=0?'jieqi':'gann')));
}
function nodeTagHtml(tags){
  if(typeof tags==='string') tags=tags?tags.split(' · '):[];
  if(!tags||!tags.length) return '';
  var txt=tags.join(' · ');
  return '<span class="al-node '+nodeCls(tags)+'" title="警示节点：'+esc2(txt)+'（该股首板落在节点日，收盘刷新后加标签；一旦大跌即摘掉）">'+esc2(txt)+'</span>';
}
/* 数字能量小标签：统一走 wuxing5.js 的 M5.energyHtml（与连板梯队 / 连板天梯 / 龙头画像 同口径，本层不另算）。
   用户口径：数字能量作为「标签」挂在股票名称下面（警示节点标签仍留在名称旁边，两者并存）。
   密集面板用 compact 呈现（色点+星名+五行），完整「数对·星」读法在 hover 的 title 里。 */
function energyOf(code){ return (window.M5 && window.M5.energyHtml) ? window.M5.energyHtml(code,{compact:true}) : ''; }
function energyUnder(code){
  var h=energyOf(code);
  return h ? ('<span class="al-eg">'+h+'</span>') : '';
}
/* 给预警样本贴节点标签（挂在 it.nodeTags） */
function alNodes(items, d){
  var N=nodeData(); if(!N) return items;
  (items||[]).forEach(function(it){
    var t=nodeTagsOf(it.code);
    if(t.length){ it.nodeTags=t; if(N.stocks&&N.stocks[it.code]) it.nodeDate=N.stocks[it.code].date; }
  });
  return items;
}
/* 警示节点状态条：参考日 5 类节点判定 + 最近一次节点首板 + 仍在跟踪的节点票（大跌已摘标签） */
function renderAlertNodes(){
  var box=document.getElementById('alertNodes'); if(!box) return;
  var N=nodeData(), S=window.SENTIMENT;
  if(!N || !N.days){ box.innerHTML=''; return; }
  var refDate=(S&&S.limitUp&&S.limitUp.date)?S.limitUp.date:(N.latest&&N.latest.date);
  var ref=N.days[refDate]||null;
  var big=(N.params&&N.params.bigDrop!=null)?N.params.bigDrop:-5;
  var chip=function(on,cls,label,txt,title){
    return '<span class="aln-chip '+(on?('on '+cls):'')+'"'+(title?' title="'+esc2(title)+'"':'')+'>'
      +'<i class="aln-ic">'+(on?'✓':'✗')+'</i><b>'+label+'</b>'
      +(txt?'<em>'+esc2(txt)+'</em>':'')+'</span>';
  };
  var chips='<div class="aln-chips">'
    +chip(ref&&ref.xiao,'xiao','小周期节点',
      ref?(ref.xiao?('断板 '+ref.prevMax+' → '+ref.nowMax+' 板'):('最高 '+ref.maxB+' 板 未断')):'无数据',
      '最高连板断板当日/次日的首板＝小周期节点')
    +chip(ref&&ref.trend,'trend','趋势节点',
      (ref&&ref.trendOf!=null)?('前五收绿 '+ref.trendDown+'/'+ref.trendOf):'',
      '30 日涨幅前五收盘批量负反馈（≥3 只收绿）当日的首板＝趋势节点')
    +chip(ref&&ref.dual,'dual','双重节点','小周期 + 趋势同日','小周期节点与趋势节点落在同一天＝双重节点')
    +chip(ref&&ref.jieqi,'jieqi','节气节点',
      (ref&&ref.jieqiNames&&ref.jieqiNames.length)?ref.jieqiNames.join('/'):'','二十四节气当日首板＝节气节点')
    +chip(ref&&ref.gann,'gann','江恩节点',
      (ref&&ref.gannDeg&&ref.gannDeg.length)?(ref.gannDeg.join('/')+'°'):'','江恩 8 方角时间窗当日首板＝江恩节点')
    +'</div>';

  var L=(N.latest&&N.latest.lastNode)||null;
  var lastHtml='';
  if(L){
    var why=[];
    if(L.xiao) why.push('最高板断板 '+L.prevMax+'→'+L.nowMax+' 板');
    if(L.trend&&L.trendDown!=null) why.push('前五收绿 '+L.trendDown+'/'+L.trendOf);
    if(L.jieqi) why.push('节气 '+((L.jieqiNames||[]).join('/')));
    if(L.gann) why.push('江恩 '+((L.gannDeg||[]).join('/'))+'°');
    var fb=(L.firstBoards||[]);
    var lt=(L.tags&&L.tags.length)?L.tags:[L.tag];
    lastHtml='<div class="aln-sec"><b>最近一次警示节点</b><em>节点日的首板即当天的「节点票」</em></div>'
      +'<div class="aln-hero"><span class="aln-hero-d">'+esc2(L.date)+'</span>'
      +'<span class="al-node '+nodeCls(lt)+'">'+esc2(L.tag)+'</span>'
      +(why.length?'<span class="aln-hero-w">'+esc2(why.join('；'))+'</span>':'')
      +'<span class="aln-hero-c">节点首板 '+fb.length+' 只</span></div>'
      +(fb.length?('<div class="aln-grid">'+fb.map(function(x){
          var s2=N.stocks&&N.stocks[x.code];
          var mk=(s2&&!s2.alive)?'<e class="aln-gone" title="该票随后出现大跌（收盘 ≤ '+big+'%），节点标签已摘掉">已摘</e>':'';
          return '<span class="aln-stk one"><b>'+esc2(x.name)+'</b><i>'+esc2(x.code)+'</i>'+mk+'</span>';
        }).join('')+'</div>'):'<div class="aln-empty">该节点当日无首板</div>');
  }

  var alive=(N.alive||[]);
  /* 排序：双重 → 小周期 → 趋势 → 节气 → 江恩；同类按节点日倒序 */
  var ORD=(window.NodesCore&&window.NodesCore.ORDER)||['双重节点','小周期节点','趋势节点','节气节点','江恩节点'];
  var ordOf=function(tags){ var m=99; (tags||[]).forEach(function(t){ var i=ORD.indexOf(t); if(i>=0&&i<m) m=i; }); return m; };
  alive=alive.slice().sort(function(a,b){
    return (ordOf(a.tags)-ordOf(b.tags)) || (a.date<b.date?1:(a.date>b.date?-1:0));
  });
  var aliveHtml='<div class="aln-sec"><b>在跟踪的节点票 '+alive.length+' 只</b>'
    +'<em>收盘刷新即加标签 · 一旦出现大跌（收盘 ≤ '+big+'%）立刻摘掉 · 只跟踪最近 40 个交易日</em></div>'
    +(alive.length?('<div class="aln-grid">'+alive.map(function(x){
        return '<span class="aln-stk">'
          +'<span class="aln-l1"><b>'+esc2(x.name)+'</b><i>'+esc2(x.code)+'</i>'+nodeTagHtml(x.tags)+'</span>'
          +energyUnder(x.code)+'</span>';
      }).join('')+'</div>'):'<div class="aln-empty">当前无在跟踪的节点票</div>');

  /* 下一个节点（节气 / 江恩）：给「变盘时间窗」提前预告 */
  var up=[];
  (N.terms||[]).forEach(function(t){ if(t.date>refDate) up.push({d:t.date,k:'jieqi',h:'节气 '+esc2(t.name)}); });
  (N.gann||[]).forEach(function(g){
    var d=g.trading||g.date; if(d>refDate) up.push({d:d,k:'gann',h:'江恩 '+g.deg+'°'});
  });
  up.sort(function(a,b){ return a.d<b.d?-1:(a.d>b.d?1:0); });
  var upHtml=up.length?('<div class="aln-sec"><b>接下来的时间窗</b><em>节气 / 江恩 8 方角 · 变盘窗口提前盯</em></div>'
    +'<div class="aln-ups">'+up.slice(0,4).map(function(x){
      return '<span class="aln-win k-'+x.k+'"><b>'+esc2(x.d)+'</b>'+x.h+'</span>';
    }).join('')+'</div>'):'';

  box.innerHTML='<div class="aln-top"><span class="aln-t">⚠ 警示节点</span>'
    +'<span class="aln-s">小周期＝最高连板断板当日/次日的首板 · 趋势＝30 日涨幅前五收盘批量负反馈当日的首板 · 双重＝两者同日 · 节气＝二十四节气当日首板 · 江恩＝江恩 8 方角时间窗当日首板｜口径固化在 nodes_core.js，收盘自动更新；名称下方的小胶囊＝该股数字能量（hover 看完整星数）</span></div>'
    +chips+lastHtml+aliveHtml+upHtml;
}

/* ---------- 次日预警 · 大字结论（收盘后用前一日数据算「明天买什么」） ---------- */
function renderAlertPlan(){
  var box=document.getElementById('alertPlan'); if(!box) return;
  var A=window.ALERT, S=window.SENTIMENT;
  if(!A||!A.planNext||!S||!S.limitUp||!S.limitUp.rows||!S.limitUp.rows.length){ box.innerHTML=''; return; }
  var prev=S.limitUp.date;
  /* 天量候选＝前一日（=prev）那一份天量扫描 */
  var tl=(S.volume&&S.volume.date===prev)?(S.volume.hits||[]):[];
  var cal=window.MarketCal;
  var next=cal?cal.nextTradingDay(cal.addDays(prev,1)):'';
  var P=A.planNext({date:prev,session:next,limitUp:S.limitUp.rows,tianliang:tl,
    adjsPrev:(A.adjsBefore?A.adjsBefore(window.BREADTH,next||prev):null)});

  var head='<div class="alp-top"><span class="alp-t">🎯 '+(next||'次日')+' 出手名单</span>'
    +'<span class="alp-s">依据 '+(prev||'—')+' 收盘数据自动生成 · '
    +'前一日最高板 '+(P.maxBoards==null?'--':P.maxBoards+' 板')
    +(P.maxCount>1?'（并列 '+P.maxCount+' 只·无唯一性）':'（唯一）')
    +' · 竞价阈值已算好，9:25 直接比价</span></div>';

  var body=P.modes.map(function(m){
    var hit=m.list.length;
    var rows=m.list.map(function(x){
      return '<div class="alp-row">'
        +'<span class="alp-nmw"><span class="alp-nm">'+esc2(x.name)+'</span><span class="alp-tags">'+energyUnder(x.code)+nodeTagHtml(nodeTagsOf(x.code))+'</span></span>'
        +'<span class="alp-meta"><span class="alp-meta-1">'+esc2(x.code)+'</span>'
        +'<span class="alp-meta-2">'+esc2(x.meta)+(x.extra?(' · '+esc2(x.extra)):'')+'</span></span>'
        +'<span class="alp-line">'+esc2(x.line)+'</span></div>';
    }).join('');
    if(!hit){
      rows='<div class="alp-row" style="border-style:dashed"><span class="alp-nm" style="color:var(--up)">本期不出手</span>'
        +(m.blocked&&m.blocked.length?'<span class="alp-meta">'+m.blocked.map(function(b){return esc2(b.name+b.why?'（'+b.code+'）'+b.why:'');}).join('；')+'</span>':'')
        +'</div>';
    }
    var blockedNote=(hit&&m.blocked&&m.blocked.length)
      ? '<div class="alp-note">另排除：'+m.blocked.map(function(b){return esc2(b.name+'（'+b.code+'）→ '+b.why);}).join('；')+'</div>' : '';
    return '<div class="alp-m alp-m--'+m.key+' '+(hit?'hit':'none')+'">'
      +'<div class="alp-mt"><span class="alp-mn">'+esc2(m.name)+'</span>'
      +'<span class="alp-tag">'+esc2(m.tag)+'</span>'
      +'<span class="alp-cnt'+(hit?'':' zero')+'">'+(hit?('出手 '+hit+' 只'):'—')+'</span></div>'
      +'<div class="alp-rows">'+rows+'</div>'+blockedNote
      +'<div class="alp-how"><b>怎么买：</b>'+esc2(m.how)+'</div></div>';
  }).join('');

  box.innerHTML=head+'<div class="alp-grid">'+body+'</div>';
}
function alCondHtml(r){
  return r.map(function(c){
    var m = c.val===true ? '<span class="mk-ok">✓</span>' : (c.val===false ? '<span class="mk-no">✗</span>' : '<span class="mk-unk">?</span>');
    var ex = (c.val===true?'满足':(c.val===false?'不满足':'待确认'));
    return m+' '+c.name+'<span style="opacity:.6">（'+ex+'）</span>';
  }).join('<br>');
}
function renderAlert(){
  var A=window.ALERT;
  var listEl=document.getElementById('alertList');
  if(!A) return;
  var H=(window.LADDERHIST&&window.LADDERHIST.days)||{};
  var S=window.SENTIMENT, cur=(S&&S.ladder)?S.ladder:null;
  var hDates=Object.keys(H).filter(function(k){return H[k]&&H[k].items&&H[k].items.length;}).sort();
  var allDates=(cur&&cur.date&&hDates.indexOf(cur.date)<0)?hDates.concat([cur.date]).sort():hDates;
  var MC=window.MarketCal, today=MC?MC.ymd(new Date()):'';
  if(today&&MC&&MC.isTradingDay(today)&&allDates.indexOf(today)<0) allDates=allDates.concat([today]).sort();

  var sel=document.getElementById('alertDateSel');
  if(sel){
    // 签名带上当日条目数：数据到达（items 由空变有）时签名必变，下拉标记（待更新 / 实时快照）才会刷新掉
    var sig=allDates.join('|')+'#'+(cur&&cur.date)+'#'+(cur&&cur.items?cur.items.length:0);
    if(sel._alSig!==sig){
      sel.innerHTML=allDates.map(function(d){
        var tag=(cur&&cur.date===d)?'（9:25 实时快照）':((today&&d===today)?'（待更新）':'');
        return '<option value="'+d+'">'+d+tag+'</option>';
      }).join('');
      sel._alSig=sig;
    }
    if(!alertDate||allDates.indexOf(alertDate)<0) alertDate=allDates[allDates.length-1];
    if(sel.value!==alertDate) sel.value=alertDate;
    if(!sel._alBound){ sel.onchange=function(){ alertDate=sel.value; renderAlert(); }; sel._alBound=true; }
  }
  var sBtn=document.getElementById('alertStrictBtn');
  if(sBtn){
    sBtn.classList.toggle('on',alertStrict);
    sBtn.querySelector('.rf-tx').textContent=alertStrict?'严格口径（首封9:25即排除）':'放宽口径（T字板算换手）';
    if(!sBtn._alBound){ sBtn.onclick=function(){ alertStrict=!alertStrict; renderAlert(); }; sBtn._alBound=true; }
  }
  var cBtn=document.getElementById('alertShowCBtn');
  if(cBtn){
    cBtn.classList.toggle('on',alertShowC);
    if(!cBtn._alBound){ cBtn.onclick=function(){ alertShowC=!alertShowC; renderAlert(); }; cBtn._alBound=true; }
  }
  var vBtn=document.getElementById('alertViewBtn');
  if(vBtn){
    vBtn.classList.toggle('on',alertView==='split');
    vBtn.querySelector('.rf-tx').textContent = alertView==='split'?'分区视图':'合并对照';
    if(!vBtn._alBound){ vBtn.onclick=function(){ alertView=(alertView==='split'?'merge':'split'); renderAlert(); }; vBtn._alBound=true; }
  }
  if(!alertDate){ var _se=document.getElementById('alertSummary'); if(_se) _se.textContent='暂无历史数据'; if(listEl) listEl.innerHTML=''; return; }

  var items=alNodes(alEnrich(alertItemsOf(alertDate), alertDate), alertDate);
  var R=A.evaluate(items,{date:alertDate,strictTurnover:alertStrict,adjsPrev:(A.adjsBefore?A.adjsBefore(window.BREADTH,alertDate):null)});
  renderAlertPlan();
  renderAlertNodes();
  /* 选中「今天」而 9:25 竞价尚未抓到时，明确说明下方展示的是最近一个已收盘交易日，避免误读为「今天没票打」 */
  if(alertDate===today && items && !items.length){
    var plEl=document.getElementById('alertPlan');
    if(plEl) plEl.insertAdjacentHTML('afterbegin','<div class="al-pend">'+alertDate
      +' 的竞价数据将在 <b>09:25 开盘后自动抓取</b>更新；下方暂显示最近一个已收盘交易日的结果</div>');
  }
  var s=R.stats;
  var sumEl=document.getElementById('alertSummary');
  if(sumEl){
    sumEl.innerHTML =
      '<span class="al-chip"><i>日期</i><b>'+alertDate+'</b></span>'
      +'<span class="al-chip"><i>样本</i><b>'+s.total+'</b></span>'
      +'<span class="al-chip"><i>最高板</i><b>'+(R.ctx.maxBoards==null?'--':R.ctx.maxBoards)+' 板'+(R.ctx.maxCount>1?'·并列'+R.ctx.maxCount:'·唯一')+'</b></span>'
      +'<span class="al-chip ok"><i>入选</i><b>'+s.A+'</b></span>'
      +'<span class="al-chip warn"><i>待确认</i><b>'+s.B+'</b></span>'
      +'<span class="al-chip"><i>排除</i><b>'+s.C+'</b></span>'
      +'<span class="al-chip sub"><i>分模式</i><b>卡位 '+s.cardA+' · 最高标 '+s.topA+' · 天量 '+s.tianliangA+' · 五虎 '+s.fiveA+'</b></span>'
      +'<span class="al-chip sub"><i>前日 ADJS</i><b>'+(R.ctx.adjsPrev==null?'--':R.ctx.adjsPrev)+(R.ctx.adjsPrev!=null&&R.ctx.adjsPrev<-4000?'·五虎已激活':'')+'</b></span>';
  }

  document.getElementById('alertModes').innerHTML=
    '<div class="al-modes">'+
    '<div class="al-mode"><h4>换手卡位模式（≥4板）</h4><div class="al-desc">'+
    '前提：前一日必须是换手板（首封≠9:25）· 无可转债/非次新/非利好公告 · 4板及以上才做 · 弱转强卡位优先 · 竞价必须红开 · 上板在上午<br>'+
    '买入：炸板打回封的排板 · 仓位5层 · 同身位批量卡位分仓打两只弱转强，次日去弱留强<br>'+
    '卖出：非节点票 7个点以下等冲高拐头、7个点以上或一字炸板即卖；节点票 格局优先（深水涨停+批量跌停+天量炸板就走）</div></div>'+
    '<div class="al-mode"><h4>最高标模式（≥5板）</h4><div class="al-desc">'+
    '前提：前一日必须是换手板 · 无可转债/非次新/非利好公告 · 5板及以上 · <b>需要有唯一性</b>成为唯一次日出手 · 竞价红开 · 上板在上午 · 当天不能有跌停 · 必须是节点票<br>'+
    '买入：连板过程中至少有过一次换手 · 炸板打回封的排板 · 仓位5层，继续强则打满<br>'+
    '卖出：同节点票规则（深水涨停+批量跌停+天量炸板走；其余时间尾盘竞价不涨停即走）</div></div>'+
    '<div class="al-mode"><h4>天量弱转强（博弈量能 · ★天量的次日做）</h4><div class="al-desc">'+
    '口径：<b>做在天量的次日</b>——候选池取【前一交易日】的天量扫描（上市以来前三大成交量，含历史第2/第3大量，不限连板）<br>'+
    '平台：天量当日收盘价＝<b>博弈量能平台</b>（多空共同认可的价格）<br>'+
    '进点：次日竞价<b>今开 ≥ 平台价</b>即多头胜＝弱转强进点；跌破平台＝空头胜，放弃<br>'+
    '加分：平量（量能与天量日相当）·竞价最后几分钟<b>抢筹上跳</b>（成功率极高）· 须是主线龙头，忌拿三流跟风的高量柱</div></div>'+
    '<div class="al-mode"><h4>五虎模式（前一日 ADJS <b>&lt; −4000</b> → 次日竞价触发）</h4><div class="al-desc">'+
    '前提：<b>前一日 ADJS < −4000</b>（上涨家数−下跌家数，恐慌杀跌）<br>'+
    '触发（次日 9:25 四条同时成立）：① 竞价换手率<b>进前五</b>（全池排名）② 竞价涨幅 <b>&gt; 7%</b> ③ 连板天数 <b>&gt; 2</b>（≥3板）<br>'+
    '买入：四条齐了直接给预警，竞价直接打；不满足就空仓等。恐慌日后的抢筹，看的是资金真金白银抢前五</div></div>'+
    '</div>';

  /* ---------- 三模式总览表：一行一只票，一眼看出命中哪个模式 ---------- */
  var keys=(R.modeKeys||['card','top']);
  var shortOf={card:'换手卡位',top:'最高标',tianliang:'天量弱转强',five:'五虎'};
  var brd=R.rows.slice().sort(function(a,b){
    var r={A:0,B:1,C:2};
    if(r[a.best.level]!==r[b.best.level]) return r[a.best.level]-r[b.best.level];
    var ah=keys.filter(function(k){return a[k]&&a[k].level==='A';}).length;
    var bh=keys.filter(function(k){return b[k]&&b[k].level==='A';}).length;
    if(ah!==bh) return bh-ah;
    return (b.it.boards||0)-(a.it.boards||0);
  });
  function dot(k,x){
    var m=x[k]; if(!m) return '<td class="ab-c">·</td>';
    if(m.level==='A') return '<td class="ab-c"><span class="ab-dot ok" title="'+m.short+'：入选">✓</span></td>';
    if(m.level==='B') return '<td class="ab-c"><span class="ab-dot unk" title="'+m.short+'：待确认（有前提数据缺失）">?</span></td>';
    return '<td class="ab-c"><span class="ab-dot no" title="'+m.short+'：不满足">✗</span></td>';
  }
  var brdEl=document.getElementById('alertBoard');
  if(brdEl){
    if(!brd.length){ brdEl.innerHTML=''; }
    else{
      var rowsHtml=brd.map(function(x){
        var it=x.it;
        var hits=keys.filter(function(k){return x[k]&&x[k].level==='A';}).map(function(k){return shortOf[k];});
        var pend=keys.filter(function(k){return x[k]&&x[k].level==='B';}).map(function(k){return shortOf[k];});
        var concl = hits.length ? '<b style="color:var(--up)">'+hits.join(' + ')+'</b>'
          : (pend.length ? '<span style="color:#b8860b">待确认：'+pend.join('/')+'</span>' : '<span style="color:#a0a4ad">不符合任一模式</span>');
        return '<tr class="'+x.best.level+'">'+
          '<td class="ab-nm"><span class="ab-nmw">'+it.name+nodeTagHtml(it.nodeTags)+'<span class="ab-cd">'+it.code+'</span></span>'+energyUnder(it.code)+'</td>'+
          '<td class="ab-c"><b>'+(it.boards==null?'--':it.boards)+'</b></td>'+
          '<td class="ab-c '+(it.gap>0?'up':(it.gap<0?'dn':''))+'">'+(it.gap==null?'--':(it.gap>0?'+':'')+fmt(it.gap))+'</td>'+
          '<td class="ab-c">'+fmt(it.turnover)+'</td>'+
          dot('card',x)+dot('top',x)+dot('tianliang',x)+dot('five',x)+
          '<td class="ab-concl">'+concl+'</td></tr>';
      }).join('');
      brdEl.innerHTML=
        '<div style="font-size:12.5px;font-weight:700;margin-bottom:5px">四模式总览 · 一行一只票，色块直接标出命中模式</div>'+
        '<div class="ab-legend"><span class="ab-dot ok">✓</span>入选 '+
          '<span class="ab-dot unk">?</span>待确认（数据缺失） '+
          '<span class="ab-dot no">✗</span>不满足　|　'+
        keys.map(function(k){return '<span class="ab-lg" data-k="'+k+'">'+shortOf[k]+'</span>';}).join('')+'</div>'+
        '<div class="scroll" style="max-height:340px"><table class="ab-table"><thead><tr>'+
        '<th>股票</th><th>连板</th><th>竞价%</th><th>竞价换手%</th>'+
        keys.map(function(k){return '<th class="ab-h-'+k+'">'+shortOf[k]+'</th>';}).join('')+
        '<th>命中模式</th></tr></thead><tbody>'+rowsHtml+'</tbody></table></div>';
    }
  }
  /* ---------- 分区 / 对照视图（明细列表容器不存在时整体跳过） ---------- */
  if(listEl){
  function alOneCard(x,k){
    var it=x.it, m=x[k], lv=m.level;
    return '<div class="al-card lv'+lv+'">'+
      '<div class="al-h"><span class="al-nmw"><span class="al-name">'+it.name+nodeTagHtml(it.nodeTags)+'</span>'+energyUnder(it.code)+'</span>'+
      '<span class="al-tag">'+(it.boards==null?'--':it.boards)+'板</span>'+
      '<span class="al-tag '+(k==='top'?'t-top':(k==='tianliang'?'t-tian':'t-card'))+'">'+m.short+'</span>'+
      (k==='tianliang'&&it.tianLiangNote?'<span class="al-tag">'+it.tianLiangNote+'</span>':'')+
      '<span class="al-tag lv lv'+lv+'">'+(lv==='A'?'✓ 入选':(lv==='B'?'? 待确认':'✗ 排除'))+'</span>'+
      (it.theme?'<span class="al-tag">'+it.theme+'</span>':'')+
      '<span class="al-metrics">竞价 '+(it.gap==null?'--':(it.gap>0?'+':'')+fmt(it.gap)+'%')+
      ' · 竞价换手 '+(it.turnover==null?'--':fmt(it.turnover)+'%')+
      (it.firstZT?' · 昨日首封 '+it.firstZT:(it.boardType?' · '+it.boardType:''))+'</span></div>'+
      '<div class="al-col" style="margin:8px 11px 10px">'+
        '<div class="al-cond"><b style="color:var(--sub)">硬性前提</b><br>'+alCondHtml(m.hard)+'</div>'+
        '<div class="al-cond" style="margin-top:5px;opacity:.85"><b style="color:var(--sub)">加分 / 人工项</b><br>'+alCondHtml(m.soft)+'</div>'+
      '</div></div>';
  }
  if(alertView==='split'){
    /* ── 分区视图：两个模式各自独立成块 ── */
    var html='';
    (R.modeKeys||['card','top']).forEach(function(k){
      var meta=A.MODES[k];
      var rows=R.rows.map(function(x){return {x:x,m:x[k]};})
        .filter(function(o){ return o.m && (alertShowC || o.m.level!=='C'); })
        .sort(function(p,q){
          var r={A:0,B:1,C:2};
          if(r[p.m.level]!==r[q.m.level]) return r[p.m.level]-r[q.m.level];
          return (q.x.it.boards||0)-(p.x.it.boards||0);
        });
      var aN=rows.filter(function(o){return o.m.level==='A';}).length;
      var bN=rows.filter(function(o){return o.m.level==='B';}).length;
      html+='<div class="al-sec al-sec-'+k+'">'+
        '<div class="al-sec-h"><span class="al-sec-t">'+meta.name+'</span>'+
        '<span class="al-tag '+(k==='top'?'t-top':(k==='tianliang'?'t-tian':'t-card'))+'">'+
          (k==='tianliang'?'竞价进点':(k==='five'?'ADJS<−4000 · 次日竞价触发':('≥'+meta.minBoards+'板'+(k==='top'?' 且唯一':''))))+'</span>'+
        '<span class="al-sec-s">入选 <b class="mk-ok">'+aN+'</b> · 待确认 <b style="color:#b8860b">'+bN+'</b>'+
        (alertShowC?' · 排除 '+rows.filter(function(o){return o.m.level==='C';}).length:'')+'</span></div>'+
        '<div class="al-sec-d">'+meta.desc+'</div>';
      if(!rows.length){
        html+='<div class="al-sec-empty">本模式下当日无标的'+
          (alertShowC?'':'（可点「含被排除」查看未通过原因）')+'</div>';
      }else{
        html+=rows.map(function(o){ return alOneCard(o.x,k); }).join('');
      }
      html+='</div>';
    });
    listEl.innerHTML=html;
  }else{
  var rows=R.rows.filter(function(x){ return alertShowC || x.best.level!=='C'; });
  if(!rows.length){ listEl.innerHTML='<div class="muted" style="padding:14px">当日无满足任一模式前提条件的标的（可点「含被排除」查看全部 '+s.total+' 只的未通过原因）。</div>'; }
  else{
    listEl.innerHTML=rows.map(function(x){
      var it=x.it, lv=x.best.level;
      var mcls=lv==='A'?'':(lv==='B'?'lvB':'lvC');
      var cols=(R.modeKeys||['card','top']).map(function(k){
        var m=x[k];
        if(!m) return '';
        return '<div class="al-col"><div class="al-ct">'+m.short+
          (k==='tianliang'&&it.tianLiangNote?' <span class="al-tag">'+it.tianLiangNote+'</span>':'')+
          ' <span class="al-tag lv '+m.level+'">'+(m.level==='A'?'入选':(m.level==='B'?'待确认':'排除'))+'</span></div>'+
          '<div class="al-cond">'+alCondHtml(m.hard)+'</div>'+
          '<div class="al-cond" style="margin-top:4px;opacity:.8">＋加分/人工项：'+alCondHtml(m.soft)+'</div></div>';
      }).join('');
      return '<div class="al-card lv'+lv+'">'+
        '<div class="al-h"><span class="al-nmw"><span class="al-name">'+it.name+nodeTagHtml(it.nodeTags)+'</span>'+energyUnder(it.code)+'</span>'+
        '<span class="al-tag">'+(it.boards==null?'--':it.boards)+'板</span>'+
        '<span class="al-tag '+(x.best.key==='top'?'t-top':'t-card')+'">'+x.best.short+'</span>'+
        '<span class="al-tag lv lv'+lv+'">'+(lv==='A'?'✓ 入选':(lv==='B'?'? 待确认':'✗ 排除'))+'</span>'+
        (it.theme?'<span class="al-tag">'+it.theme+'</span>':'')+
        '<span class="al-metrics">竞价 '+(it.gap==null?'--':(it.gap>0?'+':'')+fmt(it.gap)+'%')+
        ' · 换手 '+(it.turnover==null?'--':fmt(it.turnover)+'%')+
        (it.firstZT?' · 首封 '+it.firstZT:'')+'</span></div>'+
        '<div class="al-body">'+cols+'</div>'+
        (lv==='C'?'<div class="al-foot">已被硬条件排除，展开可看具体未通过项</div>':'')+
        '</div>';
    }).join('');
  }
  }
  }
  document.getElementById('alertNote').innerHTML=
    '<b>判定说明</b>：<span class="mk-ok">✓</span>满足 · <span class="mk-no">✗</span>不满足（一票否决）· <span class="mk-unk">?</span>数据缺失需人工确认（首次涨停时间/上市日期/公告/节点票等盘中或人工项）。<br>'+
    '<b>口径</b>：样本＝当日收盘封死涨停且连板≥2 的主板股；「前一日换手板」以该股昨日首次涨停时间是否早于 09:26 判定（严格口径下 T 字板开盘秒板也会被排除，可切「放宽口径」纳入）。<br>'+
    '<b>天量弱转强</b>：前一日天量取自「天量扫描」（放宽口径＝该股成交量进入上市以来前三大，含历史第2/第3大量）；博弈量能平台＝天量日收盘价，今开站上平台即入列；历史日期若无天量数据会标 ? 需人工核量。<br>'+
    '<b>警示节点</b>：股票名旁的「小周期节点 / 趋势节点 / 双重节点 / 节气节点 / 江恩节点」小标签＝该股首板落在节点日（节点票）。收盘刷新后自动加标签，该票一旦出现大跌（收盘 ≤ −5%）立即摘掉标签；只跟踪最近 40 个交易日内出现的节点票。判定口径固化在 nodes_core.js，数据由 _nodes.js 收盘自动生成 nodes_data.js，视图层不另算。<br>'+
    '<b>数字能量</b>：股票名<b>下方</b>的小胶囊＝该股代码按数字能量「八星」读出的星（色点＝五行，如 天医·土），鼠标悬停可看完整「数对·星」读法；统一走 wuxing5.js 的 M5.energyHtml(code,{compact:true})，与连板梯队 / 连板天梯 / 龙头画像 同源同口径，视图层不另算。<br>'+
    '<b>提醒</b>：「上板时间在上午」「节点票」「高位板深水放巨量」「当天无跌停」「竞价抢筹上跳」均为盘中或人工判断项，本模块只做前提条件筛选，不构成买卖建议。';
}
window.DASH.register('alert','复盘计划及交易预警', renderAlert);
renderAlert();          /* 首屏即渲染：不依赖任何手动刷新（此前只注册未调用，打开页面会一直停在「加载中…」） */
renderSkyLadder();

/* ---------- 题材周期主图（模块四） ---------- */
/* 矩阵定位状态：放在模块作用域，renderCycle 每次执行时刷新，
   DOM 事件只绑定一次（刷新时重复绑定会导致跳转叠加） */
const CYC_NAV = {
  focusCi: null,      // 当前高亮列索引（dates/colList 下标）
  cell: 20, Ncol: 0, GUT: 160,
  colList: [], dates: [],
  redraw: null,       // 指向最新的 drawMatrix（闭包）
  scroll: null        // #cycMatrixScroll
};
function cycWeekName(ds){
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ds||''); if(!m) return '';
  return '周' + '日一二三四五六'[new Date(+m[1], +m[2]-1, +m[3]).getDay()];
}

/* ═══ 连板色阶：涨=红，板数越高越深（矩阵 / 表格 / 折线共用） ═══ */
const LVL_BG = ['#ffd9d9','#ff9b9b','#f96a6a','#e04545','#c62f2f','#a81f1f','#7e1010'];
const LVL_FG = ['#b03636','#8a2020','#ffffff','#ffffff','#ffffff','#ffffff','#ffffff'];
const lvlIdx = b => Math.max(0, Math.min(6, (b||1)-1));
const lvlBg = b => LVL_BG[lvlIdx(b)];
const lvlFg = b => LVL_FG[lvlIdx(b)];
/* 非涨停格样式 */
const CELL_ST = {
  zb:{bg:'#ffe6c7', fg:'#b35c00', t:'炸'},
  dt:{bg:'#d6f0e3', fg:'#12734e', t:'跌'},
  db:{bg:'#eceff4', fg:'#a2aabd', t:'/'},
  up:{bg:'#e6eeff', fg:'#2f6bff', t:'↥'},
  down:{bg:'#fbeadb', fg:'#c06a12', t:'↧'},
  note:{bg:'#fbf7e8', fg:'#9a8b5a', t:''}
};
/* 圆角矩形路径（自己画，避免 roundRect 兼容问题） */
function cycRR(ctx,x,y,w,h,r){
  r = Math.max(0, Math.min(r, Math.min(w,h)/2));
  ctx.beginPath();
  ctx.moveTo(x+r,y);
  ctx.lineTo(x+w-r,y); ctx.quadraticCurveTo(x+w,y,x+w,y+r);
  ctx.lineTo(x+w,y+h-r); ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);
  ctx.lineTo(x+r,y+h); ctx.quadraticCurveTo(x,y+h,x,y+h-r);
  ctx.lineTo(x,y+r); ctx.quadraticCurveTo(x,y,x+r,y);
  ctx.closePath();
}
/* 按宽度截断（measureText 兜底：宽度不够就逐字砍） */
function cycEllip(ctx, s, maxW){
  s = String(s==null?'':s);
  if(maxW<=0) return '';
  let w = 0; try{ w = ctx.measureText(s).width; }catch(e){ w = s.length*12; }
  if(w<=maxW) return s;
  let t = s;
  while(t.length>1){ t = t.slice(0,-1); let ww=0; try{ ww=ctx.measureText(t+'…').width; }catch(e){ ww=(t.length+1)*12; }
    if(ww<=maxW) return t+'…'; }
  return '';
}
function renderCycle(){
  const C = window.CYCLE;
  const el = document.getElementById('cycChart');
  const tb = document.querySelector('#cycTable tbody');
  const lg = document.getElementById('cycLegend');
  const selYear = document.getElementById('cycYear');
  const inpTheme = document.getElementById('cycTheme');
  const cnt = document.getElementById('cycCount');
  const esc = s => String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;');
  if(!C || !C.dragons || !C.dragons.length){
    if(el) el.innerHTML = '<div style="color:#7a8194;font-size:13px">暂无周期数据</div>';
    return;
  }

  /* ---- 日期轴自动延伸：Excel 只预填到 2026-12-31，这里补到「明年 12-31」。
         只在末尾增删列，列号从 6 起连续，所以 dragons[].c0 不受影响；
         补出来的列天然没有数据，年/月/日表头由 drawMatrix 通用绘制，自动排好。
         幂等：每次刷新都调，结果一致。 ---- */
  if(window.CycleAxis && window.CycleAxis.extendAxis){
    try{ window.__cycAxis = window.CycleAxis.extendAxis(C); }catch(e){ window.__cycAxis = {err:e.message}; }
  }

  /* ---- 图例（连板色阶 + 状态色块，比原始符号直观得多） ---- */
  const COLOR = {zt:'#d93a3a', zb:'#e08b1a', dt:'#1a9c6b', db:'#98a0b3', up:'#2f6bff', down:'#d98324', note:'#9aa1b1'};
  const TINT  = {zt:'#fdeaea', zb:'#fdf3e3', dt:'#e8f6ef', db:'#f4f5f8', up:'#eaf0ff', down:'#fbf0e2', note:'#ffffff'};
  const LABEL = {zt:'涨停', zb:'炸板', dt:'跌停', db:'断板/非涨停', up:'趋势↑', down:'趋势↓', note:'注解(忽略)'};
  if(lg){
    const sw = (bg,fg,t) => `<i class="cyc-sw" style="background:${bg};color:${fg}">${t}</i>`;
    lg.innerHTML =
      '<span class="chip" style="border:none;background:transparent;padding-left:0;color:var(--sub)">连板高度</span>' +
      [1,2,3,4,5,6].map(b=>`<span class="chip">${sw(lvlBg(b),lvlFg(b),b)}${b}板</span>`).join('') +
      `<span class="chip">${sw(lvlBg(7),lvlFg(7),'7+')}7板及以上</span>` +
      '<span class="cyc-lg-sep"></span>' +
      `<span class="chip">${sw(CELL_ST.zb.bg,CELL_ST.zb.fg,'炸')}炸板</span>` +
      `<span class="chip">${sw(CELL_ST.dt.bg,CELL_ST.dt.fg,'跌')}跌停</span>` +
      `<span class="chip">${sw(CELL_ST.db.bg,CELL_ST.db.fg,'/')}断板</span>` +
      '<span class="cyc-lg-sep"></span>' +
      '<span class="chip" title="取消「连板数字」勾选可看到 Excel 里的原始符号 ⭕◯∅┻/">原始符号</span>';
  }

  /* ---- 列 -> 日期 ---- */
  const axis = C.axis || {};
  const colList = Object.keys(axis).map(Number).sort((a,b)=>a-b);
  const firstCol = colList.length ? colList[0] : 0;
  const col2ci = {}; colList.forEach((c,i)=>col2ci[c]=i);
  const Ncol = colList.length;
  const dates = colList.map(c=>axis[c]);
  /* 状态栏：把「日期轴覆盖到哪天」显式写出来，省得以为表头断了 */
  const axStat = document.getElementById('cycAxisStat');
  if(axStat){
    const ai = window.__cycAxis || {};
    axStat.innerHTML = '日期轴 <b>'+(dates[0]||'-')+'</b> ~ <b>'+(dates[dates.length-1]||'-')+'</b>'
      + '（共 '+Ncol+' 个交易日，自动延伸到 '+(ai.horizon||'')+'）'
      + (ai.added ? ' · 本次补 ' + ai.added + ' 列' : '')
      + (ai.err ? ' · <span style="color:#c0392b">延伸失败：' + ai.err + '</span>' : '');
  }

  /* ---- 分类 ---- */
  const SET = {}; (C.legend||[]).forEach(g=>{ g.syms.forEach(s=>SET[s]=g.key); });
  const EXTRA = {'↥':'up','↧':'down','Ⓩ':'zt'};
  function autoCat(ch){
    if(ch===' '||ch==='') return null;
    if(SET[ch]) return SET[ch];
    if(EXTRA[ch]) return EXTRA[ch];
    if(/^[0-9A-Za-z.\-)]$/.test(ch)) return 'note';
    return 'db';
  }
  // 手动修正（localStorage）
  const OV_KEY='wb_cyc_overrides_v1';
  let OV = {}; try{ OV = JSON.parse(localStorage.getItem(OV_KEY))||{}; }catch(e){}
  function saveOV(){ try{ localStorage.setItem(OV_KEY, JSON.stringify(OV)); }catch(e){} }
  function effCat(d, gcol){
    if(gcol < d.c0 || gcol >= d.c0 + (d.s||'').length) return null;
    const ch = d.s[gcol - d.c0];
    const ov = OV[d.r + ':' + gcol];
    if(ov) return ov==='__none' ? null : ov;
    return autoCat(ch);
  }

  /* ---- 计算每只龙头的连板阶梯（尊重修正） ---- */
  function ladder(d){
    const out=[]; let cur=0;
    for(let j=0;j<(d.s||'').length;j++){
      const gcol=d.c0+j, ci=col2ci[gcol];
      if(ci==null) continue;
      const k=effCat(d,gcol);
      cur = (k==='zt') ? cur+1 : 0;
      out.push({ci:ci, gcol:gcol, ch:d.s[j], k:k, b:(k==='zt'?cur:0)});
    }
    return out;
  }
  /* bmap：列号 → 该行在该列的连板数（矩阵画数字时用，避免每帧重算） */
  const all = C.dragons.map(d=>{
    const pts=ladder(d), bm={};
    pts.forEach(p=>{ if(p.k==='zt') bm[p.gcol]=p.b; });
    return {d:d, pts:pts, bmap:bm};
  }).filter(o=>o.pts.length>0);
  all.forEach(o=>{
    o.peak = o.pts.reduce((m,p)=>Math.max(m,p.b),0);
    o.lastZt = -1; o.pts.forEach(p=>{ if(p.k==='zt') o.lastZt = p.ci; });   // 最后一次涨停所在列
  });
  /* 全局最后一个「还有人在涨停」的交易日：折线默认聚焦它附近的梯队 */
  let lastZtCi = 0;
  all.forEach(o=>{ if(o.lastZt>lastZtCi) lastZtCi = o.lastZt; });

  /* ---- 年份下拉 ---- */
  const years = {};
  all.forEach(o=>{ const y=(o.pts[0]&&dates[o.pts[0].ci]||'').slice(0,4); if(y) years[y]=1; });
  selYear.innerHTML = '<option value="">全部</option>' +
    Object.keys(years).sort().map(y=>`<option value="${y}">${y}年</option>`).join('');

  /* ---- 连板梯队折线图（时间轴：纵轴=连板高度，横轴=交易日） ---- */
  let cycChartInst = null;
  function drawChart(){
    if(!el) return;
    const y = selYear.value;
    const kw = (inpTheme.value||'').trim();
    const minB = parseInt(document.getElementById('cycMin').value,10)||2;
    const nameEl = document.getElementById('cycNameLbl');
    const nameOn = !(nameEl && nameEl.checked===false);
    /* 梯队范围：0 = 全部（按历史最高板），>0 = 只看该窗口内还在连板的票 */
    const scopeEl = document.getElementById('cycScope');
    const rawWin = scopeEl ? String(scopeEl.value) : '';
    const win = rawWin==='' ? 60 : (parseInt(rawWin,10)||0);   // 空值兜底近 60 日
    const winFrom = win>0 ? Math.max(0, lastZtCi - win + 1) : 0;
    const liveFrom = lastZtCi;       // 只在「最后一个有涨停的交易日」仍在板 = 在连（当前梯队）

    /* ① 逐日汇总：一天一天把全表扫一遍，得到每天的「最高连板 + 高度龙一 + 涨停/炸板/跌停数」
       —— 数据就是下方「全量还原矩阵」那张表（CYCLE.dragons 经同一套 effCat 解析），没有第二套口径 */
    const dayTop = new Array(Ncol).fill(null);   // 当天最高连板数
    const dayWho = new Array(Ncol).fill(null);   // 当天的高度龙一
    const dayCnt = new Array(Ncol).fill(null);   // 当天涨停 / 炸板 / 跌停 各多少只
    const dayLb  = new Array(Ncol).fill(0);       // 当天连板个数：≥2 板的只数（情绪空间 SCJYVALUE(23,2,0)）
    all.forEach(o=>o.pts.forEach(p=>{
      const ci = p.ci; if(ci<0 || ci>=Ncol) return;
      let c = dayCnt[ci]; if(!c) c = dayCnt[ci] = {zt:0, zb:0, dt:0};
      if(p.k==='zt'){ c.zt++; if(p.b>=2) dayLb[ci]++; } else if(p.k==='zb') c.zb++; else if(p.k==='dt') c.dt++;
      if(p.k!=='zt') return;
      if(dayTop[ci]==null || p.b>dayTop[ci]){ dayTop[ci]=p.b; dayWho[ci]=o; }
    }));

    /* ② 每只龙头挑一段「最像样的纯连板」画成一条线：
         一天不涨停就当场断，反包再涨停也另起一段从第 1 板重数（绝不累计高度）；
         同一只票历史有多段时取最长那段（在连的那段优先）。 */
    const picked = [];
    all.forEach(o=>{
      let cur = null, bestSeg = null;
      const better = s => {
        if(!s) return false;
        if(!bestSeg) return true;
        if(s.pts.length !== bestSeg.pts.length) return s.pts.length > bestSeg.pts.length;
        return s.pts[s.pts.length-1].ci >= lastZtCi;      // 同样长：在连的那段优先
      };
      const flush = ()=>{ if(better(cur)) bestSeg = cur; cur = null; };
      o.pts.forEach(p=>{ if(p.k==='zt'){ if(!cur) cur={pts:[]}; cur.pts.push(p); } else flush(); });
      flush();
      if(!bestSeg) return;
      const pts = bestSeg.pts;
      picked.push({o:o, pts:pts, len:pts.length, start:pts[0].ci, end:pts[pts.length-1].ci,
                   live:pts[pts.length-1].ci>=liveFrom});
    });

    let segList = picked.filter(s=>s.len>=minB
      && (!win || s.end>=winFrom)
      && (!y || (dates[s.start]||'').slice(0,4)===y)
      && (!kw || ((s.o.d.t||'').indexOf(kw)>=0 || (s.o.d.n||'').indexOf(kw)>=0)));
    segList.sort((a,b)=> (b.live?1:0)-(a.live?1:0) || b.len-a.len || b.end-a.end || (a.o.d.n<b.o.d.n?-1:1));
    const drawSegs = segList;   // 红线口径：全部通过筛选的龙头（参照图＝全市场最高板，不再截断）
    const liveN = drawSegs.filter(s=>s.live).length;
    if(cnt) cnt.innerHTML = '全表 <b>'+all.length+'</b> 只龙头'
      + '，符合条件的连接段 <b>'+segList.length+'</b> 段'
      + (win>0 ? '（近 '+win+' 个交易日）' : '（全部历史）')
      + '，红线取前 <b>'+drawSegs.length+'</b> 只龙头的高度'
      + (liveN ? '，其中 <b style="color:#d93a3a">在连 '+liveN+' 只</b>（已排在前面）' : '')
      + (segList.length>drawSegs.length ? '，其余见下方表格/矩阵' : '');
    if(!cycChartInst){
      // 重复刷新时先销毁旧实例，避免 _echarts_instance_ 残留导致空白
      const old = echarts.getInstanceByDom(el); if(old) old.dispose();
      el.removeAttribute('_echarts_instance_');
      cycChartInst = echarts.init(el);
    }
    if(!drawSegs.length){
      cycChartInst.clear();
      cycChartInst.setOption({title:{text:'当前筛选条件下无数据',left:'center',top:'middle',textStyle:{color:'#7a8194',fontSize:13,fontWeight:'normal'}}});
      return;
    }
    /* 默认时间窗：最近梯队看 lastZtCi 前后一段，历史模式看命中票的最后一段 */
    let maxCi = 0, maxPeak = 0;
    drawSegs.forEach(s=>{ if(s.len>maxPeak) maxPeak=s.len; if(s.end>maxCi) maxCi=s.end; });
    const anchor = win>0 ? lastZtCi : maxCi;
    const sv = Math.max(0, win>0 ? anchor - win - 2 : anchor - 90);
    const ev = Math.min(Ncol-1, anchor + 3);
    /* 纵轴要容得下红线在窗口内的最高点，否则顶端会被裁掉（扫描放在 chartTop 算完后） */

    /* —— 严格按照参考图（同花顺·连板天梯）的表达：整张图只有一条红线 ——
       纵轴＝当日最高连板，横轴＝交易日；每个交易日一个点，
       点旁竖排「股票名＋板数」，同日并列龙头挨个往下排；
       涨跌全程连线：上去是坡、下来也是坡，完整山峰，不切任何段。 ---- */
    const TOP_NAME = '梯队顶端（当日最高板）';
    /* 红线口径：筛选后（题材/年份/最低连板/龙头条数）龙头池里每天的最高板 */
    const chartTop = new Array(Ncol).fill(null);
    const chartWhos = new Array(Ncol).fill(null);
    drawSegs.forEach(s=>s.pts.forEach(p=>{
      const ci = p.ci; if(ci<0 || ci>=Ncol) return;
      if(chartTop[ci]==null || p.b>chartTop[ci]){ chartTop[ci]=p.b; chartWhos[ci]=[s.o]; }
      else if(p.b===chartTop[ci] && !chartWhos[ci].some(x=>x===s.o)) chartWhos[ci].push(s.o);
    }));
    /* 池子里没有 ≥2 板的日子（全市场只剩首板）也要落点：用全表口径的 1 板补上，线才不断 */
    dayTop.forEach((v,i)=>{
      if(v===1 && chartTop[i]==null){ chartTop[i]=1; chartWhos[i]= dayWho[i] ? [dayWho[i]] : []; }
    });
    const topData = chartTop.map((v,i)=>{
      if(v==null) return { value:[i, null] };
      const names = (chartWhos[i]||[]).map(o=>String(o.d.n||'')).slice(0,3);
      const vert = names.map(n=> n.slice(0,6).split('').join('\n') + '\n' + v).join('\n');
      return { value:[i,v], symbolSize:4.5,
        label:{ show:nameOn, position:'top', distance:4,
          formatter:vert, fontSize:10, lineHeight:12, fontWeight:600,
          color:'#b03a2e', align:'center' } };
    });
    const series = [{
      name:TOP_NAME, type:'line', data:topData,
      symbol:'circle', showSymbol:true, connectNulls:false,
      lineStyle:{color:'#b03a2e', width:2.2},
      itemStyle:{color:'#b03a2e'},
      emphasis:{disabled:true},
      z:9999, triggerLineEvent:false
    }];
    for(let i=sv;i<=ev;i++){ const v=chartTop[i]; if(v!=null && v>maxPeak) maxPeak=v; }

    /* ---- 情绪空间叠加曲线（X1 / X2）：把市场级统计按连板高度轴归一化后同图展示 ----
       公式原文：NOTEXT涨停:HLB*涨停数/HZT   NOTEXT连板:HLB*连板个数/HLBGS
       HLB=CONST(HHV(连板高度,窗口))  HZT=CONST(HHV(涨停数,窗口))  HLBGS=CONST(HHV(连板个数,窗口)) */
    const mkScale = (arr, pick)=>{
      let mx = 1; for(let i=sv;i<=ev;i++){ const v = pick(i); if(v>mx) mx=v; }
      return { mx, fn:(i)=> (HLB*mx>0 && pick(i)>0) ? +(HLB*pick(i)/mx).toFixed(3) : null };
    };
    const HLB = Math.max(1, ...chartTop.slice(sv,ev+1).map(v=>v||0));   // 窗口内最高连板（红线口径）
    const x1El = document.getElementById('cycX1'), x2El = document.getElementById('cycX2');
    const x1On = !(x1El && x1El.checked===false), x2On = !(x2El && x2El.checked===false);
    if(x1On){
      const s = mkScale(dayCnt, i=> dayCnt[i] ? dayCnt[i].zt : 0);
      const data = []; for(let i=sv;i<=ev;i++){ const v=s.fn(i); if(v!=null) data.push([i, v]); }
      series.push({ name:'涨停数(归一化)', type:'line', data, step:'middle', symbol:'none',
        lineStyle:{color:'#5E409B', width:1.4, type:'dashed'},
        itemStyle:{color:'#5E409B'}, z:5000, silent:true });
    }
    if(x2On){
      const s = mkScale(dayLb, i=> dayLb[i]);
      const data = []; for(let i=sv;i<=ev;i++){ const v=s.fn(i); if(v!=null) data.push([i, v]); }
      series.push({ name:'连板个数(归一化)', type:'line', data, step:'middle', symbol:'none',
        lineStyle:{color:'#808080', width:1.4, type:'dotted'},
        itemStyle:{color:'#808080'}, z:5000, silent:true });
    }

    /* ---- 与下方矩阵联动：矩阵定位到哪天，折线图就在这里竖一条蓝线 ---- */
    const fCi = CYC_NAV.focusCi;
    if(fCi!=null && fCi>=0 && fCi<Ncol){
      series.push({ name:'__focus', type:'line', data:[], silent:true, animation:false,
        tooltip:{show:false}, legendHoverLink:false, z:100000,
        markLine:{ silent:true, symbol:'none', animation:false,
          lineStyle:{color:'#2f6bff', width:1.2},
          label:{ show:true, position:'insideEndTop', formatter:(dates[fCi]||''), fontSize:10,
                  color:'#2f6bff', backgroundColor:'rgba(234,240,255,.95)', padding:[2,4], borderRadius:3 },
          data:[{xAxis:fCi}] } });
    }

    cycChartInst.setOption({
      animation:false,
      tooltip:{ trigger:'axis', axisPointer:{type:'line', lineStyle:{color:'#b9c0cf', width:1}},
        backgroundColor:'rgba(26,29,36,.95)', borderWidth:0, padding:[8,10],
        textStyle:{color:'#fff', fontSize:12},
        formatter: ps=>{
          if(!ps || !ps.length) return '';
          const ci = (ps[0].value && ps[0].value[0]!=null) ? ps[0].value[0] : ps[0].dataIndex;
          const dt = dates[ci]||'';
          let s = '<b>'+dt+'</b> <span style="color:#a8b0c0">'+cycWeekName(dt)+'</span>';
          const c0 = dayCnt[ci];
          if(chartTop[ci]!=null){
            const who = (chartWhos[ci]||[]).map(o=>esc(o.d.n||'')).join('、');
            s += '　当日最高 <b style="color:#ffd9d9">'+chartTop[ci]+'板</b>（'+who+'）';
          }else{
            s += '<br><span style="color:#a8b0c0">（当日无连板）</span>';
          }
          if(c0) s += '<br><span style="color:#a8b0c0">全表当日：涨停 ' + c0.zt + '　炸板 ' + c0.zb + '　跌停 ' + c0.dt
            + (dayLb[ci] ? '　连板 ' + dayLb[ci] : '') + '</span>';
          return s;
        }},
      grid:{left:46,right:30,top:20,bottom:58},
      xAxis:{ type:'category', data:dates, boundaryGap:false,
        axisLabel:{formatter:v=>String(v).slice(5), fontSize:10, color:'#7a8194', interval:'auto'},
        axisLine:{lineStyle:{color:'#d9dde6'}}, axisTick:{show:false},
        splitLine:{show:true, lineStyle:{color:'#f4f6f9'}} },
      /* 纵轴＝连板高度：一格 = 一板，强行按整数板刻度，看一眼就知道谁爬到第几层 */
      yAxis:{ type:'value', name:'连板', nameTextStyle:{fontSize:10, color:'#7a8194', padding:[0,0,0,-6]},
        min:0, max:Math.max(3, maxPeak+1),        // 顶上留一格，名称标签不会被裁
        interval:1, minInterval:1, maxInterval:1, // 每板一条网格线
        alignTicks:true,
        axisLabel:{fontSize:10, color:'#8a91a3', formatter:v=>(v%1===0 && v>0) ? (v+'板') : ''},
        axisLine:{show:false}, axisTick:{show:false},
        splitLine:{show:true, lineStyle:{color:'#e8ebf2', type:'dashed'}},
        /* 注意：areaStyle 必须是「单个对象 + color 数组」，写成数组会让 ECharts 抛
           "Cannot read properties of null (reading 'length')" 并整张图白屏 */
        splitArea:{show:true, areaStyle:{color:['rgba(255,255,255,0)','rgba(47,107,255,.028)']}} },
      dataZoom:[
        {type:'inside', startValue:sv, endValue:ev, zoomOnMouseWheel:true, moveOnMouseMove:true, filterMode:'none'},
        {type:'slider', height:16, bottom:10, startValue:sv, endValue:ev, filterMode:'none',
         borderColor:'transparent', backgroundColor:'#f5f6fa', fillerColor:'rgba(47,107,255,.12)',
         handleStyle:{color:'#fff', borderColor:'#b9c0cf'}, textStyle:{fontSize:10, color:'#7a8194'},
         labelFormatter:(v,d)=>String(d||'').slice(5)}
      ],
      series: series
    }, true);

    /* ---- 反向联动：点折线图任意一天 → 矩阵定位到该日，并自动筛出当天有标注的龙头 ---- */
    if(cycChartInst && !cycChartInst.__wbLinked){
      cycChartInst.__wbLinked = 1;
      const jump = ci=>{
        if(ci==null || ci<0 || ci>=Ncol) return;
        const oe = document.getElementById('cycOnlyDay');
        if(oe && !oe.checked) oe.checked = true;          // 顺手打开纵向过滤，矩阵只留当天有动作的票
        if(typeof gotoCi === 'function') gotoCi(ci);
        else if(CYC_NAV.applyFilter){ CYC_NAV.focusCi = ci; CYC_NAV.applyFilter(); }
      };
      try{
        cycChartInst.on('click', pr=>{
          if(!pr) return;
          jump((pr.value && pr.value[0]!=null) ? pr.value[0] : pr.dataIndex);
        });
        const zr = cycChartInst.getZr && cycChartInst.getZr();
        if(zr && zr.on && typeof cycChartInst.convertFromPixel==='function'){
          zr.on('click', ev=>{
            const pt = [ev.offsetX, ev.offsetY];
            if(typeof cycChartInst.containPixel!=='function') return;
            if(!cycChartInst.containPixel({gridIndex:0}, pt)) return;   // 点在网格外（如下方滑块）不处理
            const v = cycChartInst.convertFromPixel({seriesIndex:0}, pt);
            if(v && v[0]!=null) jump(Math.round(v[0]));
          });
        }
      }catch(e){}
    }
  }
  CYC_NAV.syncChart = drawChart;      // 矩阵换一天 → 重画折线，把聚焦竖线挪过去

  /* ---- 明细表 ---- */
  function drawTable(){
    if(!tb) return;
    const y = selYear.value, kw=(inpTheme.value||'').trim(), minB=parseInt(document.getElementById('cycMin').value,10)||2;
    let list = all.filter(o=>o.peak>=minB);
    if(y) list=list.filter(o=>o.pts.some(p=>(dates[p.ci]||'').slice(0,4)===y));
    if(kw) list=list.filter(o=>(o.d.t||'').indexOf(kw)>=0||(o.d.n||'').indexOf(kw)>=0);
    list.sort((a,b)=>b.peak-a.peak||(a.d.n<b.d.n?-1:1));
    const shown=list.slice(0,200);
    if(!shown.length){ tb.innerHTML='<tr><td colspan="6" style="color:var(--sub)">无数据</td></tr>'; return; }
    tb.innerHTML = shown.map(o=>{
      const p0=o.pts[0], p1=o.pts[o.pts.length-1];
      /* 逐日图例：与矩阵同一套色阶（涨停=连板数字，其余=状态色块） */
      const bar=o.d.s.split('').map((ch,j)=>{
        const gcol=o.d.c0+j, k=effCat(o.d,gcol);
        if(!k) return '<i class="s" title="无标注">·</i>';
        if(k==='zt'){
          const b=(o.bmap&&o.bmap[gcol])||1;
          return `<i class="z" style="background:${lvlBg(b)};color:${lvlFg(b)}" title="${dates[col2ci[gcol]]||''}　${b}板涨停　原始符号 ${esc(ch)}">${b}</i>`;
        }
        const cs=CELL_ST[k]||CELL_ST.db;
        const cls2=k==='zb'?'b':k==='dt'?'d':k==='note'?'n':'s';
        return `<i class="${cls2}" title="${dates[col2ci[gcol]]||''}　原始符号 ${esc(ch)}">${cs.t||esc(ch)}</i>`;
      }).join('');
      return `<tr><td>${esc(o.d.n)}</td><td>${esc(o.d.t||'')}</td><td>${o.peak}板</td>`
           + `<td>${dates[p0.ci]||'-'}</td><td>${dates[p1.ci]||'-'}</td>`
           + `<td class="cycbar">${bar}</td></tr>`;
    }).join('');
  }

  /* ============ 全量还原矩阵（Canvas 虚拟化） ============ */
  const mScroll = document.getElementById('cycMatrixScroll');
  const mCanvas = document.getElementById('cycMatrixCanvas');
  const mSpacer = document.getElementById('cycMatrixSpacer');
  const zoomEl  = document.getElementById('cycZoom');
  const fixPop  = document.getElementById('cycFixPop');
  const fixSel  = document.getElementById('cycFixSel');
  const fixTitle= document.getElementById('cycFixTitle');
  const fixStat = document.getElementById('cycFixStat');
  const GUT=190, HDR_Y=15, HDR_M=14, HDR_D=19, HDR=HDR_Y+HDR_M+HDR_D;
  let cell = parseInt(zoomEl?zoomEl.value:20,10)||20;
  let curFix = null;
  let rows = all;              // 当前纵向显示的行集合（「只看当日有标注」时收窄）
  CYC_NAV.rows = rows;         // 事件只绑一次，闭包必须读 CYC_NAV 而不是捕获首次 render 的 rows

  /* ---- 日期定位：把当前尺寸/轴同步到模块级状态，供"只绑一次"的事件读取 ---- */
  CYC_NAV.cell=cell; CYC_NAV.Ncol=Ncol; CYC_NAV.GUT=GUT;
  CYC_NAV.colList=colList; CYC_NAV.dates=dates; CYC_NAV.scroll=mScroll;
  CYC_NAV.redraw=drawMatrix;   // 函数声明提升，指向本次 render 的闭包版本
  const navY=document.getElementById('cycNavY'), navM=document.getElementById('cycNavM');
  const navD=document.getElementById('cycNavD'), navStat=document.getElementById('cycNavStat');
  function opt(v,t,sel){ return '<option value="'+v+'"'+(sel===v?' selected':'')+'>'+t+'</option>'; }
  /* 填充缓存：◀▶ 连点时年/月没变就不重建上千个 option，避免卡顿 */
  let fkY='', fkM='', fkD='';
  function fillYears(){
    if(!navY) return; const ys=[];
    dates.forEach(d=>{ const y=(d||'').slice(0,4); if(y&&ys.indexOf(y)<0) ys.push(y); });
    ys.sort();
    if(fkY==='y'&&navY.innerHTML) return;
    fkY='y';
    navY.innerHTML='<option value="">年</option>'+ys.map(y=>opt(y,y+'年',navY.value)).join('');
  }
  function fillMonths(){
    if(!navM) return; const y=navY?navY.value:'', ms=[];
    dates.forEach(d=>{ if(!d) return; if(y&&d.slice(0,4)!==y) return;
      const m=d.slice(0,7); if(ms.indexOf(m)<0) ms.push(m); });
    ms.sort();
    if(fkM!==y){ fkM=y; navM.innerHTML='<option value="">月</option>'+ms.map(m=>opt(m,(+m.slice(5))+'月',navM.value)).join(''); }
    navM.value = (navM.value && ms.indexOf(navM.value)>=0) ? navM.value : '';  // 换年后旧月份失效，必须清空否则日期列表为 0
  }
  function fillDays(){
    if(!navD) return; const y=navY?navY.value:'', mo=navM?navM.value:'', key=y+'|'+mo;
    if(fkD!==key){
      fkD=key;
      const ds=dates.filter(d=>d&&(!y||d.slice(0,4)===y)&&(!mo||d.slice(0,7)===mo));
      navD.innerHTML='<option value="">日</option>'+ds.map(d=>opt(d,(+d.slice(8))+' '+cycWeekName(d),navD.value)).join('');
      navD._list=ds;
    }
    const ds2=navD._list||[];
    navD.value = (navD.value && ds2.indexOf(navD.value)>=0) ? navD.value : '';
  }
  /* ---- 纵向过滤：只保留定位日有标注的龙头（挂到 CYC_NAV 供只绑一次的事件调用） ---- */
  const onlyEl=document.getElementById('cycOnlyDay');
  /* 状态栏统一重建 —— 用 += 会在反复切换时把旧标记留在上面 */
  function paintNavStat(){
    if(!navStat) return;
    const ci=CYC_NAV.focusCi, n=CYC_NAV.dates.length;
    const ds = ci==null ? '' : (CYC_NAV.dates[ci]||'');
    const ts = cycTodayStr();
    let h = '已定位 <b>'+ds+'</b> '+cycWeekName(ds)+'　第 '+((ci==null?0:ci)+1)+'/'+n+' 列';
    if(CYC_NAV.viaToday && ds!==ts)
      h += '　<span style="color:#b8860b">（今天 '+ts+' 暂无标注，已停在最近有标注的一天）</span>';
    const on = !!(onlyEl && onlyEl.checked);
    if(on && ci!=null)
      h += '　<span style="color:#2f6bff">已筛选 '+rows.length+' 只（当日有标注 / 全 '+all.length+' 只）</span>';
    navStat.innerHTML = h;
  }
  CYC_NAV.applyFilter=function(){
    const on = !!(onlyEl && onlyEl.checked), ci = CYC_NAV.focusCi;
    const before = rows.length;
    if(on && ci!=null){
      const gcol=CYC_NAV.colList[ci];
      rows = all.filter(o=>effCat(o.d,gcol)!=null);
    } else rows = all;
    CYC_NAV.rows = rows;
    if(mScroll && rows.length!==before) mScroll.scrollTop = 0;   // 行集合变了才回顶部（缩放时不打扰）
    resizeSpacer(); paintNavStat();
    if(CYC_NAV.redraw) CYC_NAV.redraw();
  };
  if(onlyEl && !onlyEl._wbBound){ onlyEl._wbBound=1; onlyEl.onchange=()=>CYC_NAV.applyFilter(); }

  function navSetDate(ds){
    if(!navY||!navM||!navD) return;
    navY.value=(ds||'').slice(0,4); fillMonths();
    navM.value=(ds||'').slice(0,7); fillDays();
    navD.value=ds||'';
  }
  function navTarget(){   /* 选了日→当天；只选月→该月首个交易日；只选年→该年首个 */
    const t=(navD&&navD.value)||(navM&&navM.value)||(navY&&navY.value)||'';
    if(!t) return null;
    const ds=CYC_NAV.dates;
    for(let i=0;i<ds.length;i++){ if((ds[i]||'')>=t) return i; }
    return ds.length-1;
  }
  /* 今天（本地时区）；轴末列可能是 Excel 预填的未来日期，所以「当日」必须按今天找，
     今天非交易日/无数据时停在"不晚于今天的最后一个交易日" */
  function cycTodayStr(){
    const t=new Date();
    return t.getFullYear()+'-'+String(t.getMonth()+1).padStart(2,'0')+'-'+String(t.getDate()).padStart(2,'0');
  }
  function colHasAny(ci){   // 该列是否至少有一只龙头有标注（用于跳过未来/未收盘的空列）
    const gcol=CYC_NAV.colList[ci]; if(gcol==null) return false;
    for(let i=0;i<all.length;i++){ if(effCat(all[i].d,gcol)!=null) return true; }
    return false;
  }
  function todayCi(){
    const ts=cycTodayStr(), ds=CYC_NAV.dates, n=ds.length; if(!n) return -1;
    let ci=-1;
    for(let i=0;i<n;i++){ if(ds[i]&&ds[i]<=ts) ci=i; else if(ds[i]>ts) break; }
    if(ci<0) ci=0;
    /* 今天这列一只都没标注（未收盘 / 休市 / 数据还没补），退到最近有标注的一天 */
    if(!colHasAny(ci)){ for(let j=ci-1;j>=0;j--){ if(colHasAny(j)) return j; } }
    return ci;
  }
  function gotoCi(ci, viaToday){
    const sc=CYC_NAV.scroll||mScroll, n=CYC_NAV.dates.length;
    if(!sc||!n||ci==null) return;
    ci=Math.max(0,Math.min(n-1,ci));
    CYC_NAV.focusCi=ci;
    const c=CYC_NAV.cell, g=CYC_NAV.GUT, w=sc.clientWidth;
    const maxSl=Math.max(0,(g+CYC_NAV.Ncol*c)-w);
    sc.scrollLeft=Math.max(0,Math.min(maxSl, g+ci*c-(w-g)/2+c/2));
    CYC_NAV.viaToday = !!viaToday;
    navSetDate(CYC_NAV.dates[ci]);
    CYC_NAV.applyFilter();   // 内部会重建状态栏并重绘；行集合随定位日变化
    if(CYC_NAV.syncChart) CYC_NAV.syncChart();   // 折线图同步：把聚焦竖线挪到这一天
  }
  function navInit(){
    fillYears();
    const n=CYC_NAV.dates.length; if(!n) return;
    const fresh = CYC_NAV.focusCi==null;
    const ci = fresh ? todayCi() : Math.min(CYC_NAV.focusCi,n-1);
    navSetDate(CYC_NAV.dates[ci]); gotoCi(ci, fresh);
  }
  if(!window.__cycNavBound){
    window.__cycNavBound=1;
    const gt=document.getElementById('cycGoToday'), gd=document.getElementById('cycGoDate');
    const pv=document.getElementById('cycPrevDay'), nx=document.getElementById('cycNextDay');
    if(gt) gt.onclick=()=>{ const n=CYC_NAV.dates.length; if(n) gotoCi(todayCi(), true); };
    if(gd) gd.onclick=()=>{ const ci=navTarget();
      if(ci==null){ if(navStat) navStat.innerHTML='<span style="color:#b8860b">请先选择年 / 月 / 日</span>'; return; } gotoCi(ci); };
    if(pv) pv.onclick=()=>{ const c=CYC_NAV.focusCi; gotoCi((c==null?todayCi():c)-1); };
    if(nx) nx.onclick=()=>{ const c=CYC_NAV.focusCi; gotoCi((c==null?todayCi():c)+1); };
    if(navY) navY.onchange=()=>{ fillMonths(); fillDays(); };
    if(navM) navM.onchange=()=>{ fillDays(); };
    if(navD) navD.onchange=()=>{ const ci=navTarget(); if(ci!=null) gotoCi(ci); };
  }

  function updateFixStat(){ if(fixStat) fixStat.textContent = '已手动修正 ' + Object.keys(OV).length + ' 处'; }
  function resizeSpacer(){ if(mSpacer){ mSpacer.style.width=(GUT+Ncol*cell)+'px'; mSpacer.style.height=(rows.length*cell)+'px'; } }
  function setupCanvas(){
    const dpr=Math.max(2, window.devicePixelRatio||1);
    const w=mScroll.clientWidth, h=mScroll.clientHeight;
    mCanvas.width=w*dpr; mCanvas.height=h*dpr;
    mCanvas.style.width=w+'px'; mCanvas.style.height=h+'px';
    const ctx=mCanvas.getContext('2d'); ctx.setTransform(dpr,0,0,dpr,0,0);
    return ctx;
  }
  function drawMatrix(){
    if(!mCanvas||!mScroll) return;
    const ctx=setupCanvas();
    const w=mScroll.clientWidth, h=mScroll.clientHeight;
    const sl=mScroll.scrollLeft, st=mScroll.scrollTop;
    ctx.clearRect(0,0,w,h);
    ctx.fillStyle='#ffffff'; ctx.fillRect(0,0,w,h);
    ctx.textBaseline='middle'; ctx.textAlign='center';
    const ci0=Math.max(0,Math.floor(sl/cell)), ci1=Math.min(Ncol-1,Math.ceil((sl+w)/cell));
    const ri0=Math.max(0,Math.floor(st/cell)), ri1=Math.min(rows.length-1,Math.ceil((st+h)/cell));
    const numMode = CYC_NAV.numMode !== false;
    const hvCi = CYC_NAV.hoverCi, hvRi = CYC_NAV.hoverRi;
    const fsN = Math.max(8, Math.min(13, Math.round(cell*0.6)));
    const fsS = Math.max(8, Math.min(11, Math.round(cell*0.55)));
    const pad = cell>=18 ? 2 : 1, rad = cell>=18 ? 4 : 2;

    if(!rows.length){   // 开了「只看当日有标注」但该日无标注
      ctx.fillStyle='#7a8194'; ctx.font='13px sans-serif'; ctx.textAlign='center';
      ctx.fillText('该交易日没有任何龙头标注（可取消勾选「只看当日有标注」）', GUT+(w-GUT)/2, HDR+46);
    }

    /* ---- 底层：斑马纹 + 悬停行/列 ---- */
    for(let ri=ri0;ri<=ri1;ri++){
      const y=HDR + ri*cell - st;
      ctx.fillStyle = (ri===hvRi) ? '#eef4ff' : (ri%2 ? '#fbfcfe' : '#ffffff');
      ctx.fillRect(GUT, y, w-GUT, cell);
    }
    if(hvCi!=null && hvCi>=ci0 && hvCi<=ci1){
      ctx.fillStyle='rgba(47,107,255,.06)';
      ctx.fillRect(GUT+hvCi*cell-sl, HDR, cell, h-HDR);
    }

    /* ---- 单元格：涨停用「连板色阶 + 数字」，其余用状态色块 ---- */
    for(let ri=ri0;ri<=ri1;ri++){
      const o=rows[ri]; if(!o) continue;
      const d=o.d, y=HDR + ri*cell - st;
      for(let ci=ci0;ci<=ci1;ci++){
        const gcol=colList[ci], x=GUT+ci*cell - sl;
        const k=effCat(d,gcol);
        if(!k) continue;
        const ch = (gcol>=d.c0 && gcol<d.c0+(d.s||'').length) ? (d.s[gcol-d.c0]||'') : '';
        if(k==='zt'){
          if(numMode){
            const b=(o.bmap&&o.bmap[gcol])||1;
            ctx.fillStyle=lvlBg(b); cycRR(ctx,x+pad,y+pad,cell-2*pad,cell-2*pad,rad); ctx.fill();
            ctx.fillStyle=lvlFg(b); ctx.font='600 '+fsN+'px ui-monospace,Consolas,monospace';
            ctx.fillText(String(b), x+cell/2, y+cell/2);
          }else{
            ctx.fillStyle=TINT.zt; ctx.fillRect(x+1,y+1,cell-2,cell-2);
            ctx.fillStyle=COLOR.zt; ctx.font='12px ui-monospace,Consolas,monospace';
            ctx.fillText(ch, x+cell/2, y+cell/2);
          }
        }else{
          const cs=CELL_ST[k]||CELL_ST.db;
          ctx.fillStyle=cs.bg; cycRR(ctx,x+pad,y+pad,cell-2*pad,cell-2*pad,rad); ctx.fill();
          const tx = numMode ? (cs.t||ch) : ch;
          if(tx && cell>=13){
            ctx.fillStyle=cs.fg;
            ctx.font=(numMode?'600 ':'')+fsS+'px ui-monospace,Consolas,monospace';
            ctx.fillText(tx, x+cell/2, y+cell/2);
          }
        }
      }
    }

    /* ---- 网格：月首实线、每 5 列中线、其余极淡 ---- */
    ctx.save(); ctx.lineWidth=1;
    for(let ci=ci0;ci<=ci1;ci++){
      const x=GUT+ci*cell - sl + 0.5;
      const isMon = ci>0 && (dates[ci]||'').slice(0,7)!==(dates[ci-1]||'').slice(0,7);
      ctx.strokeStyle = isMon ? '#c9d0dc' : (ci%5===0 ? '#e3e7ee' : '#f0f2f6');
      ctx.setLineDash(isMon?[]:[2,3]);
      ctx.beginPath(); ctx.moveTo(x,HDR); ctx.lineTo(x,h); ctx.stroke();
    }
    for(let ri=ri0;ri<=ri1;ri++){
      const y=HDR + ri*cell - st + 0.5;
      ctx.strokeStyle = (ri%5===0) ? '#e6eaf1' : '#f2f4f8';
      ctx.setLineDash([]);
      ctx.beginPath(); ctx.moveTo(GUT,y); ctx.lineTo(w,y); ctx.stroke();
    }
    ctx.restore();

    /* ---- 冻结表头：年 / 月 / 日 ---- */
    ctx.fillStyle='#f7f8fb'; ctx.fillRect(0,0,w,HDR);
    ctx.textAlign='center';
    // 年（合并跨列，交替底色）
    let i=ci0;
    while(i<=ci1){
      const yr=(dates[i]||'').slice(0,4);
      let j=i; while(j+1<=ci1 && (dates[j+1]||'').slice(0,4)===yr) j++;
      const x0=GUT+i*cell - sl, x1=GUT+(j+1)*cell - sl;
      ctx.fillStyle = (String(yr).slice(3)==='0'||String(yr).slice(3)==='2'||String(yr).slice(3)==='4'||String(yr).slice(3)==='6'||String(yr).slice(3)==='8') ? '#eceff5' : '#f6f7fb';
      ctx.fillRect(x0+1, 1, Math.max(0,x1-x0-2), HDR_Y-2);
      ctx.fillStyle='#7a8194'; ctx.font='bold 11px ui-monospace,monospace';
      ctx.fillText(yr, (x0+x1)/2, HDR_Y/2);
      ctx.strokeStyle='#c2c7d2'; ctx.setLineDash([]);
      ctx.beginPath(); ctx.moveTo(x1+0.5,0); ctx.lineTo(x1+0.5,HDR_Y); ctx.stroke();
      i=j+1;
    }
    // 月（合并跨列，胶囊底）
    let m=ci0;
    while(m<=ci1){
      const mo=(dates[m]||'').slice(0,7);
      let n=m; while(n+1<=ci1 && (dates[n+1]||'').slice(0,7)===mo) n++;
      const x0=GUT+m*cell - sl, x1=GUT+(n+1)*cell - sl;
      ctx.fillStyle='#e6eaf2'; cycRR(ctx,x0+2,HDR_Y+2,Math.max(2,x1-x0-4),HDR_M-4,3); ctx.fill();
      ctx.fillStyle='#4d5566'; ctx.font='bold 11px ui-monospace,monospace';
      ctx.fillText((+mo.slice(5))+'月', (x0+x1)/2, HDR_Y+HDR_M/2);
      ctx.strokeStyle='#c2c7d2'; ctx.beginPath(); ctx.moveTo(x1+0.5,HDR_Y); ctx.lineTo(x1+0.5,HDR_Y+HDR_M); ctx.stroke();
      m=n+1;
    }
    // 日（定位列蓝色胶囊 + 周几小字）
    for(let ci=ci0;ci<=ci1;ci++){
      const x=GUT+ci*cell - sl;
      const isF=(ci===CYC_NAV.focusCi);
      if(isF){ ctx.fillStyle='#2f6bff'; cycRR(ctx,x+1,HDR_Y+HDR_M+2,cell-2,HDR_D-5,3); ctx.fill(); }
      ctx.fillStyle = isF ? '#ffffff' : '#3a4150';
      ctx.font = (isF?'bold ':'')+'10px ui-monospace,monospace';
      ctx.fillText((dates[ci]||'').slice(8), x+cell/2, HDR_Y+HDR_M+6.5);
      if(cell>=16){
        ctx.fillStyle = isF ? 'rgba(255,255,255,.8)' : '#a8b0c0'; ctx.font='8px sans-serif';
        ctx.fillText(cycWeekName(dates[ci]).slice(1), x+cell/2, HDR_Y+HDR_M+14.5);
      }
      ctx.strokeStyle='#d5dae3'; ctx.beginPath(); ctx.moveTo(x+0.5,HDR_Y+HDR_M); ctx.lineTo(x+0.5,HDR); ctx.stroke();
    }
    ctx.strokeStyle='#c2c7d2'; ctx.setLineDash([]);
    ctx.beginPath(); ctx.moveTo(GUT,HDR_Y+0.5); ctx.lineTo(w,HDR_Y+0.5); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(GUT,HDR_Y+HDR_M+0.5); ctx.lineTo(w,HDR_Y+HDR_M+0.5); ctx.stroke();

    /* ---- 冻结左侧栏：色条 + 龙头/题材 + 峰值徽标 ---- */
    ctx.fillStyle='#fbfcfe'; ctx.fillRect(0,0,GUT,h);
    ctx.fillStyle='#eef1f6'; ctx.fillRect(0,0,GUT,HDR);
    ctx.textAlign='left'; ctx.fillStyle='#6b7285'; ctx.font='11px sans-serif';
    ctx.fillText('龙头 · 题材', 10, HDR/2);
    ctx.textAlign='right'; ctx.fillStyle='#8a91a3'; ctx.font='10px sans-serif';
    ctx.fillText('峰值', GUT-12, HDR/2);
    const BADGE=32;
    for(let ri=ri0;ri<=ri1;ri++){
      const o=rows[ri]; if(!o) continue;
      const d=o.d, y=HDR + ri*cell - st;
      if(ri===hvRi){ ctx.fillStyle='#e6eefc'; ctx.fillRect(0,y,GUT,cell); }
      ctx.fillStyle=lvlBg(o.peak||1); ctx.fillRect(0,y+1,3,cell-2);   // 峰值色条
      ctx.textAlign='left';
      ctx.font=(cell>=16?'12.5px sans-serif':'11px sans-serif'); ctx.fillStyle='#20242e';
      const name=String(d.n||'');
      const thW = d.t ? Math.min(52, Math.round((d.t+'').length*10+8)) : 0;
      ctx.fillText(cycEllip(ctx, name, GUT-14-BADGE-thW), 9, y+cell/2);
      let nw = 0; try{ nw = ctx.measureText(name).width; }catch(e){ nw = name.length*12; }
      if(d.t){
        ctx.font='10px sans-serif'; ctx.fillStyle='#9aa1b1';
        ctx.fillText(cycEllip(ctx, d.t, Math.max(10, GUT-14-BADGE-nw-6)), 9+nw+5, y+cell/2);
      }
      const pk=o.peak||0;
      if(pk>0 && cell>=14){
        const bx=GUT-10-BADGE, by=y+cell/2-7;
        ctx.fillStyle=lvlBg(pk); cycRR(ctx,bx,by,BADGE,14,7); ctx.fill();
        ctx.fillStyle=lvlFg(pk); ctx.font='600 10px ui-monospace,Consolas,monospace'; ctx.textAlign='center';
        ctx.fillText(pk+'板', bx+BADGE/2, by+7);
      }
    }
    ctx.textAlign='center';

    /* ---- 定位列高亮 ---- */
    if(CYC_NAV.focusCi!=null && CYC_NAV.focusCi>=ci0 && CYC_NAV.focusCi<=ci1){
      const fx=GUT+CYC_NAV.focusCi*cell - sl;
      ctx.save(); ctx.setLineDash([]);
      ctx.fillStyle='rgba(47,107,255,.06)'; ctx.fillRect(fx,HDR,cell,h-HDR);
      ctx.strokeStyle='rgba(47,107,255,.55)'; ctx.lineWidth=1.5; ctx.strokeRect(fx+0.75,HDR+1,cell-1.5,h-HDR-1);
      ctx.restore();
    }
    /* ---- 边框 ---- */
    ctx.strokeStyle='#d9dde6'; ctx.lineWidth=1; ctx.setLineDash([]);
    ctx.strokeRect(0.5,0.5,w-1,h-1);
    ctx.strokeStyle='#c2c7d2'; ctx.beginPath(); ctx.moveTo(GUT,0); ctx.lineTo(GUT,h); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(0,HDR); ctx.lineTo(w,HDR); ctx.stroke();
  }

  /* ---- 悬停提示 ---- */
  function cycHideTip(){ const t=document.getElementById('cycTip'); if(t) t.style.display='none'; }
  function cycShowTip(e, ci, ri){
    const tip=document.getElementById('cycTip'); if(!tip) return;
    const o=(CYC_NAV.rows||rows)[ri]; if(!o) return;
    const gcol=CYC_NAV.colList[ci], k=effCat(o.d,gcol), b=(o.bmap&&o.bmap[gcol])||0;
    const ds=CYC_NAV.dates[ci]||'';
    const mean = !k ? '无标注'
      : (k==='zt' ? '<b>'+b+' 板涨停</b>'
        : {zb:'炸板',dt:'跌停',db:'断板 / 非涨停',up:'趋势↑',down:'趋势↓',note:'注解（忽略）'}[k] || k);
    const ch = (gcol>=o.d.c0 && gcol<o.d.c0+(o.d.s||'').length) ? o.d.s[gcol-o.d.c0] : '';
    tip.innerHTML = '<b>'+esc(o.d.n)+'</b>'+(o.d.t?' <span class="t-sub">'+esc(o.d.t)+'</span>':'')
      + '<br>'+ds+' '+cycWeekName(ds)+'　'+mean
      + '<br><span class="t-sub">原始符号 '+esc(ch||'空')+'　最高 '+o.peak+' 板</span>';
    tip.style.display='block';
    const stage=document.querySelector('.cyc-matrix-stage');
    const sr = stage ? stage.getBoundingClientRect() : {left:0, top:0};
    const tw=tip.offsetWidth||170, th=tip.offsetHeight||46;
    let L=e.clientX-sr.left+14, T=e.clientY-sr.top+14;
    if(stage){ if(L+tw>stage.clientWidth-4) L=e.clientX-sr.left-tw-14; if(T+th>stage.clientHeight-4) T=e.clientY-sr.top-th-14; }
    tip.style.left=Math.max(0,L)+'px'; tip.style.top=Math.max(0,T)+'px';
  }

  if(mScroll && mCanvas && !mScroll._wbBound){
    mScroll._wbBound = 1;   // 事件只绑一次（刷新时重复绑定会导致拖动/滚动叠加）
    resizeSpacer();
    mScroll.addEventListener('scroll', drawMatrix);
    // Shift + 滚轮 = 横向滚动（1200 多列，纯拖动太累）
    mScroll.addEventListener('wheel', e=>{
      if(e.shiftKey && e.deltaY){ mScroll.scrollLeft += e.deltaY; e.preventDefault(); }
    }, {passive:false});
    window.addEventListener('resize', ()=>{ if(document.getElementById('cycMatrixCanvas')) drawMatrix(); });
    // 折线图 echarts 实例也要随窗口重绘
    if(el && !el._wbResize){ el._wbResize=1;
      window.addEventListener('resize', ()=>{ const c=echarts.getInstanceByDom(el); if(c) c.resize(); }); }
    if(zoomEl) zoomEl.addEventListener('input', ()=>{
      cell=parseInt(zoomEl.value,10)||20; CYC_NAV.cell=cell; resizeSpacer();
      if(CYC_NAV.focusCi!=null) gotoCi(CYC_NAV.focusCi); else drawMatrix(); });
    // 「连板数字」开关：色块+数字 ⇄ Excel 原始符号
    const numEl=document.getElementById('cycNumMode');
    if(numEl){
      if(CYC_NAV.numMode==null) CYC_NAV.numMode = !!numEl.checked;
      if(!numEl._wbBound){ numEl._wbBound=1; numEl.onchange=()=>{
        CYC_NAV.numMode=!!numEl.checked;
        const rd=CYC_NAV.redraw; if(rd) rd(); else drawMatrix(); }; }
    }
    // drag pan
    let dragging=false, lx=0, ly=0, moved=false;
    mCanvas.addEventListener('mousedown', e=>{ dragging=true; moved=false; lx=e.clientX; ly=e.clientY; });
    window.addEventListener('mouseup', ()=>dragging=false);
    mCanvas.addEventListener('mousemove', e=>{
      const rect=mCanvas.getBoundingClientRect();
      const mx=e.clientX-rect.left, my=e.clientY-rect.top;
      if(dragging){
        const dx=e.clientX-lx, dy=e.clientY-ly; if(Math.abs(dx)+Math.abs(dy)>3) moved=true;
        mScroll.scrollLeft-=dx; mScroll.scrollTop-=dy; lx=e.clientX; ly=e.clientY;
        cycHideTip();
        return;
      }
      /* 悬停：十字高亮 + 提示（读 CYC_NAV，不能用首次 render 捕获的 rows） */
      if(mx<CYC_NAV.GUT || my<HDR){ if(CYC_NAV.hoverCi!=null||CYC_NAV.hoverRi!=null){ CYC_NAV.hoverCi=null; CYC_NAV.hoverRi=null; cycHideTip(); drawMatrix(); } return; }
      const ci=Math.floor((mx-CYC_NAV.GUT+mScroll.scrollLeft)/CYC_NAV.cell);
      const ri=Math.floor((my-HDR+mScroll.scrollTop)/CYC_NAV.cell);
      const rws=CYC_NAV.rows||[];
      if(ci<0||ci>=CYC_NAV.Ncol||ri<0||ri>=rws.length){ if(CYC_NAV.hoverCi!=null){ CYC_NAV.hoverCi=null; CYC_NAV.hoverRi=null; cycHideTip(); drawMatrix(); } return; }
      if(ci!==CYC_NAV.hoverCi || ri!==CYC_NAV.hoverRi){ CYC_NAV.hoverCi=ci; CYC_NAV.hoverRi=ri; drawMatrix(); }
      cycShowTip(e, ci, ri);
    });
    mCanvas.addEventListener('mouseleave', ()=>{
      cycHideTip();
      if(CYC_NAV.hoverCi!=null||CYC_NAV.hoverRi!=null){ CYC_NAV.hoverCi=null; CYC_NAV.hoverRi=null; drawMatrix(); }
    });
    // click -> correction
    mCanvas.addEventListener('click', e=>{
      if(moved) return;
      const rect=mCanvas.getBoundingClientRect();
      const mx=e.clientX-rect.left, my=e.clientY-rect.top;
      if(mx<GUT||my<HDR) return;
      const ci=Math.floor((mx-GUT+mScroll.scrollLeft)/cell);
      const ri=Math.floor((my-HDR+mScroll.scrollTop)/cell);
      const rws=CYC_NAV.rows||rows;
      if(ci<0||ci>=Ncol||ri<0||ri>=rws.length) return;
      const gcol=colList[ci]; const d=rws[ri].d;
      if(gcol<d.c0||gcol>=d.c0+(d.s||'').length) return; // 空格区域不修正
      curFix={r:d.r, gcol:gcol, d:d};
      const ch=d.s[gcol-d.c0]; const k=effCat(d,gcol);
      fixTitle.innerHTML='<b>'+esc(d.n)+'</b> · '+dates[ci]+' · 原符号 <b style="color:'+(COLOR[k]||'#333')+'">'+esc(ch)+'</b>';
      fixSel.value = k||'';
      fixPop.style.display='block';
      fixPop.style.left=Math.min(e.clientX, window.innerWidth-260)+'px';
      fixPop.style.top=(e.clientY+8)+'px';
    });
    drawMatrix();
  }
  navInit();   // 打开即停在最新交易日；刷新后保持原定位列
  // popover actions
  if(fixPop){
    const okBtn=document.getElementById('cycFixOk'), cancelBtn=document.getElementById('cycFixCancel');
    if(okBtn) okBtn.onclick=()=>{
      if(!curFix) return;
      const v=fixSel.value;
      if(v==='__clear'){ delete OV[curFix.r+':'+curFix.gcol]; }
      else { OV[curFix.r+':'+curFix.gcol]=v; }
      saveOV(); updateFixStat(); fixPop.style.display='none'; drawMatrix(); drawChart(); drawTable();
    };
    if(cancelBtn) cancelBtn.onclick=()=>{ fixPop.style.display='none'; };
  }
  const clearBtn=document.getElementById('cycClearFix');
  if(clearBtn) clearBtn.onclick=()=>{ if(confirm('确认清除全部手动修正？')){ OV={}; saveOV(); updateFixStat(); drawMatrix(); drawChart(); drawTable(); } };
  const expBtn=document.getElementById('cycExportFix');
  if(expBtn) expBtn.onclick=()=>{
    const blob=new Blob([JSON.stringify(OV,null,1)],{type:'application/json'});
    const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='cyc_overrides.json'; a.click();
  };
  updateFixStat();

  /* ---- 每日自动追踪 ---- */
  (function renderAuto(){
    const atb = document.querySelector('#cycAutoTable tbody');
    const auto = C.auto || [];
    if(!atb) return;
    if(!auto.length){
      atb.innerHTML = '<tr><td colspan="5" style="color:var(--sub)">暂无自动追踪记录，交易日收盘后自动写入</td></tr>';
      return;
    }
    const rows = [];
    auto.slice().reverse().forEach(rec=>{ (rec.items||[]).forEach(it=>{ rows.push({date:rec.date, it:it}); }); });
    atb.innerHTML = rows.map(r=>{
      const mean={zt:'涨停',zb:'炸板',dt:'跌停',db:'断板'}[r.it.state]||r.it.state;
      const cls2={zt:'z',zb:'b',dt:'d',db:'s'}[r.it.state]||'s';
      return `<tr><td>${esc(r.date)}</td><td>${esc(r.it.name)} <span class="muted">${esc(r.it.code)}</span></td>`
           + `<td>${r.it.boards}板</td>`
           + `<td><span class="cycbar ${cls2}">${esc(r.it.symbol||'')}</span> ${esc(mean)}</td>`
           + `<td>${esc(r.it.theme||'')}</td></tr>`;
    }).join('');
  })();

  selYear.onchange = ()=>{ drawChart(); drawTable(); };
  inpTheme.oninput = ()=>{ drawChart(); drawTable(); };
  const minSel = document.getElementById('cycMin');
  if(minSel) minSel.onchange = ()=>{ drawChart(); drawTable(); };
  /* 折线条数 / 名称标注：只重绘折线，表格与矩阵不动（省一次上千行的重排） */
  const bindChart = (id, ev)=>{ const n=document.getElementById(id); if(n && !n._wbBound){ n._wbBound=1; n.addEventListener(ev, drawChart); } };
  bindChart('cycNameLbl','change'); bindChart('cycScope','change');
  bindChart('cycX1','change'); bindChart('cycX2','change');
  drawChart(); drawTable();
}
window.DASH.register('cycle','题材周期主图', renderCycle);

/* ============================================================================
   模块⑤ 市场情绪双图：上格 ADJS 涨跌家数柱（+涨停家数线），下格 竞价中位数 / 总体标准差
   —— ADJS = 上涨家数 − 下跌家数。通达信写的是
      ADJS:"999999$ADVANCE"-"999999$DECLINE", VOLSTICK;  同花顺写的是
      ADJS:上涨家数-下跌家数;  两边同一算法，这里照搬。
      参考线把两套公式自带的都画上：通达信 ±1000，同花顺 ±2500 / ±4000。
      A股红涨绿跌：ADJS>0（涨家数多）画红柱，<0 画绿柱。
   —— 两张图共用一条时间轴：拖动 / 滚轮 / 「近N日」按钮，上下同时移动、同时放大缩小。
   ========================================================================== */
let moodInst = null;
function renderMood(){
  const B = window.BREADTH;
  const el = document.getElementById('adjsChart');
  const kp = document.getElementById('breadthKpis');
  const hint = document.getElementById('breadthHint');
  if(!el || !kp) return;                       // 卡片被删掉就直接跳过

  /* ---- ① 顶部 KPI：当日涨跌家数 / 涨停跌停 / ADJS + 市场广度条 ---- */
  const strip = document.getElementById('breadthStrip');
  if(!B || !B.today){
    kp.innerHTML = '<div class="kpi hero"><div class="label">全市场涨跌家数</div><div class="value">--</div>'
      + '<div class="note">尚未生成 breadth.js（等待 15:20 自动抓取）</div></div>';
    if(strip) strip.innerHTML = '<div class="bs-legend"><span class="flat">等待 15:20 自动抓取涨跌家数…</span></div>';
  }else{
    const t = B.today;
    const U = v => '<span class="up">'+v+'</span>';
    const Dn = v => '<span class="down">'+v+'</span>';
    const SEP = '<span class="sep"> : </span>';
    const s = v => (v==null?'--':v);
    const arrow = t.adjs==null?'':(t.adjs>0?'▲':(t.adjs<0?'▼':''));
    const net = (t.adjs==null?'--':arrow+(t.adjs>0?'+':'')+t.adjs);
    const netCls = t.adjs>0?'up':(t.adjs<0?'down':'');
    const heroTint = t.adjs>0 ? 'background:linear-gradient(135deg,rgba(217,58,58,.12),rgba(217,58,58,.03));border-color:#f3cfcf'
      : t.adjs<0 ? 'background:linear-gradient(135deg,rgba(26,156,107,.12),rgba(26,156,107,.03));border-color:#c6e9d8'
      : 'background:linear-gradient(135deg,rgba(217,58,58,.06),rgba(26,156,107,.06))';
    /* 昨日涨停溢价率：看板「昨日涨停」名单今开相对昨收的平均涨幅（= lastDetail.detail 的 gap 均值） */
    let ztyj=null, ztyjN=0;
    const _LD=(window.SENTIMENT&&window.SENTIMENT.lastDetail&&window.SENTIMENT.lastDetail.detail)||[];
    const _zg=_LD.map(x=>x.gap).filter(v=>v!=null&&isFinite(v));
    if(_zg.length){ ztyj=_zg.reduce((a,b)=>a+b,0)/_zg.length; ztyjN=_zg.length; }
    kp.innerHTML =
      '<div class="kpi hero" style="'+heroTint+'"><div class="label">涨跌家数<span class="sub">涨 : 跌</span></div>'
        + '<div class="value v2">'+U(s(t.up))+SEP+Dn(s(t.down))+'</div>'
        + '<div class="note">沪深两市合计 · 涨−跌＝<b class="'+netCls+'">'+net+'</b>（'+(t.adjs>0?'涨方占优':(t.adjs<0?'跌方占优':'均衡'))+'）</div></div>'
      + '<div class="kpi"><div class="label">昨日涨停溢价率</div>'
        + '<div class="value '+(ztyj==null?'':(ztyj>=0?'up':'down'))+'">'+(ztyj==null?'--':(ztyj>0?'+':'')+ztyj.toFixed(2)+'%')+'</div>'
        + '<div class="note">昨日涨停股今开均值（n='+ztyjN+'）· 溢价&gt;0 次日有肉</div></div>'
      + '<div class="kpi"><div class="label">涨跌停家数<span class="sub">涨停 : 跌停</span></div>'
        + '<div class="value v2">'+U(s(t.zt))+SEP+Dn(s(t.dt))+'</div>'
        + '<div class="note">东财涨停 / 跌停池</div></div>'
      + '<div class="kpi"><div class="label">炸板率</div><div class="value">'+s(t.zbr==null?null:t.zbr+'%')+'</div>'
        + '<div class="note">炸板 '+s(t.zb)+' / 曾涨停 '+s(t.zb)+'+'+s(t.zt)+'</div></div>';
    if(strip){
      const up=t.up||0, dn=t.down||0, fl=t.flat==null?0:t.flat;
      const tot = up+dn+fl || 1;
      const upP = up/tot*100, dnP = dn/tot*100, flP = fl/tot*100;
      const v = t.adjs==null?null:(t.adjs>=2500?{t:'普涨',c:'up'}:t.adjs>=1000?{t:'偏多',c:'up'}:t.adjs>-1000?{t:'分化',c:'mid'}:t.adjs>-2500?{t:'偏空',c:'down'}:{t:'普跌·冰点',c:'down'});
      strip.innerHTML =
        '<div class="bs-bar"><div class="bs-up" style="width:'+upP.toFixed(2)+'%"></div>'
          + '<div class="bs-flat" style="width:'+flP.toFixed(2)+'%"></div>'
          + '<div class="bs-down" style="width:'+dnP.toFixed(2)+'%"></div></div>'
        + '<div class="bs-legend">'
          + '<span class="up"><span class="arr">▲</span> 上涨 '+up+' · '+upP.toFixed(1)+'%</span>'
          + '<span class="flat">■ 平 '+fl+'</span>'
          + '<span class="down"><span class="arr">▼</span> 下跌 '+dn+' · '+dnP.toFixed(1)+'%</span>'
          + (v?'<span class="bs-verdict '+v.c+'">'+v.t+'</span>':'')
        + '</div>';
    }
  }
  if(hint){
    const t = (B && B.today) || {};
    const when = ((B && B.updated) || '').replace('T',' ').slice(0,16);
    hint.textContent = '数据来源：涨跌家数＝' + (t.src || '东方财富涨跌停池')
      + (t.intraday ? ' · 当前为盘中数据' : ' · 已收盘')
      + (when ? ' · 更新于 ' + when : '')
      + ' · 中位数＝通达信（交易日 9:25 自动更新）';
  }

  /* ---- ② 两条数据源按日期并集对齐：缺的那边留空，不补不造 ---- */
  const bHist = ((B && B.hist) || []).filter(r=>r && r.d);
  const bMap = {}; bHist.forEach(r=>{ bMap[r.d] = r; });
  const recs = getRecs();
  const mMap = {}; recs.forEach(r=>{ mMap[r.date] = r; });
  const alld = Array.from(new Set(bHist.map(r=>r.d).concat(recs.map(r=>r.date)))).sort();
  const num = v => (v==null || v==='' || isNaN(v)) ? null : Number(v);
  if(!alld.length){
    el.innerHTML = '<div style="color:#7a8194;font-size:13px;padding:16px 0">暂无历史序列</div>';
    return;
  }
  const adjs = alld.map(d=> bMap[d] ? num(bMap[d].adjs) : null);
  const med  = alld.map(d=> mMap[d] ? num(mMap[d].median) : null);
  const std  = alld.map(d=> mMap[d] ? num(mMap[d].std)    : null);
  if(hint){
    const wA = bHist.filter(r=>r.adjs!=null);
    hint.textContent += ' · ADJS 已积累 ' + wA.length + ' 个交易日'
      + (wA.length ? '（自 '+wA[0].d+' 起，15:20 逐日累积）' : '（今天开始逐日累积）')
      + ' · 中位数 ' + recs.length + ' 个交易日';
  }

  if(!moodInst){
    const old = echarts.getInstanceByDom(el); if(old) old.dispose();
    el.removeAttribute('_echarts_instance_');
    moodInst = echarts.init(el);
  }
  const N = alld.length;
  const sv = Math.max(0, N - 20);              // 默认窗口：最近 20 个交易日

  /* 时间轴一致：上下两格同一条 category 轴，boundaryGap 一致才能让线和柱对齐 */
  const catAxis = (gi, showLabel) => ({
    type:'category', gridIndex:gi, data:alld, boundaryGap:true,
    axisLine: showLabel ? {lineStyle:{color:'#d9dde6'}} : {show:false},
    axisTick:{show:false},
    axisLabel: showLabel
      ? {formatter:v=>String(v).slice(5), fontSize:10, color:'#7a8194', interval:'auto'}
      : {show:false},
    axisPointer:{label:{show:false}}
  });

  moodInst.setOption({
    animation:false,
    /* 十字准线联动：鼠标在任一格，上下两格同时出现指示线，读数一眼对得上 */
    tooltip:{ trigger:'axis',
      axisPointer:{type:'line', lineStyle:{color:'#b9c0cf',width:1}, link:[{xAxisIndex:'all'}]},
      backgroundColor:'rgba(26,29,36,.95)', borderWidth:0, padding:[8,10], textStyle:{color:'#fff',fontSize:12},
      formatter: ps=>{
        if(!ps || !ps.length) return '';
        const i = ps[0].dataIndex, d = alld[i] || '';
        const b = bMap[d], m = mMap[d];
        let s = '<b>'+d+'</b>';
        if(b){
          if(b.adjs==null) s += '<br><span style="color:#a8b0c0">当日涨跌家数未抓取（涨停家数来自 ladderhist 回填）</span>';
          else s += '<br>涨 <b style="color:#ffd9d9">'+b.up+'</b>　跌 <b style="color:#bfe8d8">'+b.down+'</b>　平 '+(b.flat==null?'--':b.flat)
                 + '<br>ADJS <b style="color:'+(b.adjs>0?'#ffd9d9':'#bfe8d8')+'">'+(b.adjs>0?'+':'')+b.adjs+'</b>　涨占比 '+(b.upPct==null?'--':b.upPct+'%');
          if(b.zt!=null) s += '<br>涨停 '+b.zt+' 家'+(b.dt!=null?('　跌停 '+b.dt+' 家'):'')+(b.zbr!=null?('　炸板率 '+b.zbr+'%'):'');
        }
        if(m) s += '<br><span style="color:#ffb3b3">竞价中位数</span> <b style="color:#ff8f8f">'+Number(m.median).toFixed(2)+'%</b>'
                + (m.std==null ? '' : '　<span style="color:#c9cedb">总体标准差 '+Number(m.std).toFixed(2)+'</span>');
        return s;
      }},
    legend:{ type:'scroll', data:['竞价中位数(%)','总体标准差(%)','ADJS（涨−跌）'],
      top:0, itemHeight:9, itemWidth:16, textStyle:{fontSize:11} },
    /* 三段式：上格 中位数主图（大）→ 中带 ADJS 柱（矮，压缩在主图与时间轴之间）→ 时间轴沉底
       两格左右边界一致（left/right 相同）→ 上下刻度对齐，看盘不串位 */
    grid:[
      {left:64, right:30, top:34, height:'46%'},        // 上格：竞价中位数 + 总体标准差
      {left:64, right:30, top:'62%', height:'24%'}      // 下带：ADJS 柱（只留一项，不给涨停家数占位）
    ],
    xAxis:[ catAxis(0,false), catAxis(1,true) ],        // 时间轴标签挂最底下的 ADJS 带
    yAxis:[
      { gridIndex:0, type:'value', name:'%', scale:true, nameTextStyle:{fontSize:10,color:'#7a8194',padding:[0,0,0,-12]},
        axisLabel:{fontSize:10,color:'#8a91a3'}, splitLine:{show:true,lineStyle:{color:'#eef0f5'}} },
      { gridIndex:1, type:'value', name:'ADJS 家数', nameTextStyle:{fontSize:10,color:'#7a8194',padding:[0,0,0,-16]},
        axisLabel:{fontSize:10,color:'#8a91a3'}, splitLine:{show:true,lineStyle:{color:'#eef0f5'}} }
    ],
    dataZoom:[
      { type:'inside', xAxisIndex:[0,1], startValue:sv, endValue:N-1, filterMode:'none',
        zoomOnMouseWheel:true, moveOnMouseMove:true, throttle:60 },
      { type:'slider', xAxisIndex:[0,1], startValue:sv, endValue:N-1, filterMode:'none',
        height:18, bottom:8, borderColor:'transparent', backgroundColor:'#f5f6fa',
        fillerColor:'rgba(47,107,255,.12)', handleStyle:{color:'#fff',borderColor:'#b9c0cf'},
        textStyle:{fontSize:10,color:'#7a8194'}, labelFormatter:v=>String(alld[v]||'').slice(5) }
    ],
    series:[
      { name:'ADJS（涨−跌）', type:'bar', xAxisIndex:1, yAxisIndex:1, data:adjs, barMaxWidth:18,
        itemStyle:{color:p=> (p.value==null?'transparent':(p.value>=0?'#d93a3a':'#1a9c6b'))},
        label:{show:true,position:'top',fontSize:9,color:'#8a91a3',
               formatter:p=> (p.value==null||p.value===0)?'':((p.value>0?'+':'')+p.value)},
        labelLayout:{hideOverlap:true},
        markLine:{silent:true,symbol:'none',lineStyle:{type:'solid',width:1},
          label:{fontSize:9,position:'insideEndTop',color:'#98a0b3'},
          data:[
            {yAxis:0,lineStyle:{color:'#c3c9d6'},label:{formatter:'0',position:'end'}},
            {yAxis:1000,lineStyle:{color:'#e08b1a'},label:{formatter:'+1000'}},
            {yAxis:-1000,lineStyle:{color:'#e08b1a'},label:{formatter:'-1000',position:'insideEndBottom'}},
            {yAxis:2500,lineStyle:{color:'#9aa1b1',type:'dashed'},label:{formatter:'+2500'}},
            {yAxis:-2500,lineStyle:{color:'#9aa1b1',type:'dashed'},label:{formatter:'-2500',position:'insideEndBottom'}}
          ]}},
      { name:'竞价中位数(%)', type:'line', xAxisIndex:0, yAxisIndex:0, data:med, smooth:true, connectNulls:false,
        symbol:'circle', symbolSize:5, lineStyle:{width:2.6,color:'#d93a3a'}, itemStyle:{color:'#d93a3a'},
        label:{show:true,position:'top',distance:4,fontSize:10,color:'#d93a3a',fontWeight:600,
               formatter:p=> Number(p.value).toFixed(2)},
        labelLayout:{hideOverlap:true},
        areaStyle:{color:{type:'linear',x:0,y:0,x2:0,y2:1,
          colorStops:[{offset:0,color:'rgba(217,58,58,.16)'},{offset:1,color:'rgba(217,58,58,0)'}]}},
        markLine:{silent:true,symbol:'none',lineStyle:{type:'dashed',color:'#c3c9d6'},
          label:{formatter:'0',fontSize:9,color:'#98a0b3'},data:[{yAxis:0}]} },
      { name:'总体标准差(%)', type:'line', xAxisIndex:0, yAxisIndex:0, data:std, smooth:true, connectNulls:false,
        symbol:'circle', symbolSize:4, lineStyle:{width:1.6,color:'#8a91a3',type:'dashed'}, itemStyle:{color:'#8a91a3'},
        label:{ show:true, position:'bottom', distance:3, fontSize:10, color:'#8a91a3',
                formatter:p=> (p.value==null ? '' : Number(p.value).toFixed(2)) } }
    ]
  }, true);

  /* ---- 快捷区间：全部 / 近60日 / 近20日 / 近10日 —— 上下两格一起动 ---- */
  const zb = document.getElementById('zoomBtns');
  if(zb && N>0){
    zb.querySelectorAll('button').forEach(b=>{
      if(b._wbZoom) return;                    // 只绑一次，刷新后不叠加监听
      b._wbZoom = 1;
      b.onclick = ()=>{
        const c = echarts.getInstanceByDom(el); if(!c) return;
        const w = +b.dataset.w;
        const start = w ? Math.max(0, N - w) : 0;
        c.dispatchAction({type:'dataZoom', dataZoomIndex:[0,1], startValue:start, endValue:N-1});
        zb.querySelectorAll('button').forEach(x=>x.classList.remove('on'));
        b.classList.add('on');
      };
    });
  }
  // resize 只绑一次（刷新时重复绑定会叠加监听）
  if(!el._wbResize){ el._wbResize=1; window.addEventListener('resize',()=>{ const c=echarts.getInstanceByDom(el); if(c) c.resize(); }); }
}
window.DASH.register('breadth','市场情绪双图·ADJS×中位数', renderMood);
window.DASH.register('trend','市场情绪双图·ADJS×中位数', renderMood);   // 老按钮 / 自动引擎仍按 trend 调用
renderMood();

/* ============================================================================
   模块⑧ 30 日涨幅榜 · 异动监控（逼近监管红线）· 监管票列表
   数据：newmod_data.js（同花顺 iFinD 选股/指标/公告检索）—— 缺数据就明说，不编。
   异动口径（用户确认）：连续 10 个交易日累计涨幅 → 100%、连续 30 个交易日 → 200%
   为交易所「严重异常波动」红线；入池＝10 日涨幅 ≥50% 或 30 日涨幅 ≥100%。
   ========================================================================== */
function renderNewMod(){
  const mv = document.getElementById('nmMove'), rg = document.getElementById('nmReg'), box = document.getElementById('nmR30Box');
  if(!mv || !rg || !box) return;
  const D = window.NEWMOD;
  const esc = s => String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
  const pct = v => (v==null||v==='') ? '--' : (v>0?'+':'')+Number(v).toFixed(2)+'%';
  const tags = (s,max) => String(s||'').split(',').filter(Boolean).slice(0, max||99)
    .map(x=>'<span style="display:inline-block;background:#f2f5fb;border:1px solid #e2e8f4;color:#4b556b;'
      +'border-radius:4px;padding:0 5px;margin:1px 3px 1px 0;font-size:11px">'+esc(x)+'</span>').join('');
  const hint = document.getElementById('newmodHint');
  if(!D || (!D.r30 || !D.r30.length)){
    mv.innerHTML = '<h3>异动监控</h3><div class="muted">尚未生成 newmod_data.js</div>';
    rg.innerHTML = '<h3>监管票</h3><div class="muted">尚未生成 newmod_data.js</div>';
    box.innerHTML = '<h3>30 日涨幅榜</h3><div class="muted">尚未生成 newmod_data.js</div>';
    return;
  }
  if(hint) hint.textContent = '数据来源：同花顺 iFinD（选股 / 指标 / 公告检索）· 30 日区间 '
    + '2026-08-27→' + (D.updated||'') + '、10 日区间 2026-09-24→' + (D.updated||'')
    + ' · 涨幅榜 ' + D.r30.length + ' 只 · 异动 ' + (D.move||[]).length + ' 只 · 监管公告 ' + (D.reg||[]).length + ' 条';

  /* ---- ① 异动监控：按「距红线进度」排序（10日/100 与 30日/200 取大者） ---- */
  const rows = (D.move||[]).map(r=>({code:r[0], name:r[1], p10:r[2], p30:r[3], th:r[4]}));
  rows.forEach(r=>{ r.prog = Math.max(r.p10/100, r.p30/200); });
  rows.sort((a,b)=> b.prog - a.prog);
  mv.innerHTML = '<h3>异动监控 <small>距「严重异常波动」红线：10 日 → 100%、30 日 → 200%</small></h3>'
    + '<table class="nm-table"><thead><tr><th>代码</th><th>名称</th><th class="num">10 日涨幅</th>'
    + '<th class="num">30 日涨幅</th><th>距红线进度</th></tr></thead><tbody>'
    + rows.map(r=>{
        const a = Math.min(100, r.p10), b = Math.min(100, r.p30/2);
        return '<tr><td class="code">'+esc(r.code)+'</td><td class="nm-name">'+esc(r.name)+'</td>'
          + '<td class="num'+(r.p10>=80?' nm-warn':' nm-up')+'">'+pct(r.p10)+'</td>'
          + '<td class="num'+(r.p30>=160?' nm-warn':' nm-up')+'">'+pct(r.p30)+'</td>'
          + '<td><div class="nm-bar"><i class="r" style="width:'+a.toFixed(1)+'%"></i></div>'
          + '<div class="nm-bar-tx">10日 '+a.toFixed(1)+'% / 100%</div>'
          + '<div class="nm-bar" style="margin-top:3px"><i class="o" style="width:'+b.toFixed(1)+'%"></i></div>'
          + '<div class="nm-bar-tx">30日 '+b.toFixed(1)+'% / 200%</div></td></tr>';
      }).join('')
    + '</tbody></table>'
    + (rows.length ? '' : '<div class="muted">当前没有逼近红线的票</div>');

  /* ---- ② 监管票列表：交易所异常波动 / 严重异常波动公告 ---- */
  const reg = D.reg||[];
  rg.innerHTML = '<h3>监管票列表 <small>交易所异常波动 / 严重异常波动公告</small></h3>'
    + '<table class="nm-table"><thead><tr><th>代码</th><th>名称</th><th>日期</th><th>要点</th></tr></thead><tbody>'
    + reg.map(r=>'<tr><td class="code">'+esc(r[0])+'</td><td class="nm-name">'+esc(r[1])+'</td>'
        + '<td class="num">'+esc(String(r[3]).slice(5))+'</td><td class="nm-tags">'+esc(r[4])+'</td></tr>').join('')
    + '</tbody></table>'
    + (reg.length ? '' : '<div class="muted">暂无监管公告</div>');

  /* ---- ③ 30 日涨幅榜：主板、剔 ST、区间涨幅 > 30%，按涨幅降序 ---- */
  const list = (D.r30||[]).slice().sort((a,b)=> b[2]-a[2]);
  box.innerHTML = '<h3>30 日涨幅榜 <small>沪深主板 · 剔除 ST 与退市股 · 区间涨幅 &gt; 30%，共 '
    + list.length + ' 只 · 按涨幅降序</small></h3>'
    + '<div class="nm-scroll"><table class="nm-table"><thead><tr><th>#</th><th>代码</th><th>名称</th>'
    + '<th class="num">30 日涨幅</th><th>题材（同花顺概念）</th></tr></thead><tbody>'
    + list.map((r,i)=>'<tr><td class="num">'+(i+1)+'</td><td class="code">'+esc(r[0])+'</td>'
        + '<td class="nm-name">'+esc(r[1])+'</td><td class="num nm-up">'+pct(r[2])+'</td>'
        + '<td class="nm-tags">'+tags(r[3])+'</td></tr>').join('')
    + '</tbody></table></div>';
}
window.DASH.register('newmod','30日涨幅榜·异动监控·监管票', renderNewMod);
renderNewMod();

/* ============================================================================
   赚钱效应排行 · 30 日涨幅榜拆解（连板天梯下方）
   样本：earn_data.js（由 _build_earn.js 生成）
     · 52 只样本＝30 日涨幅榜（沪深主板 · 非ST · 区间 08-27→10-15 涨幅>30%）
     · 形态标签由 ladderhist.js 连板梯队快照（08-27→10-14）推导：
       分段＝连续连板段，≥2 段即「断板反包」；竞价 gap<0 记「竞价低吸」；gap≤-3% 记「核按钮反包」；
       反复开板 openCnt≥2；换手板/高开板≥2 次记「弱转强换手」；无连板记录记「趋势型」
     · 地域/市值来自同花顺 iFinD；五行＝数字能量主星之五行（基础读法）
   ========================================================================== */
function renderEarn(){
  const box = {
    ov:document.getElementById('earnOverview'), form:document.getElementById('earnForm'),
    wx:document.getElementById('earnWx'), theme:document.getElementById('earnTheme'),
    region:document.getElementById('earnRegion'), cap:document.getElementById('earnCap'),
    top:document.getElementById('earnTop')
  };
  if(!box.ov || !box.form) return;
  /* 核心逻辑：只要上游数据（30 日涨幅榜 / 连板梯队快照）与算法都在，就【现算】——
     这样点「刷新」是真更新；earn_data.js 只是快照，供缺数据文件时兜底。 */
  let E = null, live = false;
  try{
    if(window.EARN_CORE && window.NEWMOD && window.LADDERHIST && window.M5 && window.M5.mainStar){
      E = window.EARN_CORE.build({
        newmod: window.NEWMOD,
        ladderhist: window.LADDERHIST,
        mainStar: window.M5.mainStar,
        mcap: window.MCAP,                 /* 市值口径唯一来源＝实际流通市值 */
        updated: window.NEWMOD.updated
      });
      live = true;
    }
  }catch(err){ console.error('[赚钱效应] 现算失败，退回快照', err); }
  if(!E) E = window.EARN;
  const esc = s => String(s==null?'':s).replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
  const pc = v => (v>0?'+':'')+Number(v).toFixed(2)+'%';
  const WX_COLOR = {'木':'#2e9e5b','火':'#d93a3a','土':'#c08a3e','金':'#8b93a6','水':'#2f6bff'};
  const hint = document.getElementById('earnHint');
  if(!E || !E.stocks || !E.stocks.length){
    ['form','wx','theme','region','cap','top'].forEach(k=>{ if(box[k]) box[k].innerHTML='<div class="muted">尚未生成 earn_data.js</div>'; });
    box.ov.innerHTML = '<span>缺少 earn_data.js —— 请先运行 <b>_build_earn.js</b> 生成数据</span>';
    return;
  }
  const W = E.window || {};
  const miss = E.missing || {region:[], mv:[]};
  const missAll = Array.from(new Set((miss.region||[]).concat(miss.mv||[])));
  if(hint) hint.textContent = '样本＝30 日涨幅榜 ' + E.stocks.length + ' 只（沪深主板 · 非ST · 数据日 '
    + (E.updated||'') + '）· 形态窗口 ' + W.from + '→' + W.to + '（' + W.days + ' 个交易日，连板梯队快照推导）'
    + ' · 地域＝同花顺 iFinD · 市值＝实际流通市值（同花顺自由流通口径，'
    + ((E.mcap && E.mcap.updated) || '') + '）· 五行＝数字能量主星'
    + (live ? ' · 已按最新数据现算' : ' · 快照数据（上游文件缺失）')
    + (missAll.length ? ' · 待补 iFinD：' + missAll.length + ' 只' : '');

  const R = E.rank || {};
  const FBD = E.formByDay || {};
  /* 数字能量小标签：统一走 wuxing5.js 的 M5.energyHtml，保证与连板梯队/连板天梯同口径 */
  const egOf = code => (window.M5 && window.M5.energyHtml) ? window.M5.energyHtml(code) : '';
  /* 榜单口径文案也从数据层 SORT_SPEC 注入，避免视图与数据两套说法 */
  const SPEC = E.sortSpec || {};
  const SPEC_EL = {form:'eaSpecForm', wx:'eaSpecWx', cap:'eaSpecCap', theme:'eaSpecTheme', region:'eaSpecRegion'};
  Object.keys(SPEC_EL).forEach(k => {
    const el = document.getElementById(SPEC_EL[k]);
    if (el && SPEC[k] && SPEC[k].label) el.textContent = SPEC[k].label + (k==='form' ? ' · 固定花名册（样本为 0 也占一行）' : '');
  });
  /* 档位名带括号（如 大盘(>100亿)）：主名一行、括号里的区间另起一行，避免被挤压或截断 */
  const kParts = k => {
    const s = String(k==null ? '' : k);
    const g = s.match(/^(.*?)\s*[（(]\s*(.+?)\s*[）)]$/);
    return g ? { main: g[1], sub: '（' + g[2] + '）' } : { main: s, sub: '' };
  };
  const kHtml = k => {
    const p = kParts(k);
    return p.sub ? '<span class="ea-k ea-k2">' + esc(p.main) + '<em>' + esc(p.sub) + '</em></span>'
                 : '<span class="ea-k">' + esc(p.main) + '</span>';
  };
  /* 形态榜是「固定花名册」：样本为 0 也保留一行（本期空着），等批量出现时自动上榜 */
  const rankOf = (arr, unit, withDays, showMed) => (arr||[]).map((x,i)=>{
    const maxAvg = Math.max.apply(null, (arr||[]).map(y=>y.avg||0)) || 1;
    const medal = i<3 ? ' r'+(i+1) : '';
    if(x.empty){
      return '<div class="ea-row ea-empty">'
        + '<div class="ea-line">'+kHtml(x.k)
        + '<span class="ea-bar"><i style="width:0%"></i></span>'
        + '<span class="ea-v">— <em>· 本期 0 只</em></span></div>'
        + '<div class="ea-ex">◌ 本期无样本 · 该赚钱效应一旦批量出现会自动上榜</div></div>';
    }
    const w = Math.max(8, Math.round((x.avg||0) / maxAvg * 100));
    const d = withDays ? (FBD[x.k] || null) : null;
    const medHtml = (showMed && x.n>1)
      ? ' <em class="ea-med">中位 '+(x.med!=null?fmt(x.med):'--')+'%</em>' : '';
    const thinHtml = x.thin ? '<span class="ea-thin" title="样本不足 '+((E.overview&&E.overview.minN)||5)+' 只：均涨幅易被单只拉高，不参与「最赚钱/最强」冠军评定">样本不足 '+(x.n||0)+' 只</span>' : '';
    const dayLine = (d && d.peak)
      ? '<span class="ea-days"><i>窗口 <b>'+d.days.length+'</b> 日</i><i>峰值 <b>'+d.peak+'</b> 只</i><i>连续 <b>'+d.run+'</b> 日</i>'
        + (d.batch ? '<i>批量 <b>'+d.batch+'</b> 天</i>' : '') + '</span>'
      : '';
    const exHtml = (x.top||[]).slice(0,3).map(t=>
      '<div class="ea-exrow">'
      + '<span class="ea-tag'+(i<3?' hot':'')+'">'+esc(t[0])+'<b>'+pc(t[1])+'</b></span>'
      + '<span class="ea-exeg">'+egOf(t[2])+'</span></div>').join('');
    return '<div class="ea-row'+(i===0?' r1':'')+(' rank-'+(i+1))+'"'+(x.thin?' data-thin="1"':'')+'>'
      + '<div class="ea-line"><span class="ea-rank'+medal+'">'+(i+1)+'</span>'
      + kHtml(x.k)
      + '<span class="ea-bar"><i style="width:'+w+'%"></i></span>'
      + '<span class="ea-v"><b>'+pc(x.avg)+'</b> <em>· '+x.n+(unit||'只')+'</em>'+medHtml+'</span>'
      + thinHtml + '</div>'
      + '<div class="ea-ex">'+exHtml+(dayLine?'<span class="ea-days-row">'+dayLine+'</span>':'')+'</div>'
      + '</div>';
  }).join('') || '<div class="muted">无</div>';

  box.form.innerHTML   = rankOf(R.form, '只', true, true);
  box.wx.innerHTML     = rankOf(R.wx);
  box.theme.innerHTML  = rankOf(R.theme);
  box.region.innerHTML = rankOf(R.region);
  box.cap.innerHTML    = rankOf(R.cap, '只', false, true);

  /* ⑤ 概览条：统计卡片 */
  const o = E.overview || {};
  const bestForm = o.topForm, bestTheme = o.topTheme, bestRegion = o.topRegion, bestWx = o.topWx, bestCap = o.topCap;
  const small = o.smallCap;
  const capTop = bestCap ? kParts(bestCap.k) : null;
  const capTopHtml = capTop ? esc(capTop.main) + (capTop.sub ? '<small class="sub2">' + esc(capTop.sub) + '</small>' : '') : '—';
  const tile = (k, v, ac, extra) => '<div class="ea-st" style="--ac:'+(ac||'#334155')+'"><em>'+k+'</em><b>'+v+'</b>'+(extra||'')+'</div>';
  box.ov.innerHTML =
      tile('样本', o.n+'<small>只</small>', '#334155')
    + tile('平均涨幅', pc(o.avg), '#d93a3a')
    + tile('中位数', pc(o.med), '#e08b1a')
    + tile('最强形态 <em>按均涨幅</em>', esc(bestForm?bestForm.k:'—'), '#d93a3a', (bestForm?'<small>'+pc(bestForm.avg)+'</small>':''))
    + tile('最集中题材 <em>按家数</em>', esc(bestTheme?bestTheme.k:'—'), '#e08b1a', (bestTheme?'<small>'+bestTheme.n+'只</small>':''))
    + tile('最集中地域 <em>按家数</em>', esc(bestRegion?bestRegion.k:'—'), '#2e9e5b', (bestRegion?'<small>'+bestRegion.n+'只</small>':''))
    + tile('最旺五行 <em>按家数</em>', '<span style="color:'+(WX_COLOR[bestWx&&bestWx.k]||'#333')+'">'+esc(bestWx?bestWx.k:'—')+'</span>', (WX_COLOR[bestWx&&bestWx.k]||'#334155'), (bestWx?'<small>'+bestWx.n+'只</small>':''))
    + tile('最赚钱市值档 <em>按均涨幅</em>', capTopHtml, '#8b5cf6', (bestCap?'<small>'+pc(bestCap.avg)+'</small>':''))
    + tile(esc(o.smallLabel||'实际流通<20亿'), small+'<small>只</small>', '#6366f1');

  /* ⑥ 龙头画像：Top 15 各维度标签 */
  const top = E.stocks.slice().sort((a,b)=> b.g - a.g).slice(0,15);
  box.top.innerHTML = '<div class="nm-scroll" style="max-height:560px"><table class="ea-table"><thead><tr>'
    + '<th>#</th><th>代码</th><th>名称</th><th class="num">30日</th><th>地域</th><th>五行</th>'
    + '<th class="num" title="实际流通市值＝自由流通股本×股价（同花顺自由流通口径，扣除限售股与大股东等非自由流通筹码），单位亿元">实际流通(亿)</th><th>形态标签</th><th>题材（主线）</th></tr></thead><tbody>'
    + top.map((s,i)=>{
        const wxc = WX_COLOR[s.wx] || '#8b93a6';
        const ths = (s.th||[]).filter(t=>['融资融券','沪股通','深股通','上证380成份股'].indexOf(t)<0).slice(0,3);
        const medal = i<3 ? ' r'+(i+1) : '';
        const eg = (window.M5 && window.M5.energyHtml) ? window.M5.energyHtml(s.c) : '';
        return '<tr><td class="num"><span class="ea-rank'+medal+'" style="flex:none;width:17px;height:17px">'+(i+1)+'</span></td>'
          + '<td class="code">'+esc(s.c)+'</td>'
          + '<td class="ea-nm">'+esc(s.n)+(eg?'<div class="ea-eg">'+eg+'</div>':'')+'</td>'
          + '<td class="num" style="color:#d93a3a;font-weight:800">▲ '+pc(s.g)+'</td>'
          + '<td>'+esc(s.r)+'</td>'
          + '<td><span class="ea-tag wx" style="background:'+wxc+'">'+esc(s.wx||'—')+'</span>'
          +   '<small style="color:var(--sub)">'+esc(s.star||'')+'</small></td>'
          + '<td class="num">'+(s.fm==null?'--':s.fm.toFixed(1))+'</td>'
          + '<td>'+(s.form||[]).map(f=>'<span class="ea-tag'+(['连板妖股','断板反包','反核'].indexOf(f)>=0?' hot':'')+'">'+esc(f)+'</span>').join('')+'</td>'
          + '<td style="max-width:230px">'+ths.map(t=>'<span class="ea-tag">'+esc(t)+'</span>').join('')+'</td></tr>';
      }).join('')
    + '</tbody></table></div>'
    + '<div class="muted" style="font-size:11.5px;margin-top:6px;line-height:1.7">'
    + '形态口径：连板妖股＝最高≥5板 · 中位连板＝3-4板 · 二板接力＝2板 · 断板反包＝区间内出现≥2段连续连板'
    + ' · 低吸＝有交易日竞价低开仍封板 · 反核＝竞价低开≥3%后仍封板 · 弱转强＝换手板/高开板≥2次'
    + ' · 趋势＝区间内无连板记录（走趋势上涨）</div>';
}
window.DASH.register('earn','赚钱效应排行', renderEarn);
renderEarn();

/* ============================================================================
   模块⑨ 24 小时股票人气榜（同花顺 / 东方财富股吧 / 淘股吧）
   —— 只要排名和股票名；取不到就明说取不到，不编数据。
   ========================================================================== */
function renderHotRank(){
  const box = document.getElementById('hotCols');
  if(!box) return;
  const H0 = window.HOTRANK;
  if(!H0 || !H0.sources){ box.innerHTML = '<div class="muted">尚未生成 hotrank.js（等待自动抓取）</div>'; return; }
  const ORDER = ['ths','guba','tgb'];
  const COLORS = {ths:'#c0392b', guba:'#e08b1a', tgb:'#2f6bff'};
  box.innerHTML = ORDER.map(k=>{
    const s = H0.sources[k] || {name:k,items:[],err:'未配置'};
    const head = '<div style="font-weight:600;color:'+COLORS[k]+';font-size:13px;margin-bottom:2px">'+s.name+'</div>'
      + '<div class="src">'+(s.err?('✗ '+s.err):('更新 '+(H0.updated||'').replace('T',' ').slice(0,16)))+(s.note?('　'+s.note):'')+'</div>';
    if(!s.items.length) return '<div>'+head+'<div class="muted" style="font-size:12px">该平台本次未取到数据</div></div>';
    const rows = s.items.map(x=>{
      const rk = x.rank==null?'':'<span class="rk'+(x.rank<=3?' t3':'')+'">'+x.rank+'</span>';
      const name = x.name ? x.name : '<span class="muted">（名称未取到）</span>';
      const tag = x.theme? ('<span class="muted" style="font-size:11px">　'+x.theme+'·'+x.role+'</span>') : '';
      return '<tr><td style="width:34px">'+rk+'</td><td>'+name+tag+'</td></tr>';
    }).join('');
    return '<div>'+head+'<table><thead><tr><th>#</th><th>股票</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
  }).join('');
}
window.DASH.register('hotrank','24小时人气榜', renderHotRank);
renderHotRank();
renderCycle();

try{
/* ---------- 纪律清单 ---------- */
const RULE_KEY='wb_rules';
function loadRules(){try{return JSON.parse(localStorage.getItem(RULE_KEY))||defaultRules()}catch(e){return defaultRules()}}
function defaultRules(){return [
  {t:'竞价中位数 < 0 时，不接力高位板',done:false},
  {t:'标准差 > 5（严重分化）时，只做龙头不做跟风',done:false},
  {t:'单笔仓位不超过计划上限，不满仓梭哈',done:false},
  {t:'买入前先写清楚理由，错了认错离场',done:false}
]}
function saveRules(r){localStorage.setItem(RULE_KEY,JSON.stringify(r))}
let rules = loadRules();
function renderRules(){
  const auto = document.getElementById('autoRule');
  const box = document.getElementById('ruleList');
  if(!auto || !box) return;
  const recs=getRecs(), last = recs.length ? recs[recs.length-1] : null;
  if(last){
    let msg = [];
    if(last.median < 0) msg.push('⚠️ 今日竞价中位数 < 0（'+fmt(last.median)+'%），情绪退潮，谨慎接力。');
    else if(last.median >= 2) msg.push('🔥 竞价中位数 '+fmt(last.median)+'%，情绪高潮，注意兑现节奏。');
    if(last.std > 5) msg.push('⚠️ 标准差 '+fmt(last.std)+'%，分歧大，只做核心龙头。');
    auto.innerHTML = msg.length ? msg.join('<br>') : '✅ 情绪指标正常，按既定纪律执行。';
  } else auto.innerHTML = '暂无最新数据。';
  box.innerHTML = rules.map((r,i)=>`<label class="${r.done?'done':''}"><input type="checkbox" data-i="${i}" ${r.done?'checked':''}><span>${r.t}</span></label>`).join('');
  box.querySelectorAll('input').forEach(cb=>cb.onchange=e=>{rules[e.target.dataset.i].done=e.target.checked;saveRules(rules);renderRules();});
}
window.DASH.register('rules','纪律清单', renderRules);
renderRules();
const ruleForm=document.getElementById('ruleForm');
if(ruleForm) ruleForm.onsubmit=e=>{
  e.preventDefault();
  const v=document.getElementById('newRule').value.trim();
  if(v){rules.push({t:v,done:false});saveRules(rules);document.getElementById('newRule').value='';renderRules();}
};

/* ---------- 交易记录 ---------- */
const TRADE_KEY='wb_trades';
function loadTrades(){try{return JSON.parse(localStorage.getItem(TRADE_KEY))||[]}catch(e){return []}}
function saveTrades(t){localStorage.setItem(TRADE_KEY,JSON.stringify(t))}
let trades = loadTrades();
function pnl(t){return (t.sell!=null&&t.buy)?((t.sell-t.buy)/t.buy*100):null}
function renderTrades(){
  const tb=document.querySelector('#tradeTable tbody');
  if(!tb) return;
  tb.innerHTML = trades.length? trades.slice().reverse().map((t,ri)=>{
    const i=trades.length-1-ri, p=pnl(t);
    return `<tr><td>${t.date}</td><td>${t.name}</td><td class="num">${t.buy}</td><td class="num">${t.sell??'持仓'}</td><td class="num ${p>0?'pos':p<0?'neg':''}">${p==null?'--':fmt(p)}</td><td>${t.pos||''}</td><td><button class="mini" data-del="${i}">✕</button></td></tr>`;
  }).join('') : '<tr><td colspan="7" style="color:var(--sub)">还没有记录，先记一笔吧</td></tr>';
  tb.querySelectorAll('[data-del]').forEach(b=>b.onclick=e=>{trades.splice(+e.target.dataset.del,1);saveTrades(trades);renderTrades();});
  // 统计
  const closed=trades.filter(t=>pnl(t)!=null);
  const wins=closed.filter(t=>pnl(t)>0).length;
  const total=closed.reduce((a,t)=>a+pnl(t),0);
  const st=document.getElementById('tradeStats');
  if(st) st.innerHTML=`
    <span>总笔数 <b>${trades.length}</b></span>
    <span>已了结 <b>${closed.length}</b></span>
    <span>胜率 <b style="color:${closed.length&&wins/closed.length>=0.5?'var(--up)':'var(--down)'}">${closed.length?fmt(wins/closed.length*100)+'%':'--'}</b></span>
    <span>累计盈亏 <b style="color:${total>0?'var(--up)':total<0?'var(--down)':'inherit'}">${closed.length?fmt(total)+'%':'--'}</b></span>`;
  // 收益曲线
  const el=document.getElementById('tradeChart');
  if(!el) return;
  let chart=echarts.getInstanceByDom(el); if(chart) chart.dispose();
  el.removeAttribute('_echarts_instance_');
  chart=echarts.init(el);
  let cum=0; const curve=closed.map((t,i)=>{cum+=pnl(t);return [t.date,+fmt(cum)]});
  chart.setOption({
    tooltip:{trigger:'axis'},grid:{left:44,right:16,top:16,bottom:26},
    xAxis:{type:'category',data:curve.map(c=>c[0]),axisLabel:{fontSize:10}},
    yAxis:{type:'value',scale:true,splitLine:{lineStyle:{color:'#eef0f5'}}},
    series:[{type:'line',data:curve.map(c=>c[1]),smooth:true,step:false,
      lineStyle:{width:2.5,color:curve.length&&curve[curve.length-1][1]>=0?'#d93a3a':'#1a9c6b'},
      itemStyle:{color:'#2f6bff'},symbolSize:5}]
  });
  if(!el._wbResize){ el._wbResize=1; window.addEventListener('resize',()=>{ const c=echarts.getInstanceByDom(el); if(c) c.resize(); }); }
}
window.DASH.register('trades','交易记录', renderTrades);
renderTrades();
const tradeForm=document.getElementById('tradeForm');
if(tradeForm) tradeForm.onsubmit=e=>{
  e.preventDefault();
  const g=id=>document.getElementById(id).value;
  trades.push({date:g('tDate'),code:g('tCode'),name:g('tName'),buy:+g('tBuy'),sell:g('tSell')?+g('tSell'):null,pos:g('tPos'),why:g('tWhy')});
  saveTrades(trades); e.target.reset(); renderTrades();
};

/* ---------- 题材周期 ---------- */
const THEME_KEY='wb_themes';
const STAGES=['启动','发酵','高潮','退潮'];
function loadThemes(){try{return JSON.parse(localStorage.getItem(THEME_KEY))||[]}catch(e){return []}}
function saveThemes(t){localStorage.setItem(THEME_KEY,JSON.stringify(t))}
let themes=loadThemes();
function renderThemes(){
  const box=document.getElementById('stageBox');
  if(!box) return;
  box.innerHTML = STAGES.map((s,si)=>{
    const items=themes.filter(t=>t.stage===si);
    return `<div class="stage s${si}"><h3>${s}</h3><ul>${
      items.length? items.map(t=>{
        const gi=themes.indexOf(t);
        return `<li><span>${t.name}${t.leader?' · '+t.leader:''}</span><span><button class="mini" data-mv="${gi}">›</button> <button class="mini" data-dt="${gi}">✕</button></span></li>`;
      }).join('') : '<li>—</li>'
    }</ul></div>`;
  }).join('');
  box.querySelectorAll('[data-mv]').forEach(b=>b.onclick=e=>{const i=+e.target.dataset.mv;themes[i].stage=(themes[i].stage+1)%4;saveThemes(themes);renderThemes();});
  box.querySelectorAll('[data-dt]').forEach(b=>b.onclick=e=>{themes.splice(+e.target.dataset.dt,1);saveThemes(themes);renderThemes();});
}
window.DASH.register('themes','题材周期', renderThemes);
renderThemes();
const themeForm=document.getElementById('themeForm');
if(themeForm) themeForm.onsubmit=e=>{
  e.preventDefault();
  themes.push({name:document.getElementById('thName').value.trim(),leader:document.getElementById('thLeader').value.trim(),note:document.getElementById('thNote').value.trim(),stage:0});
  saveThemes(themes); e.target.reset(); renderThemes();
};
}catch(err){ /* 底部卡片已删除，忽略 */ }

/* ---------- 注册万年历（wuxing5.js 提供 render） + 绑定全部刷新按钮 ---------- */
if(window.M5 && window.M5.render){
  window.DASH.register('calendar','万年历', function(){ window.M5.render(true); window.M5Q && window.M5Q.render(); });
}

/* ---------- 游资悟道语录（quotes_data.js 提供 YZQ.list） ----------
   洗牌袋轮换：把全部序号洗乱存 localStorage，逐个弹出；弹完重洗。
   保证「每次刷新换一条」且连续刷新绝不重复、全部看完一轮才重新开始。
   file:// 等无 localStorage 环境自动退化为「随机且不与上一条重复」。 */
window.M5Q = (function(){
  var BAG_KEY='yzq_bag', LAST_KEY='yzq_last';
  function shuffle(a){ for(var i=a.length-1;i>0;i--){ var j=Math.floor(Math.random()*(i+1)); var t=a[i];a[i]=a[j];a[j]=t; } return a; }
  function pick(n){
    try{
      var bag=JSON.parse(localStorage.getItem(BAG_KEY)||'null');
      if(!Array.isArray(bag)||!bag.length||bag.length>n) bag=shuffle(Array.from({length:n},function(_,i){return i;}));
      var idx=bag.shift();
      localStorage.setItem(BAG_KEY, JSON.stringify(bag));
      localStorage.setItem(LAST_KEY, String(idx));
      return idx;
    }catch(e){
      var r=Math.floor(Math.random()*n);
      try{ if(String(r)===localStorage.getItem(LAST_KEY)) r=(r+1)%n; localStorage.setItem(LAST_KEY,String(r)); }catch(e2){}
      return r;
    }
  }
  function render(){
    var box=document.getElementById('m5Quote'); if(!box) return;
    var Q=window.YZQ&&window.YZQ.list; if(!Q||!Q.length){ box.style.display='none'; return; }
    var q=Q[pick(Q.length)];
    document.getElementById('m5QuoteText').textContent=''+q.t;
    document.getElementById('m5QuoteAuthor').textContent=q.a;
    box.classList.remove('swap'); void box.offsetWidth; box.classList.add('swap');
  }
  return { render: render };
})();
window.DASH.register('quote','游资悟道语录', window.M5Q.render);
window.M5Q.render();
window.DASH.bindButtons();
</script>

<!-- 自动运行引擎：必须在 DASH 注册并绑定按钮之后启动 -->
<script>
/* ===== 场景看板：万年历主界面 + 操作面板 / 复盘面板 / 快速直达 =====
   勾选模块 → 生成独立看板；选择持久化 localStorage(mod_scenes_v1) */
(function(){
  var MODULES={M02:'情绪概览 KPI',M04:'市场情绪双图',M05:'连板梯队',M06:'连板天梯',
    M07:'历史天量扫描',M08:'题材周期主图',M09:'复盘计划及交易预警',M10:'赚钱效应排行',
    M11:'30日涨幅榜·异动监控',M12:'24小时人气榜',M13:'中周期猜想',M14:'题材周期猜想'};
  var DEF={op:['M04','M05','M07','M09'],
    re:['M06','M08','M10','M11','M12'],
    quick:['M02','M04','M05','M06','M07','M08','M09','M10','M11','M12','M13','M14']};
  var NAME={op:'操作面板',re:'复盘面板',quick:'全景预览'};
  var LS='mod_scenes_v3', cur=null;
  function load(){try{var o=JSON.parse(localStorage.getItem(LS)||'null');if(o&&o.op)return o;}catch(e){}return JSON.parse(JSON.stringify(DEF));}
  var SCENES=load();
  function save(){try{localStorage.setItem(LS,JSON.stringify(SCENES));}catch(e){}}

  function updateCounts(){
    Array.prototype.forEach.call(document.querySelectorAll('.hb-n'),function(el){
      var k=el.getAttribute('data-n'); el.textContent=(SCENES[k]||[]).length+' 个模块';
    });
  }
  function resizeBoard(){
    setTimeout(function(){
      window.dispatchEvent(new Event('resize'));
      if(window.echarts){
        Array.prototype.forEach.call(document.querySelectorAll('#boardBody .mod-on [id$="Chart"],#boardBody .mod-on #cycMatrixCanvas'),function(dom){
          try{var ins=echarts.getInstanceByDom(dom);if(ins)ins.resize();}catch(e){}
        });
      }
    },80);
  }
  function showHome(){
    var hv=document.getElementById('homeView');hv.hidden=false;hv.style.display='';
    var bv=document.getElementById('boardView');bv.hidden=true;bv.style.display='none';
    window.scrollTo(0,0);
  }
  function openScene(key){
    if(!SCENES[key])return;cur=key;
    var hv=document.getElementById('homeView');hv.hidden=true;hv.style.display='none';
    var bv=document.getElementById('boardView');bv.hidden=false;bv.style.display='';
    document.getElementById('boardTitle').textContent=NAME[key]+' 看板';
    var sel=SCENES[key];
    Array.prototype.forEach.call(document.querySelectorAll('#boardBody>[data-mod]'),function(el){
      el.classList.toggle('mod-on',sel.indexOf(el.getAttribute('data-mod'))>=0);
    });
    window.scrollTo(0,0);resizeBoard();
  }
  function openPicker(key){
    var pk=document.getElementById('modPicker'),grid=document.getElementById('pkGrid');
    document.getElementById('pkTitle').textContent='选择模块 → '+(NAME[key]||'');
    grid.innerHTML='';
    var seq=0;
    Object.keys(MODULES).forEach(function(m){
      seq++;
      var on=(SCENES[key]||[]).indexOf(m)>=0;
      var it=document.createElement('label');
      it.className='pk-item'+(on?' on':'');
      it.setAttribute('data-mod',m);
      it.innerHTML='<input type="checkbox"'+(on?' checked':'')+'><span><b>'+seq+'</b>　'+MODULES[m]+'</span>';
      it.querySelector('input').addEventListener('change',function(){it.classList.toggle('on',this.checked);});
      grid.appendChild(it);
    });
    document.getElementById('pkSave').onclick=function(){
      var chosen=[];
      Array.prototype.forEach.call(grid.querySelectorAll('.pk-item'),function(it){
        if(it.querySelector('input').checked)chosen.push(it.getAttribute('data-mod'));
      });
      SCENES[key]=chosen;save();updateCounts();pk.hidden=true;openScene(key);
    };
    document.getElementById('pkAll').onclick=function(){
      Array.prototype.forEach.call(grid.querySelectorAll('.pk-item'),function(it){it.querySelector('input').checked=true;it.classList.add('on');});
    };
    document.getElementById('pkNone').onclick=function(){
      Array.prototype.forEach.call(grid.querySelectorAll('.pk-item'),function(it){it.querySelector('input').checked=false;it.classList.remove('on');});
    };
    pk.hidden=false;
  }
  function init(){
    var d=new Date(),h=d.getHours();
    var greet=h<6?'凌晨好':h<9?'早上好':h<12?'上午好':h<14?'中午好':h<18?'下午好':'晚上好';
    var ht=document.getElementById('hhTitle');if(ht)ht.textContent=greet+'，'+(d.getMonth()+1)+'月'+d.getDate()+'日';
    var hd=document.getElementById('hhDate');
    if(hd){var S=window.SENTIMENT;var t=(S&&S.limitUp&&S.limitUp.date)?S.limitUp.date:'—';hd.textContent='数据日期：'+t+' · '+d.toLocaleString('zh-CN');}
    var m01=document.querySelector('[data-mod="M01"]'),homeM01=document.getElementById('homeM01');
    if(m01&&homeM01)homeM01.appendChild(m01);
    var body=document.getElementById('boardBody');
    Array.prototype.forEach.call(document.querySelectorAll('[data-mod]'),function(el){
      if(el.getAttribute('data-mod')==='M01')return;
      if(body)body.appendChild(el);
    });
    Array.prototype.forEach.call(document.querySelectorAll('.home-btn'),function(b){
      b.onclick=function(){openScene(b.getAttribute('data-scene'));};
    });
    var back=document.getElementById('boardBack');if(back)back.onclick=showHome;
    var edit=document.getElementById('boardEdit');if(edit)edit.onclick=function(){if(cur)openPicker(cur);};
    updateCounts();showHome();
  }
  if(document.readyState==='loading')document.addEventListener('DOMContentLoaded',init);else init();
})();
</script>
<script>
/* 统一 ECharts(canvas) 字体：让所有图表内文字（轴标签/图例/tooltip/数据标签）沿用页面字体栈，
   避免回退到默认 sans-serif 造成字形发虚、中西混排粗细不一致。实例 option 仍可单独覆盖。 */
(function(){
  var CHART_FONT='"Microsoft YaHei UI","Microsoft YaHei",-apple-system,"PingFang SC","Segoe UI",system-ui,sans-serif';
  var _init=echarts.init;
  echarts.init=function(dom,theme,opt){
    opt=opt||{};
    opt.textStyle=opt.textStyle||{};
    if(!opt.textStyle.fontFamily)opt.textStyle.fontFamily=CHART_FONT;
    /* 图表清晰度：至少 2 倍设备像素渲染（普通屏超采样，高清屏取原生 dpr），保证轴标签/图例锐利 */
    opt.devicePixelRatio=Math.max(2, window.devicePixelRatio||1);
    return _init.call(echarts,dom,theme,opt);
  };
})();
</script>
<script>
/* ===== 守护状态条（主界面底层逻辑）：本地部署时可一键拉起 dashboardd + watchdog =====
   浏览器自身无法起进程，但本地服务 _serve.js 是 node 进程有权限，故走它的 /__daemon/start 端点。 */
(function(){
  var bar=document.getElementById('daemonBar');
  if(!bar) return;
  var tx=bar.querySelector('.db-tx');
  var btn=document.getElementById('dbStart');
  var isLocal=/^(127\.0\.0\.1|localhost)$/.test(location.hostname);
  function nextTxt(){
    return fetch('./autopilot_status.json?'+Date.now(),{cache:'no-store'})
      .then(function(r){return r.ok?r.json():null;})
      .then(function(j){return (j&&j.next)?(' · 下一任务 '+j.next.name+' '+j.next.at):'';})
      .catch(function(){return '';});
  }
  function tick(){
    if(!isLocal){
      bar.classList.remove('on','off');
      tx.textContent='当前非本地部署地址；用 http://127.0.0.1:8899 打开可一键启动本地守护';
      btn.hidden=true; return;
    }
    fetch('/__daemon/status',{cache:'no-store'}).then(function(r){return r.json();}).then(function(j){
      var on=!!j.alive;
      bar.classList.toggle('on',on); bar.classList.toggle('off',!on);
      if(on){
        tx.textContent='本地守护在线 · 自动运行：0点日切 / 9:25竞价 / 15:01天梯 / 15:20备料 / 15:35涨跌家数';
        btn.hidden=true; return '';
      }
      tx.textContent='本地守护未运行 · 竞价/复盘/涨跌家数等自动更新当前不会执行';
      btn.hidden=false; return '';
    }).then(function(extra){ if(extra) tx.textContent+=extra; })
      .catch(function(){
        bar.classList.add('off'); bar.classList.remove('on');
        tx.textContent='本地服务未连接（请用 http://127.0.0.1:8899 打开看板）';
        btn.hidden=true;
      });
  }
  btn.onclick=function(){
    btn.disabled=true; btn.textContent='启动中…';
    fetch('/__daemon/start',{cache:'no-store'}).then(function(r){return r.json();}).then(function(){
      btn.textContent='已启动';
      setTimeout(function(){btn.disabled=false;btn.textContent='启动守护';tick();},1800);
    }).catch(function(){btn.disabled=false;btn.textContent='启动失败';});
  };
  tick(); setInterval(tick,20000);
})();
</script>
<script>
/* ===== 版本号单一数据源 =====
   manifest.json 里的 name / version 是唯一出处：升级只改那一个文件，
   页面标题与标题旁的版本徽标会自动跟着变，不必在多处同步。 */
fetch('./manifest.json', { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (m) {
  if (!m) return;
  if (m.name) document.title = m.name;
  var b = document.getElementById('verBadge');
  if (b && m.version) b.textContent = m.version;
}).catch(function () { /* file:// 打开时取不到，保留静态兜底 */ });
</script>
<script src="./autopilot.js?v=20261015"></script>
<script>
(function(){
  try{
    if(!window.AUTOPILOT){ console.warn('[看板] autopilot.js 未加载，自动运行不可用'); return; }
    var pill=document.getElementById('apPill'); if(pill) pill.hidden=false;
    window.AUTOPILOT.start();
  }catch(e){ console.error('[看板] 自动运行启动失败', e); }
})();
</script>

<!-- ===== PWA 缓存层：注册 Service Worker，让第二次打开秒开 =====
     首屏原本约 1.7MB，每次都要重下；开启后静态壳走 cache-first、
     数据文件走 stale-while-revalidate（先秒开、后台更新）。
     仅 https 或 localhost 生效（file:// 下自动跳过，不影响本地使用）。 -->
<script>
(function(){
  if(location.protocol==='file:') return;
  if(!('serviceWorker' in navigator)) return;
  window.addEventListener('load', function(){
    navigator.serviceWorker.register('./sw.js').then(function(reg){
      reg.addEventListener('updatefound', function(){
        var nw = reg.installing;
        if(!nw) return;
        nw.addEventListener('statechange', function(){
          if(nw.state==='installed' && navigator.serviceWorker.controller){
            try{ nw.postMessage({type:'SKIP_WAITING'}); }catch(e){}
          }
        });
      });
    }).catch(function(){ /* 注册失败不影响功能，只是没有离线加速 */ });

    var pre = { ladderhist: ['./ladderhist.js'], lunar: ['./lunar.js'], calendar: ['./lunar.js'] };
    window.prefetchChunk = function(name){
      var urls = pre[name]; if(!urls) return;
      var sw = navigator.serviceWorker.controller;
      if(sw){ try{ sw.postMessage({type:'prefetch', urls:urls}); }catch(e){} }
    };
  });
})();
</script>


</body></html>