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

// Data lives in /content/about/*.ts — see CONTENT.md.

const LEADERS_CSS = '.prism-lead-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:28px}.prism-lead{position:relative;overflow:hidden;border-radius:4px;border:1px solid var(--border-hairline);box-shadow:0 18px 44px -26px rgba(12,42,74,.35);background:var(--bg-subtle);min-width:0;height:600px}.prism-lead-photo{width:100%;height:100%;object-fit:cover;object-position:50% 12%;display:block;filter:saturate(.94) contrast(1.03) brightness(1.02)}.prism-lead-panel{position:absolute;left:0;right:0;top:auto;bottom:0;height:auto;max-height:100%;display:flex;flex-direction:column;padding:20px 20px 20px;background:linear-gradient(180deg,rgba(12,42,74,0),rgba(12,42,74,.86) 22%,rgba(12,42,74,.96) 40%);transform:translateY(calc(100% - 108px));transition:transform .62s cubic-bezier(.22,.7,.2,1)}.prism-lead.is-open .prism-lead-panel,.prism-lead:hover .prism-lead-panel,.prism-lead:focus .prism-lead-panel,.prism-lead:focus-within .prism-lead-panel{transform:translateY(0)}.prism-lead-role{font:var(--type-caption);color:rgba(255,255,255,.8);letter-spacing:.06em}.prism-lead-name{font:var(--type-h3);color:#fff;margin-top:6px}.prism-lead-rule{width:34px;height:1px;background:rgba(255,255,255,.4);margin:10px 0 10px;opacity:0;transition:opacity .45s var(--ease-out) .1s}.prism-lead.is-open .prism-lead-rule,.prism-lead:hover .prism-lead-rule,.prism-lead:focus .prism-lead-rule,.prism-lead:focus-within .prism-lead-rule{opacity:1}.prism-lead-bio{min-height:0;overflow-y:auto;scrollbar-width:none;display:grid;align-content:start;gap:9px;opacity:0;transition:opacity .45s var(--ease-out) .12s}.prism-lead.is-open .prism-lead-bio,.prism-lead:hover .prism-lead-bio,.prism-lead:focus .prism-lead-bio,.prism-lead:focus-within .prism-lead-bio{opacity:1}.prism-lead-bio::-webkit-scrollbar{display:none}.prism-lead-edu{font-size:12.5px;line-height:1.8;color:#fff;margin:0 0 4px;text-wrap:pretty}.prism-lead-bio p{font-size:12.5px;line-height:1.8;color:rgba(255,255,255,.84);margin:0;text-wrap:pretty}@media(max-width:860px){.prism-lead-grid{grid-template-columns:1fr}.prism-lead{height:auto;min-height:640px;cursor:pointer}}';


function LeaderCard({ leader }) {
  const [open, setOpen] = React.useState(false);
  return (
    <div className={'prism-lead' + (open ? ' is-open' : '')} tabIndex={0} onClick={() => setOpen(o => !o)}>
      <img className="prism-lead-photo" src={window.RES(leader.photo)} alt={T(leader.name)} />
      <div className="prism-lead-panel">
        <div className="prism-lead-role">{T(leader.role)}</div>
        <div className="prism-lead-name">{T(leader.name)}</div>
        <div className="prism-lead-rule" />
        <div className="prism-lead-bio">
          <p className="prism-lead-edu">{T(leader.bio[0])}</p>
          {leader.bio.slice(1).map((p, i) => <p key={i}>{T(p)}</p>)}
        </div>
      </div>
    </div>
  );
}

// 「领导团队」模块（现置于首页「合作机构」之下）。默认白底，配合首页白灰交替节奏。
function LeadershipSection() {
  const leadSec = PrismContent.leadershipSection, leaders = PrismContent.pick(PrismContent.leadership);
  return (
    <Section eyebrow={leadSec.eyebrow} title={T(leadSec.title)}>
      <style>{LEADERS_CSS}</style>
      <div className="prism-lead-grid">
        {leaders.map(l => <LeaderCard key={l.id} leader={l} />)}
      </div>
    </Section>
  );
}


const TEAM_STATS_CSS = '.prism-team-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:24px;align-items:start;isolation:isolate}.prism-team-stat{position:relative;outline-offset:2px;overflow:hidden;contain:layout paint;transform:translate3d(0,0,0);background:var(--surface-card);border:1px solid var(--border-hairline);padding:56px 32px 40px;text-align:center;transition:background .25s var(--ease-out),border-color .25s var(--ease-out),transform .25s var(--ease-out)}.prism-team-stat::after{content:"";position:absolute;inset:0;pointer-events:none;box-shadow:0 26px 60px -26px rgba(12,42,74,.45);opacity:0;transition:opacity .25s var(--ease-out)}.prism-team-mark{position:absolute;right:-28px;bottom:-38px;width:210px;height:210px;opacity:0;transform:translateZ(0);will-change:opacity;transition:opacity .2s var(--ease-out);pointer-events:none}.prism-team-num,.prism-team-label{position:relative;z-index:1}.prism-team-num{font-family:var(--font-display-en);font-size:64px;line-height:1;letter-spacing:.01em;color:var(--brand-secondary);transition:color .25s var(--ease-out)}.prism-team-label{font:var(--type-caption);color:var(--text-muted);margin-top:18px;letter-spacing:.06em;transition:color .25s var(--ease-out)}.prism-team-note{position:relative;z-index:1;height:60px;margin-top:14px;display:flex;align-items:flex-start;justify-content:center;opacity:0;transform:translate3d(0,8px,0);transition:opacity .2s var(--ease-out),transform .2s var(--ease-out)}.prism-team-note>span{display:block;font-size:13px;line-height:1.8;color:rgba(255,255,255,.86);text-wrap:pretty}.prism-team-stat:hover,.prism-team-stat:focus-visible{background:var(--brand-primary);border-color:var(--brand-primary);transform:translate3d(0,-6px,0);will-change:transform}.prism-team-stat:hover::after,.prism-team-stat:focus-visible::after{opacity:1}.prism-team-stat:hover .prism-team-note,.prism-team-stat:focus-visible .prism-team-note{opacity:1;transform:translate3d(0,0,0);will-change:opacity,transform}.prism-team-stat:hover .prism-team-num,.prism-team-stat:focus-visible .prism-team-num{color:#fff}.prism-team-stat:hover .prism-team-label,.prism-team-stat:focus-visible .prism-team-label{color:rgba(255,255,255,.78)}.prism-team-stat:hover .prism-team-mark,.prism-team-stat:focus-visible .prism-team-mark{opacity:1}@media(max-width:860px){.prism-team-stats{grid-template-columns:1fr}.prism-team-stat{padding:44px 28px 32px;background:var(--brand-primary);border-color:var(--brand-primary)}.prism-team-num{font-size:52px;color:#fff}.prism-team-label{color:rgba(255,255,255,.78)}.prism-team-note{opacity:1;transform:none;height:auto}.prism-team-mark{opacity:.14}}';

const ABOUT_TOP_CSS = '.prism-about-top{margin-top:-22px;display:grid;grid-template-columns:repeat(3,1fr);gap:24px;align-items:center}.prism-about-copy{font:var(--type-body);color:var(--text-body);line-height:1.95;margin:0;text-wrap:pretty}.prism-about-model{width:100%;margin:0 auto;display:flex;align-items:center;justify-content:center;transform:translateZ(0);contain:paint;isolation:isolate}@media(max-width:860px){.prism-about-top{grid-template-columns:1fr;gap:32px}}';


const SLOT_CSS = '.prism-slot{display:flex;box-sizing:border-box;width:88%;max-width:420px;margin:0 auto;border:1px solid rgba(31,58,95,.12);border-radius:10px;background:#fff;box-shadow:0 10px 30px -24px rgba(12,42,74,.45);overflow:hidden;transform:translateZ(0);contain:layout paint;isolation:isolate}.prism-slot-cell{position:relative;box-sizing:border-box;flex:1 1 0;min-width:0;aspect-ratio:1/1;overflow:hidden;box-shadow:inset 1px 0 0 rgba(31,58,95,.08)}.prism-slot-cell:first-child{box-shadow:none}.prism-slot-cell:before,.prism-slot-cell:after{content:"";position:absolute;left:0;right:0;height:15%;z-index:2;pointer-events:none}.prism-slot-cell:before{top:0;background:linear-gradient(#fff,rgba(255,255,255,0))}.prism-slot-cell:after{bottom:0;background:linear-gradient(rgba(255,255,255,0),#fff)}.prism-slot-track{height:100%;transform:translate3d(0,0,0);backface-visibility:hidden}.prism-slot-item{display:flex;align-items:center;justify-content:center;font-family:var(--font-sans);font-weight:600;color:var(--brand-primary);font-size:clamp(18px,2.1vw,27px);line-height:1;user-select:none}.prism-slot-item img{width:62%;max-width:34px;display:block}.prism-slot-fade{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;transition:opacity .3s var(--ease-out)}';

// 分面路径取自 Logo 几何（viewBox 1000x880），三条分割线自左下顶点放射至右棱边。
function PrismMark() {
  return (
    <svg className="prism-team-mark" viewBox="0 0 1000 880" aria-hidden="true" fill="#D6EEF8">
      <path d="M 500 24 L 703 376 L 24 856 Z" fillOpacity=".16" />
      <path d="M 703 376 L 797 539 L 24 856 Z" fillOpacity=".10" />
      <path d="M 797 539 L 886 693 L 24 856 Z" fillOpacity=".14" />
      <path d="M 886 693 L 980 856 L 24 856 Z" fillOpacity=".08" />
    </svg>
  );
}

// 五格滚筒（老虎机）：A「▲棱镜私募」→ B「Prism」两条循环，单一 rAF 驱动五格。
// 2026-08-28：原第三条「量化FOF」按要求从词条数组中真正删除（非隐藏）。其专用字符
// 「量」「化」「F」「O」也一并从滚筒字符表移除——它们只被该条使用，留在表里滚动过程中
// 仍会连续读出「量化FO」，即所谓残留半条。剩余索引相应前移（'P' 由 14 → 10），
// 'p' 是原有的小写滚动填充字符（任何词条都不落在它上面），与本次删除无关，保留不动。
// 轮播条数不是硬编码：startSpin 用 next=(next+1)%SLOT_FORMS.length，reduced-motion
// 分支用 SLOT_FORMS.map + (f+1)%SLOT_FORMS.length，两处都随数组长度走，两条即在
// 0↔1 之间循环，无空转。格子数（5）、动画时长（1200+i*175）、缓动、停留间隔
// SLOT_HOLD、以及 .prism-slot* 全部样式均未改动。
// 唯一的连带变化：滚筒字符表由 15 个缩短为 11 个，单次旋转经过的字符数随之减少，
// 模糊段密度略降；落位、时长、缓动、节奏完全一致。
const SLOT_CHARS = ['棱', '镜', '私', '募', 'p', 'r', 'i', 's', 'm', 'LOGO', 'P'];
const SLOT_FORMS = [[9, 0, 1, 2, 3], [10, 5, 6, 7, 8]];
const SLOT_HOLD = 3000;

function SlotWord() {
  const cells = React.useRef([]);
  const box = React.useRef(null);
  const [reduced] = React.useState(() => window.matchMedia('(prefers-reduced-motion: reduce)').matches);
  const [form, setForm] = React.useState(0);

  React.useEffect(() => {
    if (reduced) {
      const t = setInterval(() => setForm(f => (f + 1) % SLOT_FORMS.length), SLOT_HOLD);
      return () => clearInterval(t);
    }
    const N = SLOT_CHARS.length;
    let H = 0, L = 0, raf = 0, paused = false, offscreen = false, dirty = true, wc = false;
    const st = SLOT_FORMS[0].map(idx => ({ from: idx, to: idx, t0: 0, dur: 0, cur: idx }));
    let next = 1, phaseStart = 0, spinning = false;
    // 步进高度取自滚筒自身解析后的内容高度，rAF 回调内绝不读布局（避免强制同步布局）
    const ro = new ResizeObserver(es => {
      const h = es[es.length - 1].contentRect.height;
      if (h > 0) { H = h; L = H * N; dirty = true; }
    });
    if (cells.current[0]) ro.observe(cells.current[0], { box: 'content-box' });

    const setWC = (on) => {
      if (on === wc) return;
      wc = on;
      for (let i = 0; i < 5; i++) {
        const el = cells.current[i] && cells.current[i].firstChild;
        if (el) el.style.willChange = on ? 'transform, opacity' : 'auto';
      }
    };
    const startSpin = (now) => {
      const targets = SLOT_FORMS[next];
      st.forEach((s, i) => {
        s.from = s.cur;
        const delta = ((targets[i] - (s.from % N)) % N + N) % N;
        s.to = s.from + (4 + i) * N + delta;
        s.dur = 1200 + i * 175;
        s.t0 = now;
      });
      next = (next + 1) % SLOT_FORMS.length;
      spinning = true;
      dirty = true;
      setWC(true);
    };
    const tick = (now) => {
      raf = requestAnimationFrame(tick);
      if (paused || offscreen || !H) return;
      if (!phaseStart) { phaseStart = now; startSpin(now); }
      // 静止期（定格停留）不做任何样式写入，主线程完全让给交互
      if (!spinning && !dirty) {
        if (now - phaseStart > SLOT_HOLD) startSpin(now);
        return;
      }
      let done = true;
      for (let i = 0; i < 5; i++) {
        const s = st[i];
        const el = cells.current[i] && cells.current[i].firstChild;
        if (!el) continue;
        const p = s.dur ? Math.min(1, (now - s.t0) / s.dur) : 1;
        if (p < 1) done = false;
        const e = 1 - Math.pow(1 - p, 3);
        let y = (s.from + (s.to - s.from) * e) * H;
        if (p > 0.85 && p < 1) y += Math.sin((p - 0.85) / 0.15 * Math.PI) * 3;
        if (p >= 1) { s.cur = s.to % N; y = s.cur * H; }
        el.style.transform = 'translate3d(0,' + (-(y % L)) + 'px,0)';
        el.style.opacity = p < 0.82 ? '0.55' : '1';
      }
      dirty = false;
      if (spinning && done) { spinning = false; phaseStart = now; setWC(false); }
      if (!spinning && now - phaseStart > SLOT_HOLD) startSpin(now);
    };
    raf = requestAnimationFrame(tick);
    const vis = () => {
      paused = document.hidden;
      if (!paused) { const d = performance.now(); st.forEach(s => { s.t0 = d - Math.min(s.dur, d - s.t0); }); phaseStart = d; dirty = true; }
    };
    document.addEventListener('visibilitychange', vis);
    let io = null;
    if (window.IntersectionObserver && box.current) {
      io = new IntersectionObserver(es => {
        const on = es.some(e => e.isIntersecting);
        offscreen = !on;
        if (on) { const d = performance.now(); st.forEach(s => { s.t0 = d - Math.min(s.dur, d - s.t0); }); phaseStart = d; dirty = true; }
        else {
          setWC(false);
          for (let i = 0; i < 5; i++) { const el = cells.current[i] && cells.current[i].firstChild; if (el) el.style.opacity = '1'; }
        }
      }, { threshold: 0 });
      io.observe(box.current);
    }
    return () => { cancelAnimationFrame(raf); ro.disconnect(); io && io.disconnect(); document.removeEventListener('visibilitychange', vis); };
  }, [reduced]);

  const glyph = (k) => k === 'LOGO' ? <img src={window.RES('../../assets/logo-mark.png')} alt="" /> : k;

  return (
    <div className="prism-slot" ref={box} aria-label="棱镜私募 Prism">
      {[0, 1, 2, 3, 4].map(i => (
        <div className="prism-slot-cell" key={i} ref={el => { cells.current[i] = el; }} aria-hidden="true">
          {reduced ? SLOT_FORMS.map((fm, k) => (
            <div className="prism-slot-fade prism-slot-item" key={k} style={{ opacity: form === k ? 1 : 0 }}>{glyph(SLOT_CHARS[fm[i]])}</div>
          )) : (
            <div className="prism-slot-track">
              {SLOT_CHARS.concat(SLOT_CHARS).map((k, j) => (
                <div className="prism-slot-item" key={j} style={{ height: '100%', aspectRatio: '1/1' }}>{glyph(k)}</div>
              ))}
            </div>
          )}
        </div>
      ))}
    </div>
  );
}

// 「我们是谁」模块（现置于首页），实现与配色不随换页改动。
function WhoWeAre() {
  const intro = PrismContent.aboutIntro, stats = PrismContent.pick(PrismContent.teamStats);
  return (
    <Section eyebrow={intro.eyebrow} title={T(intro.title)} style={{ paddingBottom: 48 }}>
      <style>{TEAM_STATS_CSS + ABOUT_TOP_CSS + SLOT_CSS}</style>
      <div className="prism-about-top">
        <p className="prism-about-copy">{T(intro.left)}</p>
        <div className="prism-about-model"><SlotWord /></div>
        <p className="prism-about-copy">{T(intro.right)}</p>
      </div>
      <div className="prism-team-stats" style={{ marginTop: 108 }}>
        {stats.map(s => (
          <div key={s.id} className="prism-team-stat" tabIndex={0}>
            <PrismMark />
            <div className="prism-team-num">{PrismContent.lang === 'en' && s.valueEn ? s.valueEn : s.value}</div>
            <div className="prism-team-label">{T(s.label)}</div>
            <div className="prism-team-note"><span>{T(s.note)}</span></div>
          </div>
        ))}
      </div>
    </Section>
  );
}

const HERO_VIDEO_CSS = '@keyframes prismHeroIn{from{opacity:0;transform:translate3d(0,12px,0)}to{opacity:1;transform:none}}.prism-hero-vid{position:relative;width:100%;overflow:hidden;background:#FAFBFC;opacity:0;aspect-ratio:var(--hero-ar,2.24/1)}.prism-hero-vid.is-in{animation:prismHeroIn .7s var(--ease-out) both}.prism-hero-poster,.prism-hero-media{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;object-position:center 50%;display:block;background:#FAFBFC}.prism-hero-media{pointer-events:none}.prism-hero-slot{position:absolute;inset:0;contain:paint}.prism-hero-slot video,video.prism-hero-media{transform:translateZ(0);backface-visibility:hidden}.prism-hero-scrim{position:absolute;inset:0;z-index:2;pointer-events:none;background:radial-gradient(ellipse 72% 62% at 50% 50%,rgba(6,16,30,.34),rgba(6,16,30,.08) 60%,rgba(6,16,30,0) 82%)}.prism-hero-slogan{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:5;pointer-events:none;width:92%;max-width:960px;text-align:center;color:#fff;font-family:\"Noto Serif SC\",\"Source Han Serif SC\",\"Songti SC\",serif;font-weight:600;font-size:clamp(32px,4.4vw,60px);line-height:1.3;letter-spacing:.04em;text-shadow:0 2px 16px rgba(6,16,30,.55),0 1px 3px rgba(6,16,30,.5);white-space:nowrap;word-break:keep-all}html[lang=\"en\"] .prism-hero-slogan{white-space:normal;letter-spacing:.02em}.prism-hero-play-big{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:4;width:76px;height:76px;border-radius:50%;border:1px solid rgba(255,255,255,.5);background:rgba(10,24,42,.4);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);color:#fff;display:inline-flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .2s var(--ease-out),background-color .2s var(--ease-out)}.prism-hero-play-big:hover{transform:translate(-50%,-50%) scale(1.06);background:rgba(10,24,42,.6)}.prism-hero-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--text-muted);font-size:15px;letter-spacing:.1em}@media(max-width:767px){.prism-hero-vid{--hero-ar:1.4/1}.prism-hero-slogan{top:24%}}@media(max-width:560px){.prism-hero-slogan{white-space:normal}}@media(prefers-reduced-motion:reduce){.prism-hero-vid{opacity:1;animation:none}}';

// 页顶宣传片横幅（可复用）：「关于棱镜」与「加入我们」两页共用一份视频逻辑。
// showSlogan 控制是否叠加正中心 slogan（文案取自 content/about/hero-video.ts 的 slogan 字段）；
// showScrim 独立控制其下的径向暗化遮罩，可在显示 slogan 的同时关闭遮罩、保持画面纯净。
// 降级：无 src 显示占位；加载失败回落 fallbackImage/渐变；移动端与 reduced-motion 不自动播放，改为 poster + 点击播放入口。
function AboutHeroVideo({ showSlogan = true, showScrim = true } = {}) {
  const cfg = PrismContent.aboutHeroVideo || {};
  const src = cfg.src ? window.RES(cfg.src) : '';
  const webm = cfg.srcWebm ? window.RES(cfg.srcWebm) : '';
  const poster = cfg.poster ? window.RES(cfg.poster) : '';
  const fallback = cfg.fallbackImage ? window.RES(cfg.fallbackImage) : poster;
  const [reduced] = React.useState(() => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches));
  const [mobile] = React.useState(() => !!(window.matchMedia && window.matchMedia('(max-width:767px)').matches));
  const canAuto = !!src && !reduced && !mobile;
  // 乒乓模式：仅在可自动播放（非 reduced-motion / 非移动端）时生效；否则退回普通 loop。
  const pingpong = cfg.playbackMode === 'pingpong' && canAuto;
  const [active, setActive] = React.useState(canAuto);
  const [failed, setFailed] = React.useState(false);
  const [entered, setEntered] = React.useState(false);
  const hostRef = React.useRef(null);
  const slotRef = React.useRef(null);
  const vref = React.useRef(null);
  useLucide();

  // 【2026-08-14 关于「回到顶部二次卡顿」】先实测再动手，结论与最初的猜测不同：
  // 循环模式（cfg.playbackMode === 'loop'，当前配置）下视频**从来没有**被 unmount、
  // 被 pause、或重新拉流。整个下滑→回顶周期内实测 readyState 恒为 4、paused 恒为 false、
  // buffered 恒为整段 11s、currentTime 连续推进、loop 由原生属性接管（v.loop = !pingpong）。
  // 下面那段带 IntersectionObserver 暂停 + currentTime 手动 seek 的代码只属于 pingpong 分支，
  // 当前配置下整个 effect 第一行就 return 了，不参与运行。
  //
  // 实测丢帧率约 1.3–1.9%，且轻微集中在重新进入视口的那一刻（回顶期间丢帧多于下滑期间），
  // 症状对得上「有概率短时卡顿」，但量级很小。据此在 HERO_VIDEO_CSS 里给视频加了
  // translateZ(0) + backface-visibility:hidden，容器加 contain:paint，把它钉在自己的合成层上，
  // 避免重新进入视口时整层重新栅格化。
  // ⚠ 这一条**没有被实测证实有效**：验证环境是 headless Chrome 且带 --disable-gpu，
  // 根本没有 GPU 合成，A/B 结果（开/关各三轮）完全落在噪声里。留着是因为它是低风险的
  // 标准做法且在真机上有理由生效；没有加 will-change:transform——那会长期占住一块
  // 视频尺寸的 GPU 内存，在收益未经证实的情况下不划算。真机上若仍卡顿，先从这里查。
  //
  // 全站单例视频：index.html 的启动加载门已经把它挂好并解码到 canplaythrough，
  // 这里只是把同一个 DOM 节点从 #prism-video-holder 搬进横幅、卸载时再搬回去。
  // 【本段覆盖上一版「preload=none + 进视口才 mount」的策略】——那样切页要现解码，
  // 会先显示一段 poster 静图；现在改为启动时付出代价，换切页即时播放。
  const adopted = !!window.__prismVideo;
  React.useLayoutEffect(() => {
    const v = window.__prismVideo, slot = slotRef.current;
    if (!v || !slot) return;
    slot.appendChild(v);
    vref.current = v;
    v.loop = !pingpong;
    const p = v.play();
    if (p && p.catch) p.catch(() => {});
    return () => {
      try { v.pause(); } catch (e) {}
      const holder = document.getElementById('prism-video-holder');
      if (holder) holder.appendChild(v);   // 搬回holder，保留已缓冲/已解码状态供下次切回
      if (vref.current === v) vref.current = null;
    };
  }, [pingpong]);

  // adopted 时视频由上面的 layout effect 接管；否则（启动预载被跳过/失败）走原来的内联渲染。
  const showVideo = active && !!src && !failed;

  React.useEffect(() => { const r = requestAnimationFrame(() => setEntered(true)); return () => cancelAnimationFrame(r); }, []);

  // 乒乓/boomerang 播放：正放到底 → rAF 逐帧递减 currentTime 倒放回开头 → 再正放。
  // 仅在视频进入视口且标签页可见时运行；卸载/滚出/切标签均暂停并清理。
  React.useEffect(() => {
    if (!pingpong || !showVideo) return;
    const v = vref.current;
    if (!v) return;
    let raf = 0, dir = 1, last = 0, inView = true, running = false;
    const SPEED = 1; // 倒放速率倍数（1 = 与正放同速）
    const stop = () => { if (raf) { cancelAnimationFrame(raf); raf = 0; } };
    const tick = (now) => {
      raf = 0;
      if (!running) return;
      const dt = last ? Math.min((now - last) / 1000, 0.1) : 0;
      last = now;
      const dur = v.duration || 0;
      if (dir === 1) {
        // 正放阶段：交给原生 play()，接近结尾时切入倒放
        if (dur && v.currentTime >= dur - 0.05) { dir = -1; try { v.pause(); } catch (e) {} }
      } else {
        // 倒放阶段：逐帧 seek 递减，到头切回正放
        const next = v.currentTime - dt * SPEED;
        if (next <= 0.05) { v.currentTime = 0; dir = 1; try { v.play(); } catch (e) {} }
        else { try { v.currentTime = next; } catch (e) {} }
      }
      raf = requestAnimationFrame(tick);
    };
    const start = () => { if (running) return; running = true; last = 0; if (dir === 1) { try { v.play(); } catch (e) {} } raf = requestAnimationFrame(tick); };
    const pause = () => { running = false; stop(); try { v.pause(); } catch (e) {} };
    const sync = () => { (inView && !document.hidden) ? start() : pause(); };
    const io = ('IntersectionObserver' in window) ? new IntersectionObserver((es) => { inView = es[0] && es[0].isIntersecting; sync(); }, { threshold: 0.01 }) : null;
    if (io) io.observe(v); else inView = true;
    const onVis = () => sync();
    document.addEventListener('visibilitychange', onVis);
    sync();
    return () => { pause(); if (io) io.disconnect(); document.removeEventListener('visibilitychange', onVis); };
  }, [pingpong, showVideo]);

  const activate = () => setActive(true);

  return (
    <React.Fragment>
      {/* 画幅由 CSS 变量 --hero-ar 控制：桌面 2.24/1；≤767px 改 1.4/1，
          否则 2.24/1 在手机上只有 ~167px 高，slogan 与播放按钮无法共存。
          仍是 object-fit:cover，只改裁切比例，不会出黑边。 */}
      <div ref={hostRef} className={'prism-hero-vid' + (entered ? ' is-in' : '')}>
        <style>{HERO_VIDEO_CSS}</style>
        {showVideo && adopted ? (
          // 单例视频的落位插槽；真正的 <video> 由 layout effect 搬进来（已解码就绪）。
          <div ref={slotRef} className="prism-hero-slot" />
        ) : showVideo ? (
          <video
            ref={vref}
            className="prism-hero-media"
            poster={poster || undefined}
            autoPlay muted playsInline preload="auto" {...(pingpong ? {} : { loop: true })}
            onError={() => setFailed(true)}
          >
            {webm && <source src={webm} type="video/webm" />}
            <source src={src} type="video/mp4" />
          </video>
        ) : src ? (
          (poster || fallback) && <img className="prism-hero-poster" src={failed ? fallback : (poster || fallback)} alt="" fetchpriority="high" decoding="async" aria-hidden="true" />
        ) : (
          <div className="prism-hero-empty">宣传片待接入</div>
        )}
        {showSlogan && showScrim && <div className="prism-hero-scrim" aria-hidden="true" />}
        {showSlogan && <div className="prism-hero-slogan">{T(cfg.slogan)}</div>}
        {!active && !!src && (
          <button type="button" className="prism-hero-play-big" onClick={activate} aria-label="播放宣传片"><Icon name="play" size={26} /></button>
        )}
      </div>
    </React.Fragment>
  );
}

Object.assign(window, { AboutHeroVideo, WhoWeAre, LeaderCard, LeadershipSection, PrismMark, SlotWord });
