const { Button, Card, Badge, Stat, PrismRule, Tag } = window.PrismDS;


const TICKERS = [
  ['招商银行', '38.60'], ['兴业银行', '18.40'], ['浦发银行', '8.20'], ['民生银行', '4.15'], ['平安银行', '11.20'],
  ['工商银行', '6.18'], ['建设银行', '8.45'], ['农业银行', '4.86'], ['交通银行', '7.32'], ['中国银行', '4.62'],
  ['光大银行', '3.45'], ['华夏银行', '7.10'], ['北京银行', '5.30'], ['上海银行', '8.90'], ['江苏银行', '8.15'],
  ['南京银行', '10.40'], ['宁波银行', '24.60'], ['杭州银行', '13.80'], ['成都银行', '15.20'], ['长沙银行', '8.40'],
  ['贵阳银行', '5.60'], ['青岛银行', '3.90'], ['苏州银行', '7.20'], ['中信银行', '6.80'], ['邮储银行', '5.10'],
  ['常熟银行', '7.60'], ['齐鲁银行', '5.40'], ['重庆银行', '8.30'], ['郑州银行', '2.30'], ['厦门银行', '5.20'],
  ['中信证券', '26.40'], ['中国平安', '52.30'], ['中国人寿', '34.20'], ['中国太保', '32.80'], ['新华保险', '46.50'],
  ['中国银河', '14.60'], ['招商证券', '18.90'], ['国信证券', '11.40'], ['华泰证券', '16.80'], ['广发证券', '16.20'],
  ['东方证券', '10.60'], ['兴业证券', '6.80'], ['光大证券', '15.40'], ['长江证券', '6.50'], ['国金证券', '9.80'],
  ['财通证券', '8.60'], ['东方财富', '14.60'], ['中金公司', '38.40'], ['申万宏源', '4.90'], ['国泰君安', '16.40'],
  ['海通证券', '9.20'], ['方正证券', '7.80'], ['浙商证券', '11.60'], ['国联证券', '11.20'], ['中银证券', '12.40'],
  ['财达证券', '8.90'], ['华创云信', '15.80'], ['国投资本', '8.40'], ['东吴证券', '7.60'], ['华西证券', '7.20'],
  ['贵州茅台', '1685.00'], ['山西汾酒', '198.50'], ['泸州老窖', '168.20'], ['洋河股份', '88.40'], ['古井贡酒', '226.00'],
  ['迎驾贡酒', '58.40'], ['舍得酒业', '82.40'], ['伊利股份', '28.60'], ['海天味业', '38.60'], ['双汇发展', '25.40'],
  ['光明乳业', '9.80'], ['涪陵榨菜', '14.20'], ['桃李面包', '6.80'], ['安琪酵母', '34.60'], ['绝味食品', '18.40'],
  ['千禾味业', '12.60'], ['中炬高新', '22.80'], ['恒顺醋业', '8.40'], ['妙可蓝多', '15.60'], ['承德露露', '8.90'],
  ['三全食品', '12.40'], ['安井食品', '84.60'], ['洽洽食品', '28.40'], ['汤臣倍健', '16.80'], ['恒瑞医药', '46.20'],
  ['云南白药', '54.30'], ['复星医药', '28.40'], ['智飞生物', '32.60'], ['长春高新', '108.40'], ['华东医药', '36.80'],
  ['步长制药', '18.90'], ['济川药业', '68.40'], ['丽珠集团', '38.60'], ['华润三九', '42.80'], ['以岭药业', '26.40'],
  ['人福医药', '22.60'], ['康缘药业', '24.80'], ['通策医疗', '68.40'], ['爱尔眼科', '14.60'], ['迈瑞医疗', '268.00'],
  ['药明康德', '62.40'], ['泰格医药', '58.60'], ['我武生物', '42.60'], ['康泰生物', '28.40'], ['沃森生物', '18.60'],
  ['宁德时代', '182.50'], ['北方华创', '385.00'], ['韦尔股份', '96.40'], ['立讯精密', '32.60'], ['海康威视', '32.40'],
  ['大华股份', '16.80'], ['兆易创新', '92.40'], ['圣邦股份', '88.40'], ['三安光电', '14.20'], ['闻泰科技', '38.40'],
  ['歌尔股份', '22.60'], ['生益科技', '18.40'], ['沪电股份', '32.80'], ['深南电路', '88.60'], ['紫光国微', '68.40'],
  ['澜起科技', '58.60'], ['中微公司', '168.40'], ['拓荆科技', '168.00'], ['长电科技', '34.60'], ['通富微电', '22.40'],
  ['华峰测控', '128.40'], ['精测电子', '68.60'], ['中际旭创', '128.60'], ['天孚通信', '88.60'], ['光迅科技', '34.20'],
  ['汇顶科技', '68.40'], ['传音控股', '88.40'], ['视源股份', '68.40'], ['用友网络', '12.80'], ['金山办公', '268.00'],
  ['恒生电子', '24.60'], ['科大讯飞', '48.60'], ['大族激光', '22.40'], ['紫光股份', '24.80'], ['浪潮信息', '42.60'],
  ['长城汽车', '24.60'], ['广汽集团', '8.80'], ['上汽集团', '14.20'], ['长安汽车', '12.80'], ['江淮汽车', '22.40'],
  ['潍柴动力', '14.80'], ['三一重工', '18.20'], ['中联重科', '7.40'], ['徐工机械', '6.80'], ['恒立液压', '58.40'],
  ['汇川技术', '62.40'], ['三花智控', '24.60'], ['双环传动', '26.80'], ['拓普集团', '48.60'], ['福耀玻璃', '46.80'],
  ['华域汽车', '18.40'], ['均胜电子', '16.80'], ['宁波华翔', '22.60'], ['银轮股份', '18.40'], ['星宇股份', '108.40'],
  ['紫金矿业', '16.80'], ['中国神华', '38.90'], ['中国石油', '8.62'], ['中国石化', '6.45'], ['中国建筑', '5.72'],
  ['中国中铁', '6.40'], ['中国铁建', '8.80'], ['中国交建', '9.60'], ['中国电建', '5.80'], ['中国能建', '2.40'],
  ['中国核电', '9.80'], ['中国广核', '4.60'], ['长江电力', '27.80'], ['华能水电', '9.40'], ['国电电力', '5.20'],
  ['华电国际', '6.80'], ['川投能源', '16.40'], ['陕西煤业', '24.60'], ['兖矿能源', '16.80'], ['中煤能源', '11.40'],
  ['山东黄金', '26.80'], ['中金黄金', '14.60'], ['赤峰黄金', '18.40'], ['洛阳钼业', '8.60'], ['华友钴业', '34.60'],
  ['赣锋锂业', '38.40'], ['天齐锂业', '42.60'], ['北方稀土', '22.40'], ['盐湖股份', '16.80'], ['万华化学', '78.40'],
  ['荣盛石化', '12.40'], ['恒力石化', '15.60'], ['东方盛虹', '8.90'], ['桐昆股份', '12.80'], ['宝丰能源', '16.40'],
  ['卫星化学', '18.60'], ['龙佰集团', '18.40'], ['扬农化工', '58.60'], ['鲁西化工', '14.80'], ['湖北宜化', '16.20'],
  ['美的集团', '68.50'], ['格力电器', '42.80'], ['海尔智家', '26.40'], ['老板电器', '22.60'], ['九阳股份', '14.60'],
  ['石头科技', '268.00'], ['欧派家居', '62.40'], ['顾家家居', '28.60'], ['志邦家居', '18.40'], ['公牛集团', '82.40'],
  ['晨光股份', '28.60'], ['上海家化', '22.60'], ['华熙生物', '58.60'], ['森马服饰', '6.80'], ['海澜之家', '7.40'],
  ['比音勒芬', '22.60'], ['水羊股份', '14.60'], ['保利发展', '8.90'], ['招商蛇口', '9.60'], ['金地集团', '4.80'],
  ['新城控股', '12.40'], ['华发股份', '6.80'], ['滨江集团', '8.60'], ['中国中免', '68.40'], ['中国联通', '5.28'],
  ['中国电信', '6.80'], ['分众传媒', '6.40'], ['华测检测', '14.20']
];

function quote(base, seed, tick) {
  const n = parseFloat(base.replace(/,/g, ''));
  if (isNaN(n)) return { price: base, pct: '+0.00%' };
  const wave = Math.sin(seed * 0.9 + tick * 0.6) * 0.6 + Math.sin(seed * 1.7 + tick * 0.31) * 0.4;
  const pct = wave * 2.4;
  const v = n * (1 + pct / 100);
  const price = v >= 1000 ? v.toLocaleString('en-US', { maximumFractionDigits: 1 }) : v.toFixed(v < 10 ? 3 : 2);
  return { price, pct: (pct >= 0 ? '+' : '') + pct.toFixed(2) + '%' };
}

// 数据墙的行列几何。行高/间距/内边距沿用原值，只是从写死改成按容器实测推算。
const TF = {
  PAD_TOP: 92,     // 顶部让位给页头 + 呼吸
  PAD_X: 28,
  PAD_BOTTOM: 24,  // 底部安全边距：保证最后一行是完整行，不会被容器边缘拦腰切断
  ROW_H: 44,
  COL_GAP: 22,
  MIN_COL: 150,    // 一条行情「名称 + 价格 + 涨跌幅」实测最宽约 137px，留余量取 150
  MAX_COLS: 8      // 上限 8 列，维持宽屏上原有的疏密观感；窄屏只减列、不压列宽
};

function TickerField({ bright, tick = 0, slots }) {
  // 【2026-08-14】原来是写死的 19 行 × 8 列、容器高写死 92 + 16*44 = 796px。
  // 两个后果都会让行情被截断：
  //  ① 竖向：19 行需要 92 + 19*44 = 928px，容器只有 796px，overflow:hidden 永远把第 17 行
  //     拦腰切断——实测 1440×900 / 1440×700 / 1280×800 均出现 1 条「半截行」。
  //     且 Hero 改成 100dvh 后高度随视口变化，796px 这个定值在高屏还会在底部留出
  //     104–284px 完全没有行情的空带（左下角「照不到」的真实来源）。
  //  ② 横向：8 列在窄屏下列宽被压到 133px，而一条行情需要 135–137px，单元格
  //     overflow:hidden 就从字符中间切开——实测 1280×800 有 35 个单元格缺字。
  // 现在行数按容器实高推算、列数按可用宽推算，保证「最后一行是完整行」「每列装得下整条行情」。
  // 光斑/遮罩/涨红跌绿/换股逻辑完全未动。
  const box = React.useRef(null);
  const [dim, setDim] = React.useState({ rows: 16, cols: 8, shift: 0 });
  React.useLayoutEffect(() => {
    const el = box.current;
    if (!el) return;
    const measure = () => {
      const w = el.clientWidth, h = el.clientHeight;
      const cols = Math.max(1, Math.min(TF.MAX_COLS,
        Math.floor((w - 2 * TF.PAD_X + TF.COL_GAP) / (TF.MIN_COL + TF.COL_GAP))));
      const rows = Math.max(1, Math.floor((h - TF.PAD_TOP - TF.PAD_BOTTOM) / TF.ROW_H));
      // 【左右边距对称化】列是 repeat(cols, 1fr) 等宽、内容在列内左对齐，于是每列右侧都有
      // 一段空白，而**最后一列那段空白直接变成了视口右边距**：左边距恒为 PAD_X(28)，
      // 右边距却是 PAD_X + (列宽 − 内容宽)。实测 1440 差 18.7px、1920 差 71.2px、
      // 2560 差 152.8px——屏幕越宽越偏，最左列因此紧贴边缘落进左下死区。
      // 推导：右边距 − 左边距 = 列宽 − 内容宽（与列数、间距无关），故整体右移一半即左右相等。
      // 用 transform 平移而不是改 padding：padding 会改变可用宽 → 改变列宽 → 又改变差值，
      // 自我耦合；transform 发生在布局之后，纯位移，不影响列宽、行数与任何动画。
      const colW = (w - 2 * TF.PAD_X - (cols - 1) * TF.COL_GAP) / cols;
      // 取全部单元格墨迹宽度的**均值**作为「一条行情的代表宽度」。
      // 不用最大值：各条行情宽度实测在 123–142px 之间浮动（名称字数、价格位数不同），
      // 右缘本就是锯齿状的，按最宽的那条对齐会让绝大多数行的右边距系统性偏大约 10px。
      // 按均值对齐则让中位行左右完全相等，其余行在 ±10px 内对称分布——这是纯位移能做到的最好结果。
      // 均值取自 100+ 个单元格，换股对它的影响在 0.5px 内，因此不会抖动；
      // 且 measure 只在 resize 时跑，两次之间数值是冻结的。
      //
      // 逐行→逐格两级下钻。不能用 querySelectorAll('div > div')：行本身也是容器的子 div，
      // 会一并命中，于是墨迹宽被量成整行宽度（实测 1377px），shift 被 clamp 成 0。
      let sum = 0, n = 0;
      for (const row of el.children) {
        for (const cell of row.children) {
          const last = cell.lastElementChild;
          if (!last) continue;
          sum += last.getBoundingClientRect().right - cell.getBoundingClientRect().left;
          n++;
        }
      }
      const shift = n ? Math.max(0, (colW - sum / n) / 2) : 0;
      setDim(d => (d.rows === rows && d.cols === cols && Math.abs(d.shift - shift) < 0.5)
        ? d : { rows, cols, shift });
    };
    measure();
    if (!window.ResizeObserver) { window.addEventListener('resize', measure); return () => window.removeEventListener('resize', measure); }
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const rows = [];
  for (let r = 0; r < dim.rows; r++) {
    rows.push(
      <div key={r} style={{ display: 'grid', gridTemplateColumns: 'repeat(' + dim.cols + ', 1fr)', gap: '0 22px', height: 44, alignItems: 'center' }}>
        {Array.from({ length: dim.cols }).map((_, c) => {
          const idx = r * dim.cols + c;
          const t = TICKERS[(slots ? slots[idx % slots.length] : idx) % TICKERS.length];
          const q = quote(t[1], idx + t[0].charCodeAt(0), tick);
          const up = q.pct.startsWith('+');
          return (
            <div key={c + '-' + t[0]} style={{ display: 'flex', alignItems: 'baseline', gap: 4, whiteSpace: 'nowrap', overflow: 'hidden' }}>
              <span style={{ fontFamily: 'var(--font-mono), "PingFang SC", "Microsoft YaHei", "Heiti SC", sans-serif', fontSize: 12, letterSpacing: '.06em', color: bright ? 'rgba(255,255,255,.9)' : '#131C26' }}>{t[0]}</span>
              <span style={{ fontFamily: 'var(--font-display-en)', fontSize: 13, color: bright ? 'rgba(255,255,255,.75)' : '#141E29' }}>{q.price}</span>
              <span style={{ fontFamily: 'var(--font-display-en)', fontSize: 12, marginLeft: 8, textAlign: 'left', color: bright ? (up ? '#FF6B6B' : '#5FD3AE') : (up ? '#1E1618' : '#101F1B') }}>{q.pct}</span>
            </div>
          );
        })}
      </div>
    );
  }
  // 容器铺满 Hero（height:100%），行数由上面按实高算出，所以 overflow 只是保险，
  // 正常情况下不会真的裁到任何一行。alignContent:'start' 保证行从顶部排，
  // 底部余量（不足一行的部分）留白而不是把行拉伸变形。
  return <div ref={box} style={{ display: 'grid', alignContent: 'start', padding: '92px 28px 0', height: '100%', flexShrink: 0, boxSizing: 'border-box', overflow: 'hidden', transform: dim.shift ? 'translateX(' + dim.shift.toFixed(2) + 'px)' : undefined }}>{rows}</div>;
}


const dynImport = new Function('m', 'return import(m)');

function PrismCanvas({ containerRef }) {
  return <div ref={containerRef} style={{ width: '100%', aspectRatio: '1 / 1' }} />;
}

const HERO_KEYFRAMES = '@keyframes heroNudge{0%,100%{transform:translateX(-50%) translateY(0);opacity:.55}50%{transform:translateX(-50%) translateY(7px);opacity:1}}';

function ScrollCue({ targetRef }) {
  const [hidden, setHidden] = React.useState(false);
  React.useEffect(() => {
    const sc = document.getElementById('scroller');
    const node = sc || window;
    const read = () => setHidden((sc ? sc.scrollTop : window.scrollY) > 70);
    read();
    node.addEventListener('scroll', read, { passive: true });
    return () => node.removeEventListener('scroll', read);
  }, []);
  return (
    <div onClick={() => { const sc = document.getElementById('scroller'); const el = targetRef.current; if (sc && el) sc.scrollTo({ top: el.offsetTop + el.offsetHeight, behavior: 'smooth' }); }}
      style={{ position: 'absolute', left: 0, right: 0, bottom: '9%', zIndex: 5, cursor: 'pointer', display: 'flex', justifyContent: 'center', opacity: hidden ? 0 : 1, pointerEvents: hidden ? 'none' : 'auto', transition: 'opacity .6s var(--ease-out)' }}>
      <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 10, animation: 'heroCue 2.8s var(--ease-out) infinite' }}>
        <svg width="30" height="18" viewBox="0 0 30 18" fill="none" stroke="rgba(255,255,255,.72)" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"><path d="M2 3 L15 15 L28 3" /></svg>
        <span style={{ font: 'var(--type-caption)', fontSize: 12, letterSpacing: 0, color: 'rgba(255,255,255,.6)' }}>{T(PrismContent.hero.scrollCue)}</span>
      </div>
      <style>{'@keyframes heroCue{0%,100%{transform:translateY(0);opacity:.62}50%{transform:translateY(7px);opacity:1}}'}</style>
    </div>
  );
}

function Hero({ go }) {
  const wrap = React.useRef(null);
  const canvasWrap = React.useRef(null);
  const sceneApi = React.useRef(null);
  const maskRef = React.useRef(null);
  const glowRef = React.useRef(null);
  const beamRef = React.useRef(null);
  const inRef = React.useRef(null);
  const projRef = React.useRef(null);
  const pos = React.useRef({ x: 0, y: 0 });
  const [drag, setDrag] = React.useState(false);
  const [tick, setTick] = React.useState(0);
  const [slots, setSlots] = React.useState(() => {
    // 洗牌整个股票池，取前 208 个作为初始填充——保证整面数据墙名称互不重复。
    const idx = Array.from({ length: TICKERS.length }, (_, i) => i);
    for (let i = idx.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1));[idx[i], idx[j]] = [idx[j], idx[i]]; }
    return idx.slice(0, 208);
  });
  const dragStartX = React.useRef(0);

  React.useEffect(() => {
    let disposed = false;
    const ready = window.THREE ? Promise.resolve() : new Promise(r => window.addEventListener('three-ready', r, { once: true }));
    ready.then(() => dynImport(window.RES('prism-scene') === 'prism-scene' ? './PrismScene.js' : window.RES('prism-scene'))).then(m => {
      if (disposed || !canvasWrap.current) return;
      sceneApi.current = m.createPrismScene(canvasWrap.current);
    });
    return () => { disposed = true; sceneApi.current && sceneApi.current.dispose(); };
  }, []);

  React.useEffect(() => {
    const id = setInterval(() => setTick(t => t + 1), 900);
    return () => clearInterval(id);
  }, []);

  React.useEffect(() => {
    // 换股只从“当前未显示”的候选（free）中取，换入后仍无重复；池容量 > 展示数，留有刷新余地。
    const id = setInterval(() => setSlots(prev => {
      const next = prev.slice();
      const used = new Set(prev);
      const free = [];
      for (let i = 0; i < TICKERS.length; i++) if (!used.has(i)) free.push(i);
      for (let i = free.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1));[free[i], free[j]] = [free[j], free[i]]; }
      const swaps = Math.min(12, free.length);
      for (let k = 0; k < swaps; k++) {
        const at = Math.floor(Math.random() * next.length);
        const cand = free[k];
        used.delete(next[at]); used.add(cand);
        next[at] = cand;
      }
      return next;
    }), 1800);
    return () => clearInterval(id);
  }, []);

  // Hero 离屏时暂停：Three 场景 + 下面那条光效 rAF 一起停，滚到下方模块时不再空转。
  // 单一 IntersectionObserver 驱动两者，避免各自为政。
  const heroVisible = React.useRef(true);
  React.useEffect(() => {
    const el = wrap.current;
    if (!el || !window.IntersectionObserver) return;
    const io = new IntersectionObserver(es => {
      const vis = es.some(e => e.isIntersecting);
      heroVisible.current = vis;
      const api = sceneApi.current;
      api && api.setPaused && api.setPaused(!vis);
    }, { threshold: 0 });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  React.useEffect(() => {
    let raf = 0;
    const loop = () => {
      raf = requestAnimationFrame(loop);
      const el = wrap.current; if (!el) return;
      if (!heroVisible.current) return;   // 离屏：保留 rAF 心跳但不做任何读写
      const w = el.clientWidth, hgt = el.clientHeight;
      // 稳定基准：取容器较小边。四面体、入射光、出射光斑全部相对它按比例定位，
      // 与视口宽高比无关——16:9 / 21:9 / 4:3 / 竖屏下几何关系一致，宽屏也不会把光斑甩出画面。
      const scale = Math.max(1, Math.min(w, hgt));
      // 四面体在容器内的真实几何中心（CSS 已 left/top:50% 居中；实测更稳）。所有光效以它为锚点。
      const p = projRef.current;
      let cx = w * 0.5, cy = hgt * 0.5;
      if (p) { const pr = p.getBoundingClientRect(), er = el.getBoundingClientRect(); cx = pr.left - er.left + pr.width / 2; cy = pr.top - er.top + pr.height / 2; }
      const api = sceneApi.current;
      const d = api && api.getBeamDir ? api.getBeamDir() : { x: 0, y: 0, z: 1 };
      const reach = Math.max(w, hgt) * 0.6;
      // 出射光斑目标点 = 四面体中心 + 出射方向 × reach（相对锚点，随四面体自适应）。
      const to = { x: cx + d.x * reach, y: cy - d.y * reach };
      if (!pos.current.x) pos.current = { x: to.x, y: to.y };
      const k = api && api.isDragging && api.isDragging() ? 0.18 : 0.06;
      pos.current.x += (to.x - pos.current.x) * k;
      pos.current.y += (to.y - pos.current.y) * k;
      const R = Math.max(96, scale * 0.155);
      // 光斑透光孔半径：取较小边的 ~22%——一小片聚焦光斑，圈外为黑幕。
      // 覆盖四角靠 reach 放大（上方）让小光斑能移到/探出屏外，而非放大半径。
      const Rspot = Math.max(120, scale * 0.22);
      const x = pos.current.x, y = pos.current.y;
      if (maskRef.current) {
        // 默认全黑遮盖（#000 不透明），仅光斑圆形区内透出数据；中心最亮、向边缘柔和渐黑。
        const g = 'radial-gradient(circle ' + Rspot + 'px at ' + x + 'px ' + y + 'px, transparent 0 58%, rgba(0,0,0,.55) 82%, #000 100%)';
        maskRef.current.style.background = g;
      }
      if (glowRef.current) {
        const s = glowRef.current.style;
        s.left = (x - R) + 'px'; s.top = (y - R) + 'px';
        s.width = s.height = (R * 2) + 'px';
      }
      if (p && beamRef.current) {
        // 出射光束从四面体中心射向光斑；起点即锚点 cx/cy。
        const px = cx, py = cy;
        const dx = x - px, dy = y - py;
        const len = Math.hypot(dx, dy);
        const bs = beamRef.current.style;
        bs.left = px + 'px'; bs.top = py + 'px';
        bs.width = len + 'px'; bs.height = (R * 1.5) + 'px';
        const ang = Math.atan2(dy, dx) * 180 / Math.PI;
        bs.transform = 'translateY(-50%) rotate(' + ang + 'deg)';
        if (inRef.current) {
          const is = inRef.current.style;
          is.left = px + 'px'; is.top = py + 'px';
          is.width = scale * 0.62 + 'px';
          // Incident ray is held at a fixed 110° to the exit beam (inside the 100–120° target),
          // so the refraction bend stays legible at every point of the tetrahedron's rotation
          // instead of collapsing into one straight line through the solid.
          is.transform = 'translateY(-50%) rotate(' + (ang - 110) + 'deg)';
        }
      }
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, []);

  return (
    // 高度改由 .prism-hero-full 用 dvh/svh/vh 三段回退给出（见 index.html）。这里不能写内联
    // style：React 的 style 对象是个 map，同名属性只能存一个值，做不了 vh→svh→dvh 的层叠回退。
    <div ref={wrap} className="prism-hero-full" onMouseUp={() => setDrag(false)} onMouseLeave={() => setDrag(false)}
      style={{ position: 'relative', background: '#000', overflow: 'hidden', marginTop: 'calc(-1 * var(--header-outer-h))', userSelect: 'none' }}>
      <style>{HERO_KEYFRAMES}</style>

      <div style={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', justifyContent: 'flex-start', pointerEvents: 'none' }}>
        <TickerField bright={true} tick={tick} slots={slots} />
      </div>

      <div ref={maskRef} style={{ position: 'absolute', inset: 0, pointerEvents: 'none', background: '#000' }} />

      <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none', mixBlendMode: 'screen', overflow: 'hidden' }}>
        {/* 三个光效元素每帧都在改 transform/尺寸：提升为独立合成层，避免每帧重绘牵连整层。 */}
        <div ref={beamRef} style={{
          position: 'absolute', transformOrigin: '0 50%', opacity: .5, willChange: 'transform, width',
          background: 'linear-gradient(90deg, rgba(198,230,242,.30) 0%, rgba(198,230,242,.10) 45%, rgba(198,230,242,0) 100%)',
          clipPath: 'polygon(0 44%, 100% 0, 100% 100%, 0 56%)', filter: 'blur(10px)'
        }} />
        <div ref={inRef} style={{
          position: 'absolute', transformOrigin: '0 50%', height: 3, opacity: .62, willChange: 'transform, width',
          background: 'linear-gradient(90deg, rgba(228,243,250,.68) 0%, rgba(198,230,242,.34) 45%, rgba(198,230,242,.08) 80%, rgba(198,230,242,0) 100%)',
          boxShadow: '0 0 9px rgba(198,230,242,.3)', filter: 'blur(1.3px)'
        }} />
        <div ref={glowRef} style={{
          position: 'absolute', borderRadius: '50%', willChange: 'transform, left, top',
          background: 'radial-gradient(circle at 50% 50%, rgba(214,238,248,.16) 0%, rgba(198,230,242,.07) 55%, rgba(198,230,242,.10) 82%, rgba(198,230,242,0) 100%)'
        }} />
      </div>

      <ScrollCue targetRef={wrap} />

      <div style={{ position: 'absolute', left: 40, bottom: 22, zIndex: 5, pointerEvents: 'none', font: 'var(--type-caption)', fontSize: 11, letterSpacing: '.04em', color: 'rgba(255,255,255,.34)' }}>示意数据，非实时行情</div>

      <div ref={projRef} style={{ position: 'absolute', left: '50%', top: '50%', transform: 'translate(-50%,-50%)', width: 'clamp(230px, 40vmin, 380px)', zIndex: 4, cursor: drag ? 'grabbing' : 'grab' }}
        onMouseDown={(e) => { setDrag(true); dragStartX.current = e.clientX; }}>
        <PrismCanvas containerRef={canvasWrap} />
      </div>

    </div>
  );
}



// Partners, milestones, philosophy and research copy live in /content — see CONTENT.md.
const MARQUEE_CSS = '@keyframes prismMarqueeL{from{transform:translateX(0)}to{transform:translateX(-50%)}}.prism-marquee:hover .prism-track{animation-play-state:paused}';

function PartnerLogo({ name, en, src, h = 44 }) {
  const BOX_H = 60;
  return (
    <div title={name} style={{ width: 210, height: BOX_H, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0 18px', flexShrink: 0, boxSizing: 'content-box', transition: 'transform .3s var(--ease-out)' }}
      onMouseEnter={e => { e.currentTarget.style.transform = 'scale(1.06)'; }}
      onMouseLeave={e => { e.currentTarget.style.transform = 'none'; }}>
      {src
        ? <img src={src} alt={name} style={{ height: h, maxWidth: '100%', width: 'auto', objectFit: 'contain', display: 'block' }} />
        : <div style={{ textAlign: 'center' }}>
            <div style={{ font: 'var(--type-h4)', color: 'var(--text-heading)', whiteSpace: 'nowrap', lineHeight: 1.2 }}>{name}</div>
            <div style={{ fontFamily: 'var(--font-display-en)', fontSize: 10, letterSpacing: '.06em', color: 'var(--neutral-500)', whiteSpace: 'nowrap', marginTop: 3 }}>{en}</div>
          </div>}
    </div>
  );
}

function PartnerRow({ items, duration }) {
  const loop = items.concat(items);
  return (
    <div className="prism-marquee" style={{ overflow: 'hidden', WebkitMaskImage: 'linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%)', maskImage: 'linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%)' }}>
      <div className="prism-track" style={{ display: 'flex', alignItems: 'center', width: 'max-content', animation: 'prismMarqueeL ' + duration + 's linear infinite' }}>
        {loop.map((p, i) => <PartnerLogo key={i} name={T(p.name)} en={p.en} src={window.RES(p.logo)} h={p.logoHeight} />)}
      </div>
    </div>
  );
}

function Partners() {
  const sec = PrismContent.partnersSection, items = PrismContent.pick(PrismContent.partners);
  if (!items.length) return null;
  return (
    <div style={{ background: 'var(--bg-subtle)', padding: '72px 40px' }}>
      <style>{MARQUEE_CSS}</style>
      <div style={{ maxWidth: 'var(--container-max)', margin: '0 auto' }}>
        <div style={{ marginBottom: 48 }}>
          <PrismRule />
          <div style={{ font: 'var(--type-label)', letterSpacing: 'var(--ls-caps)', textTransform: 'uppercase', color: 'var(--brand-secondary)', margin: '18px 0 10px', fontSize: 11 }}>{sec.eyebrow}</div>
          <h2 style={{ font: 'var(--type-h2)', fontWeight: 600, color: 'var(--text-heading)', margin: 0 }}>{T(sec.title)}</h2>
        </div>
        <PartnerRow items={items} duration={sec.marqueeDuration} />
        <p style={{ font: 'var(--type-caption)', fontSize: 12, color: 'var(--neutral-500)', textAlign: 'center', margin: '38px 0 0' }}>{T(sec.footnote)}</p>
      </div>
    </div>
  );
}



const TIMELINE_CSS = '.prism-vtl{position:relative;padding:8px 0 4px}.prism-vtl-axis{position:absolute;left:50%;top:0;bottom:0;width:1px;transform:translateX(-.5px);background:linear-gradient(180deg,rgba(126,200,227,0),var(--prism-200) 8%,var(--prism-200) 92%,rgba(126,200,227,0))}.prism-vtl-row{position:relative;display:grid;grid-template-columns:1fr 1px 1fr;align-items:center;padding:34px 0}.prism-vtl-node{opacity:0;transform:translateY(18px);transition:opacity .7s var(--ease-out),transform .7s var(--ease-out)}.prism-vtl-row.is-in .prism-vtl-node{opacity:1;transform:none}.prism-vtl-mark{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:8px;z-index:2}.prism-vtl-shard{width:11px;height:11px;background:var(--gradient-prism);transform:rotate(45deg);opacity:.9;box-shadow:0 0 0 6px var(--bg-page);pointer-events:none;cursor:default}.prism-vtl-row:hover .prism-vtl-date{color:var(--brand-primary)}.prism-vtl-date{transition:color .3s var(--ease-out)}.prism-vtl-connect{position:absolute;top:50%;height:1px;width:44px;background:linear-gradient(90deg,var(--prism-200),rgba(126,200,227,.25))}@media(max-width:860px){.prism-vtl-axis{left:5px}.prism-vtl-row{grid-template-columns:1fr;padding:24px 0 24px 40px}.prism-vtl-mark{left:5px;top:34px;transform:translate(-50%,0)}.prism-vtl-connect{display:none}.prism-vtl-node{grid-column:1!important;justify-self:start!important;text-align:left!important;padding:0!important;max-width:none}}';

// Wireframe prisms + relay beam overlaid on the timeline column. Desktop only: below 860px the
// axis moves to the left edge and there is no clear side for a prism.
function TimelineBeam({ hostRef, count }) {
  React.useEffect(() => {
    const host = hostRef.current;
    if (!host || !count) return;
    let api = null, disposed = false, mod = null;
    // gate on the same breakpoint as TIMELINE_CSS (viewport, not host width) and re-check on change
    const mq = window.matchMedia('(min-width:861px)');
    const layout = () => {
      const w = host.clientWidth;
      const rows = Array.from(host.querySelectorAll('.prism-vtl-row'));
      const off = Math.min(150, w * 0.16);
      const prisms = rows.slice(0, count).map((r, i) => ({
        x: w / 2 + (i % 2 === 0 ? off : -off),   // opposite the copy, which alternates too
        y: r.offsetTop + r.offsetHeight / 2
      }));
      return { prisms, size: w >= 1040 ? 74 : Math.max(48, w * 0.07), entry: { x: w / 2, y: 0 } };
    };
    const mount = () => {
      if (disposed || api || !mod || !mq.matches) return;
      api = mod.createTimelineBeam(host, layout());
      api.play();   // loops; the scene itself pauses out of view and on tab blur
    };
    const unmount = () => { api && api.dispose(); api = null; };
    // 懒加载：时间轴在首页第 3 屏，进入视口前 200px 才 import + init，避免与 Hero 场景
    // 在首屏叠加。init 后场景自身的 IntersectionObserver 继续负责离屏暂停。
    let gate = null;
    const boot = () => {
      if (disposed || mod) return;
      dynImport(window.RES('timeline-beam-scene') === 'timeline-beam-scene' ? './TimelineBeamScene.js' : window.RES('timeline-beam-scene')).then(m => {
        if (disposed) return;
        mod = m; mount();
      });
    };
    if (window.IntersectionObserver) {
      gate = new IntersectionObserver(es => {
        if (es.some(e => e.isIntersecting)) { gate.disconnect(); gate = null; boot(); }
      }, { rootMargin: '200px 0px' });
      gate.observe(host);
    } else {
      boot();
    }
    const onMq = () => (mq.matches ? mount() : unmount());
    mq.addEventListener ? mq.addEventListener('change', onMq) : mq.addListener(onMq);
    const ro = new ResizeObserver(() => { if (api) api.set(layout()); });
    ro.observe(host);
    return () => {
      disposed = true; ro.disconnect(); gate && gate.disconnect();
      mq.removeEventListener ? mq.removeEventListener('change', onMq) : mq.removeListener(onMq);
      unmount();
    };
  }, [count]);
  return null;
}

function Milestones() {
  const sec = PrismContent.timelineSection, items = PrismContent.pick(PrismContent.timeline);
  const ref = React.useRef(null);
  React.useEffect(() => {
    const rows = ref.current ? Array.from(ref.current.querySelectorAll('.prism-vtl-row')) : [];
    if (!('IntersectionObserver' in window)) { rows.forEach(r => r.classList.add('is-in')); return; }
    const io = new IntersectionObserver(es => es.forEach(e => { if (e.isIntersecting) { e.target.classList.add('is-in'); io.unobserve(e.target); } }), { rootMargin: '0px 0px -10% 0px', threshold: 0 });
    rows.forEach(r => io.observe(r));
    const bail = setTimeout(() => rows.forEach(r => r.classList.add('is-in')), 2000);
    return () => { clearTimeout(bail); io.disconnect(); };
  }, [items.length]);
  if (!items.length) return null;
  return (
    <Section eyebrow={sec.eyebrow} title={T(sec.title)}>
      <style>{TIMELINE_CSS}</style>
      <div className="prism-vtl" ref={ref}>
        <span className="prism-vtl-axis" />
        <TimelineBeam hostRef={ref} count={items.length} />
        {/* 行尾全角标点补偿：「。」等全角标点是 1em 宽而墨点只占左下角，行尾会多出约 .62em 空白，
            于是右对齐节点的正文视觉右缘比标题短一截（13px 字号下实测 8px）。这里对**右对齐节点**
            且**结尾是全角标点**的正文补一个负右边距，把这段空白吃掉，两行墨迹右缘落到同一条竖线。
            做成规则而非改文案：左右分栏是按 i % 2 交替算的，一旦增删里程碑所有节点会左右互换，
            在 content 里删句号的做法当场失效。
            未用 CSS 的 hanging-punctuation：Chrome 至今不支持，只有 Safari 认。 */}
        {items.slice().reverse().map((m, i) => {
          const left = i % 2 === 0;
          const desc = T(m.description);
          const fwEnd = /[。．，、；：！？」』）】]$/.test(desc);
          return (
            <div key={m.id} className="prism-vtl-row">
              <span className="prism-vtl-mark">
                <span className="prism-vtl-shard" />
                <span style={{ fontFamily: 'var(--font-display-en)', fontSize: 11, letterSpacing: '.06em', color: 'var(--brand-secondary)' }}>{'0' + (i + 1)}</span>
              </span>
              <span className="prism-vtl-connect" style={left ? { right: '50%', marginRight: 12 } : { left: '50%', marginLeft: 12 }} />
              <div className="prism-vtl-node" style={{ gridColumn: left ? 1 : 3, maxWidth: 420, justifySelf: left ? 'end' : 'start', textAlign: left ? 'right' : 'left', paddingRight: left ? 68 : 0, paddingLeft: left ? 0 : 68, transitionDelay: (i % 2 ? .06 : 0) + 's' }}>
                <div className="prism-vtl-date" style={{ fontFamily: 'var(--font-display-en)', fontSize: 34, lineHeight: 1.1, letterSpacing: '.01em', color: 'var(--brand-secondary)' }}>{m.date}</div>
                <h3 style={{ font: 'var(--type-h4)', color: 'var(--text-heading)', margin: '16px 0 10px', textWrap: 'pretty' }}>{T(m.title)}</h3>
                <p style={{ font: 'var(--type-caption)', color: 'var(--text-muted)', margin: 0, marginRight: (left && fwEnd) ? '-.62em' : 0, lineHeight: 1.9, textWrap: 'pretty' }}>{desc}</p>
              </div>
            </div>
          );
        })}
      </div>
    </Section>
  );
}


// 第 4 栏粒子聚散正四面体：Three.js 装饰，独立于理念轮播；离屏/隐藏页暂停。
function ParticleTetra() {
  const wrap = React.useRef(null);
  const [fail, setFail] = React.useState(false);
  React.useEffect(() => {
    let gl = null;
    try { gl = document.createElement('canvas').getContext('webgl') || document.createElement('canvas').getContext('experimental-webgl'); } catch (e) { gl = null; }
    if (!gl) { setFail(true); return; }
    let disposed = false, api = null, io = null, gate = null;
    // 懒加载：本模块在首页第 6 屏，急加载会让它和 Hero 的 Three 场景在首屏同时争主线程和
    // GPU。改为进入视口前 200px 才 import + init；init 后仍由 io 负责离屏暂停 rAF。
    const boot = () => {
      if (disposed || api) return;
      const ready = window.THREE ? Promise.resolve() : new Promise(r => window.addEventListener('three-ready', r, { once: true }));
      return ready.then(() => dynImport(window.RES('philosophy-particles') === 'philosophy-particles' ? './PhilosophyParticles.js' : window.RES('philosophy-particles'))).then(m => {
        if (disposed || !wrap.current || api) return;
        api = m.createParticleTetra(wrap.current);
        if (window.IntersectionObserver) {
          io = new IntersectionObserver(es => { api && api.setPaused(!es.some(e => e.isIntersecting)); }, { threshold: 0 });
          io.observe(wrap.current);
        }
      }).catch(() => setFail(true));
    };
    if (window.IntersectionObserver && wrap.current) {
      gate = new IntersectionObserver(es => {
        if (es.some(e => e.isIntersecting)) { gate.disconnect(); gate = null; boot(); }
      }, { rootMargin: '200px 0px' });
      gate.observe(wrap.current);
    } else {
      boot();
    }
    return () => { disposed = true; gate && gate.disconnect(); io && io.disconnect(); api && api.dispose(); };
  }, []);
  return <div ref={wrap} aria-hidden="true" style={{ position: 'absolute', inset: 0, background: fail ? 'radial-gradient(120% 90% at 60% 40%,rgba(126,200,227,.28),rgba(74,127,181,.14) 55%,transparent 78%)' : 'transparent', pointerEvents: 'none' }} />;
}

// 「投资理念」模块：纵向序号栏 + 分栏轮播（标题 / 正文 / 预留区）。
const PHILOS_CSS = '.prism-phil{display:grid;grid-template-columns:1fr 30%;column-gap:clamp(64px,5vw,80px);align-items:center;min-height:440px;isolation:isolate}.prism-phil-list{display:flex;flex-direction:column;justify-content:center;min-height:360px}.prism-phil-row{position:relative;border:0;background:none;text-align:left;width:100%;padding:22px 6px 22px 0;cursor:default;outline:none}.prism-phil-row::after{content:"";position:absolute;left:0;bottom:0;width:min(340px,78%);height:1px;background:var(--border-hairline)}.prism-phil-row:last-child::after{display:none}.prism-phil-row:first-child{padding-top:6px}.prism-phil-head{display:flex;align-items:baseline;gap:.5em}.prism-phil-idx{font-family:var(--font-display-en);font-weight:600;font-size:26px;color:#4A7FB5;flex:none}.prism-phil-name{font-weight:600;font-size:clamp(26px,2.2vw,30px);color:#1F3A5F;flex:1 1 auto;letter-spacing:.01em;transition:color .3s var(--ease-out)}.prism-phil-row.is-open .prism-phil-name,.prism-phil-row:focus-visible .prism-phil-name{color:#16324f}.prism-phil-wrap{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .24s var(--ease-out),opacity .22s var(--ease-out)}.prism-phil-row.is-open .prism-phil-wrap{grid-template-rows:1fr;opacity:1;transition:grid-template-rows .3s var(--ease-out),opacity .3s var(--ease-out)}.prism-phil-copy{overflow:hidden;min-height:0}.prism-phil-copy p{margin:12px 0 0;padding-left:2.6em;font-weight:400;font-size:clamp(15px,1.05vw,16px);line-height:1.75;color:var(--text-muted);text-wrap:pretty}.prism-phil-reserve{position:relative;overflow:hidden;aspect-ratio:1/1;background:transparent}@media(prefers-reduced-motion:reduce){.prism-phil-wrap{transition:none}}@media(max-width:1024px){.prism-phil{grid-template-columns:1fr;column-gap:0}.prism-phil-reserve{display:none}}';

// 悬停自动展开（互斥单条）：鼠标进入某条展开其正文、离开收起；键盘聚焦同样展开。轻度防抖，快速滑过以最终停留为准。
function PhilosophyTabs({ items }) {
  const [open, setOpen] = React.useState(-1);
  const timer = React.useRef();
  const enter = (i) => { clearTimeout(timer.current); timer.current = setTimeout(() => setOpen(i), 55); };
  const leave = () => { clearTimeout(timer.current); timer.current = setTimeout(() => setOpen(-1), 140); };
  React.useEffect(() => () => clearTimeout(timer.current), []);
  return (
    <div className="prism-phil">
      <div className="prism-phil-list" onMouseLeave={leave}>
        {items.map((p, i) => (
          <div
            key={p.id}
            className={'prism-phil-row' + (open === i ? ' is-open' : '')}
            tabIndex={0}
            role="button"
            aria-expanded={open === i}
            onMouseEnter={() => enter(i)}
            onFocus={() => { clearTimeout(timer.current); setOpen(i); }}
            onBlur={(e) => { if (!e.currentTarget.parentNode.contains(e.relatedTarget)) leave(); }}
          >
            <span className="prism-phil-head">
              <span className="prism-phil-idx">{String(i + 1).padStart(2, '0')}.</span>
              <span className="prism-phil-name">{T(p.title)}</span>
            </span>
            <span className="prism-phil-wrap">
              <span className="prism-phil-copy" aria-hidden={open !== i}><p>{T(p.description)}</p></span>
            </span>
          </div>
        ))}
      </div>
      <div className="prism-phil-reserve"><ParticleTetra /></div>
    </div>
  );
}

// 「投资理念」模块（现置于关于棱镜页），实现与动画不随换页改动。
function PhilosophySection() {
  const philSec = PrismContent.philosophySection, philos = PrismContent.pick(PrismContent.philosophy);
  return (
    <Section tone="subtle" eyebrow={philSec.eyebrow} title={T(philSec.title)}>
      <style>{PHILOS_CSS}</style>
      <PhilosophyTabs items={philos} />
    </Section>
  );
}

function HomeScreen({ go, onGate }) {
  const [filter, setFilter] = React.useState('all');
  useLucide(filter);
  return (
    <div>
      <Hero go={go} />
      <WhoWeAre />

      <Partners />

      <LeadershipSection />

      <PhilosophySection />

      <Milestones />

    </div>
  );
}

Object.assign(window, { HomeScreen, PhilosophySection, ParticleTetra, TimelineBeam, Hero, TickerField, PrismCanvas, Partners, PartnerRow, PartnerLogo, Milestones, quote });
