/* 移动端主页 — 数据库为主体，Agent 买电视助手为入口，小工具页挂前台工具。
   四个前台工具在壳内 push 打开，一个手机壳一站到底。
   复用 icons.jsx(Icon) / data.jsx(TVS) / images.js(IMG)。深链：?tab=tools · ?tool=about|room|light|inspect|setup */
const { useState, useEffect, useLayoutEffect, useRef } = React;
const cx = (...a) => a.filter(Boolean).join(' ');
const q = (k) => new URLSearchParams(location.search).get(k);
const REDUCE = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);

function Phone({ children }) {
  return (
    <div className="phone">
      <div className="pscreen">{children}</div>
      <div className="island"></div>
      <div className="phome"></div>
    </div>
  );
}

const ASKS = AG_ASKS; // 建议问题文案 + 剧本路由的单一事实源在 agent.jsx
const CMD = '我想买台客厅用的电视'; // 演示固定命令：点对话框打字机输入，点发送进入 Agent 对话

/* 前台只放四个固定工具；它们既可从本页独立打开，也可由 Agent 在对话中调用。
   名称/图标以 data.jsx TOOLS_FRONT 为单一事实源，这里只补主页私有的缩略图与色调。
   IMG.rev 是可选样片（_mkimg.py 缺图会跳过该键），与 mobile.jsx 同款兜底。 */
const TOOL_CARD_EXTRAS = {
  room: { img: () => IMG.wall, tone: 'room' },
  light: { img: () => IMG.rev || IMG.orig, tone: 'light' },
  inspect: { img: () => 'tool-inspect.webp', tone: 'inspect' },
  setup: { img: () => 'tool-setup.webp', tone: 'setup' },
};
const LIVE_TOOLS = TOOLS_FRONT.map((t) => ({ ...t, ...TOOL_CARD_EXTRAS[t.k] }));

const APPLIANCE_CATEGORIES = [
  { k: 'tv', name: '电视', desc: '画质 · 系统 · 音质', meta: `${DB_STATS.inStock} 台实测`, icon: 'tv', tone: 'tv', ready: true },
  { k: 'washer', name: '洗衣机', desc: '洗净 · 烘干 · 护理', meta: '即将上线', icon: 'washer', tone: 'washer' },
  { k: 'aircon', name: '空调', desc: '制冷 · 能效 · 静音', meta: '即将上线', icon: 'aircon', tone: 'aircon' },
  { k: 'fridge', name: '冰箱', desc: '保鲜 · 容量 · 能耗', meta: '即将上线', icon: 'fridge', tone: 'fridge' },
];

function HomeScreen({ on, openAbout, openDb, openAgent, frozen, brandVisible = true }) {
  const [ask, setAsk] = useState('');
  const [typing, setTyping] = useState(false);
  const tRef = useRef(null);
  useEffect(() => () => clearInterval(tRef.current), []);
  /* 点对话框：打字机敲入固定命令 */
  const typeCmd = () => {
    if (typing || ask === CMD) return;
    clearInterval(tRef.current); setTyping(true); setAsk('');
    let i = 0;
    tRef.current = setInterval(() => {
      i++; setAsk(CMD.slice(0, i));
      if (i >= CMD.length) { clearInterval(tRef.current); setTyping(false); }
    }, REDUCE ? 4 : 52);
  };
  /* 点发送：从 Agent 卡片 morph 进对话（空输入时直接用固定命令） */
  const send = (e) => {
    if (typing) return;
    e.stopPropagation();
    openAgent(e.currentTarget.closest('.agent'), ask || CMD);
  };
  return (
    <div className={cx('screen', !on && 'off')}>
      <div className="hm-head">
        <div className="hm-brand entry-brand-target" style={{ opacity: brandVisible ? 1 : 0 }} tabIndex={-1} aria-label="先看 AI">
          <span className="entry-brand-core hm-brand-core">
            <img className="entry-brand-logo" src="logo.png" alt="先看" draggable="false" />
            <span className="entry-brand-ai" aria-hidden="true">AI</span>
          </span>
        </div>
      </div>

      {/* AI 选购助手 — 主入口 */}
      <div className="agent">
        <span className="ag-glow g1"></span><span className="ag-glow g2"></span>
        <div className="ag-top">
          <span className="ag-orb"><Icon name="sparkle" size={22} /></span>
          <div className="ag-tt">
            <div className="ag-t">AI 选购助手</div>
            <div className="ag-s">从选到装到售后，直接问</div>
          </div>
        </div>
        <div className="ag-ask" role="button" tabIndex={0} onClick={typeCmd}>
          <span className={cx('ag-q', !ask && !typing && 'ph')}>
            {ask || (typing ? '' : '想买多大？预算多少？点我试试…')}
            {typing && <span className="ag-caret"></span>}
          </span>
          <button className={cx('ag-send', ask && !typing && 'go')} onClick={send} aria-label="发送">
            <Icon name="chevRight" size={17} />
          </button>
        </div>
        <div className="ag-chips">
          {ASKS.map((a) => <button key={a} className={cx('ag-chip', ask === a && 'on')} onClick={() => setAsk(a)}>{a}</button>)}
        </div>
      </div>

      {/* 数据资产 — 用户视角：在库量 + 即将入库 */}
      <button className="card data-asset-entry" type="button" onClick={openAbout} aria-label="了解我们的数据资产">
        <div className="sec-head">
          <Icon name="database" size={14} /><span>数据资产</span>
          <span className="data-asset-more">关于我们<Icon name="chevRight" size={13} /></span>
        </div>
        <div className="dbx">
          <div className="dbx-n">
            <b>{DB_STATS.inStock}</b>
            <span>台电视<br />实测在库</span>
          </div>
          <div className="dbx-side">
            <span className="lab-chip"><span className="lab-dot"></span>{DB_STATS.incoming} 台即将入库</span>
            <span className="dbx-sub">{DB_STATS.brands} 个品牌</span>
          </div>
        </div>
      </button>

      {/* 四大品类 — 电视已开放，其余品类明确展示建设状态 */}
      <div className="sec-row">
        <span className="sec-t">品类数据库</span>
        <span className="category-total"><Icon name="grid" size={12} />{APPLIANCE_CATEGORIES.length} 个品类</span>
      </div>
      {REDUCE ? (
        <div className="category-rail">{APPLIANCE_CATEGORIES.map((category) => <CategoryCard key={category.k} category={category} openDb={openDb} />)}</div>
      ) : (
        <div className="category-marq">
          {/* 壳内层打开时暂停跑马灯，省掉层下每帧重绘 */}
          <div className="category-track" style={{ animationPlayState: frozen ? 'paused' : 'running' }}>
            <div className="category-set">{APPLIANCE_CATEGORIES.map((category) => <CategoryCard key={category.k} category={category} openDb={openDb} />)}</div>
            <div className="category-set" aria-hidden="true">{APPLIANCE_CATEGORIES.map((category) => <CategoryCard key={'d' + category.k} category={category} openDb={openDb} duplicate />)}</div>
          </div>
        </div>
      )}
    </div>
  );
}

function AboutScreen({ onBack }) {
  const facts = [
    { n: DB_STATS.inStock, u: '台', l: '电视实测在库' },
    { n: DB_STATS.brands, u: '个', l: '主流品牌覆盖' },
    { n: DB_STATS.incoming, u: '台', l: '即将完成入库' },
  ];
  const values = [
    { icon: 'activity', title: '不是参数搬运', text: '每台电视都进入真实测试环境，记录画质、声音、系统和场景体验。' },
    { icon: 'layers', title: '数据真的能横向比', text: '同一套设备、流程和口径，让不同品牌、不同价位回到同一把尺子上。' },
    { icon: 'refresh', title: '答案持续生长', text: '新机持续入库，旧机价格与结论同步更新，让判断依据不过期。' },
  ];
  return (
    <main className="about-app">
      <header className="about-head">
        <button className="mh-back about-back" type="button" onClick={onBack} aria-label="返回首页">
          <Icon name="arrowLeft" size={18} />
        </button>
        <div className="about-head-brand">
          <img src="logo.png" alt="先看" draggable="false" />
          <div><b>关于我们</b><small>先看电视数据库</small></div>
        </div>
      </header>

      <div className="about-scroll">
        <section className="about-hero" aria-labelledby="about-title">
          <span className="about-orb orb-one" aria-hidden="true"></span>
          <span className="about-orb orb-two" aria-hidden="true"></span>
          <div className="about-kicker"><Icon name="database" size={14} />数据资产</div>
          <h1 id="about-title">我们在做全网最大的<br /><em>电视实测数据库</em></h1>
          <p>把亮度、色准、控光、声音、系统体验和真实场景表现，放进同一套可比较、可追溯的测试标准里。</p>
          <div className="about-proof"><Icon name="check" size={14} />每一条结论，都能回到实测证据</div>
        </section>

        <section className="about-stats" aria-label="数据库规模">
          {facts.map((f) => (
            <div className="about-stat" key={f.l}>
              <div><b>{f.n}</b><span>{f.u}</span></div>
              <small>{f.l}</small>
            </div>
          ))}
        </section>

        <section className="about-section" aria-labelledby="about-value-title">
          <div className="about-section-title">
            <span>我们相信</span>
            <h2 id="about-value-title">买电视，不该靠猜。</h2>
          </div>
          <div className="about-values">
            {values.map((v, i) => (
              <article className="about-value" key={v.title}>
                <span className="about-value-icon"><Icon name={v.icon} size={17} /></span>
                <div><b>{v.title}</b><p>{v.text}</p></div>
                <em>0{i + 1}</em>
              </article>
            ))}
          </div>
        </section>

        <section className="about-method" aria-labelledby="about-method-title">
          <div className="about-section-title compact">
            <span>数据如何诞生</span>
            <h2 id="about-method-title">从一台真机，到一个可信答案</h2>
          </div>
          <div className="about-steps">
            {['收样', '实测', '校验', '入库'].map((step, i) => (
              <React.Fragment key={step}>
                <div className="about-step"><span>{i + 1}</span><b>{step}</b></div>
                {i < 3 && <i aria-hidden="true"></i>}
              </React.Fragment>
            ))}
          </div>
        </section>

        <section className="about-closing">
          <img src="logo.png" alt="" draggable="false" />
          <p>我们的目标不是替你选一台电视，<br />而是把判断依据交到你手里。</p>
          <b>让每一次选择，都有实测证据。</b>
        </section>
      </div>
    </main>
  );
}

function CategoryCard({ category, openDb, duplicate = false }) {
  const ready = !!category.ready;
  return (
    <button
      className={cx('category-card', `tone-${category.tone}`, ready && 'is-ready')}
      type="button"
      onClick={ready ? openDb : undefined}
      disabled={!ready}
      tabIndex={duplicate ? -1 : undefined}
      aria-label={ready ? `进入${category.name}数据库` : `${category.name}数据库即将上线`}
    >
      <span className="category-card-orb" aria-hidden="true"></span>
      <span className="category-icon"><Icon name={category.icon} size={25} /></span>
      <span className="category-copy"><b>{category.name}</b><small>{category.desc}</small></span>
      <span className={cx('category-meta', ready && 'live')}>{category.meta}</span>
      {ready && <span className="category-arrow"><Icon name="chevRight" size={14} /></span>}
    </button>
  );
}

/* DbScreen（详细机型库）在 dbview.jsx */

function ToolsScreen({ on, openTool }) {
  return (
    <div className={cx('screen', 'tools-screen', !on && 'off')}>
      <div className="hm-head">
        <div className="hm-t">小工具</div>
      </div>

      {LIVE_TOOLS.map((t) => (
        <button key={t.k} className={cx('tool-live', 'tone-' + t.tone)} onClick={(e) => openTool(t.k, e)}>
          <div className="tl-thumb">
            {t.img && <img src={t.img()} alt="" draggable="false" />}
            {!t.img && <span className="tl-art"><Icon name={t.icon} size={31} /></span>}
          </div>
          <div className="tl-body">
            <div className="tl-tt"><b>{t.n}</b></div>
            <span className="tl-chev"><Icon name="chevRight" size={16} /></span>
          </div>
        </button>
      ))}

      <div className="sec-row">
        <span className="sec-t">Agent 统一工具</span>
        <span className="agent-tool-total">{TOOLS_BACK.reduce((n, g) => n + g.tools.length, 0)}</span>
      </div>
      <div className="agent-tool-groups">
        {TOOLS_BACK.map((group) => (
          <section className="agent-tool-group" key={group.t}>
            <div className="atg-head">
              <span className="atg-icon"><Icon name={group.icon} size={16} /></span>
              <b>{group.t}</b>
              <span>{group.tools.length}</span>
            </div>
            <div className="atg-tools">
              {group.tools.map((name) => <span className="atg-tool" key={name}>{name}</span>)}
            </div>
          </section>
        ))}
      </div>
    </div>
  );
}

/* 壳内层：真几何 morph（对标 c0lor-MAG MorphWindow，"一镜到底"）。
   壳从来源卡片 rect 弹簧生长到全屏 —— 动真实几何（left/top/width/height），不是 clip-path 揭露；
   内容按最终尺寸立即挂载、钉在壳左上随壳移动（挂载成本与逐帧重排都不落在动画帧里），~40ms 就开始淡入；
   来源卡片的复制面叠在壳上随壳拉伸、中途溶解 —— t=0 看到的就是卡片本身在长大；阴影独立 bloom。
   关闭走短 tween 而非弹簧（弹簧尾部亚像素 settle 会读作"空了一下才回来"）。
   rect 为 null（深链直开）时从屏幕中央小窗淡入张开。 */
const MORPH_MS = 560, CLOSE_MS = 300;
const MORPH_SPRING = 'linear(0, 0.022, 0.0776, 0.1538, 0.241, 0.332, 0.4219, 0.5073, 0.5862, 0.6572, 0.72, 0.7745, 0.8211, 0.8604, 0.893, 0.9198, 0.9414, 0.9586, 0.9721, 0.9825, 0.9903, 0.996, 1.0001, 1.0029, 1.0047, 1.0057, 1.0062, 1.0063, 1.0061, 1.0057, 1.0051, 1.0046, 1.004, 1.0035, 1.0029, 1.0024, 1.002, 1.0016, 1.0013, 1.001, 1.0008, 1.0006, 1.0005, 1.0003, 1.0002, 1.0002, 1.0001, 1, 1)';
const SMOOTH_EASE = 'cubic-bezier(.32,.72,0,1)';
const geoKey = (g, r) => ({ left: g.left + 'px', top: g.top + 'px', width: g.width + 'px', height: g.height + 'px', borderRadius: r + 'px' });

function MorphLayer({ rect, face, closing, children }) {
  const rootRef = useRef(null), shellRef = useRef(null), shadowRef = useRef(null), faceRef = useRef(null), contentRef = useRef(null);
  const geoRef = useRef(null);

  useLayoutEffect(() => {
    const root = rootRef.current, W = root.offsetWidth, H = root.offsetHeight;
    const from = rect
      ? { left: rect.left, top: rect.top, width: W - rect.left - rect.right, height: H - rect.top - rect.bottom }
      : { left: W * .24, top: H * .42, width: W * .52, height: H * .16 };
    /* 半径恒取卡片自身值、按起点高度截断（CSS 的自动 clamp 不作用于 WAAPI 数值插值） */
    const r0 = Math.min((face && face.r) || 22, from.height / 2);
    geoRef.current = { from, r0, W };
    const content = contentRef.current;
    content.style.width = W + 'px'; content.style.height = H + 'px';
    /* fit 盒定尺寸必须同步、无条件 —— 曾放在 rAF 回调里，存在漏执行路径：
       fit 一旦没有显式宽度就会跟随动画中的容器宽，克隆在中间尺寸被压窄 → CJK 换行卡帧。
       +2px 余量：rect 是分数像素，差 0.x px 就够换行（零余量布局不可靠） */
    if (faceRef.current) {
      const fit0 = faceRef.current.firstElementChild;
      fit0.style.width = Math.ceil(from.width + 1) + 'px';
      fit0.style.height = Math.ceil(from.height + 1) + 'px';
      /* 行内类克隆（胶囊按钮等 fit-content 元素）保持 max-content；块级 / flex 卡片则
         锁回来源元素的真实宽度。button 在脱离 grid 后 width:auto 会 shrink-to-fit，品牌卡
         会从 163px 缩成约 102px，退出末帧因此先贴左再闪回；显式锁宽消除这次重排。 */
      const c0 = fit0.firstElementChild;
      if (c0) {
        if (getComputedStyle(c0).display.indexOf('inline') === 0) fit0.style.minWidth = 'max-content';
        else c0.style.width = from.width + 'px';
      }
    }
    /* 深链没有来源卡片，不做伪造的中心卡片 morph。WAAPI 对 left/top 的 fill 在外层舞台
       滚动后会继续以初始包含块结算，导致工具页整体上移；直接以全屏稳定态挂载。 */
    if (!rect) {
      Object.assign(shellRef.current.style, {
        position: 'relative', inset: 'auto', left: 'auto', top: 'auto', right: 'auto', bottom: 'auto',
        width: '100%', height: '100%', opacity: '1', borderRadius: '0'
      });
      shadowRef.current.style.display = 'none';
      content.style.opacity = '1';
      content.style.transform = 'none';
      return;
    }
    if (REDUCE) {
      Object.assign(shellRef.current.style, { left: '0', top: '0', right: '0', bottom: '0', opacity: '1' });
      content.style.opacity = '1';
      if (faceRef.current) faceRef.current.style.display = 'none';
      return;
    }
    /* 双 rAF：内容挂载与首次布局先落地，动画帧里只剩几何插值 */
    let r2; const r1 = requestAnimationFrame(() => { r2 = requestAnimationFrame(() => {
      const frames = [geoKey(from, r0), geoKey({ left: 0, top: 0, width: W, height: H }, 0)];
      const spring = { duration: MORPH_MS, easing: MORPH_SPRING, fill: 'both' };
      shellRef.current.animate(frames, spring);
      if (!rect) shellRef.current.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, easing: 'ease-out', fill: 'both' });
      shadowRef.current.animate(frames, spring);
      shadowRef.current.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 300, easing: 'ease-out', fill: 'both' });
      const f = faceRef.current;
      if (f) {
        /* 克隆保持原始尺寸排版、scale 跟随面层宽度（尺寸已在 effect 体内同步设好） */
        const fit = f.firstElementChild;
        f.animate(frames, spring);
        fit.animate([{ transform: 'scale(1)' }, { transform: `scale(${W / from.width})` }], spring);
        f.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 240, easing: 'ease-out', fill: 'both' })
          .onfinish = () => { f.style.display = 'none'; };
      }
      /* 内容整页随壳等比缩放（不是左上角裁切）：迷你页面长成全页，"一镜到底" */
      content.animate([{ transform: `scale(${from.width / W})` }, { transform: 'scale(1)' }], spring);
      content.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200, delay: 40, easing: 'ease-out', fill: 'both' });
    }); });
    return () => { cancelAnimationFrame(r1); cancelAnimationFrame(r2); };
  }, []);

  /* 关闭：从当前几何（可能仍在张开途中）tween 收回卡片。轮廓要撑到落地 ——
     内容与阴影几乎全程在场（壳底色≈页面底色，提早撤掉就会中途"融进背景"），
     复制面最后一刻才显形，落点读作"页面收成了那颗按钮"。 */
  useEffect(() => {
    if (!closing || REDUCE) return;
    const { from, r0, W } = geoRef.current;
    const shell = shellRef.current, shadow = shadowRef.current, f = faceRef.current, content = contentRef.current;
    if (!rect) {
      shell.animate([{ opacity: 1 }, { opacity: 0 }], { duration: CLOSE_MS, easing: SMOOTH_EASE, fill: 'both' });
      content.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 180, easing: 'ease-out', fill: 'both' });
      return;
    }
    const s = getComputedStyle(shell);
    const cur = { left: s.left, top: s.top, width: s.width, height: s.height, borderRadius: s.borderRadius };
    const end = geoKey(from, r0);
    const tween = { duration: CLOSE_MS, easing: SMOOTH_EASE, fill: 'both' };
    shell.animate([cur, end], tween);
    /* 壳底色收尾淡出：真按钮是半透明胶囊，壳的不透明底色落在按钮位再消失会读作
       "白胶囊沉底变透明"——最后 120ms 壳溶解，落点只剩克隆贴在页面背景上（=真按钮的样子） */
    if (rect) shell.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 120, delay: CLOSE_MS - 120, easing: 'ease-out', fill: 'both' });
    /* 整页等比缩回卡片宽（从当前 scale 起步，开到一半点关也连续） */
    const tf = getComputedStyle(content).transform;
    const curScale = tf.startsWith('matrix') ? parseFloat(tf.slice(7)) || 1 : 1;
    content.animate([{ transform: `scale(${curScale})` }, { transform: `scale(${from.width / W})` }], tween);
    if (!rect) shell.animate([{ opacity: 1 }, { opacity: 0 }], { duration: CLOSE_MS, easing: 'ease-out', fill: 'both' });
    shadow.animate([cur, end], tween);
    shadow.animate([{ opacity: getComputedStyle(shadow).opacity }, { opacity: 0 }],
      { duration: 220, delay: 80, easing: 'ease-out', fill: 'both' });
    if (f) {
      f.style.display = '';
      f.animate([cur, end], tween);
      const fit = f.firstElementChild;
      /* 兜底：任何原因导致 fit 没定过尺寸，关闭前补上（否则跟随动画中的容器宽 → 克隆换行） */
      if (!fit.style.width) {
        fit.style.width = Math.ceil(from.width + 1) + 'px';
        fit.style.height = Math.ceil(from.height + 1) + 'px';
      }
      const clone = fit.firstElementChild;
      if (clone && getComputedStyle(clone).display.indexOf('inline') !== 0) clone.style.width = from.width + 'px';
      fit.animate([{ transform: `scale(${parseFloat(s.width) / from.width})` }, { transform: 'scale(1)' }], tween);
      f.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 160, delay: 120, easing: 'ease-out', fill: 'both' });
    }
    content.animate([{ opacity: getComputedStyle(content).opacity }, { opacity: 0 }], { duration: 240, easing: 'ease-out', fill: 'both' });
  }, [closing]);

  const base = rect
    ? { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom }
    : { left: '24%', top: '42%', right: '24%', bottom: '42%' };
  return (
    <div className="morph-root" ref={rootRef}>
      <div className="morph-shadow" ref={shadowRef} aria-hidden="true" style={base}></div>
      <div className="morph-shell" ref={shellRef} style={rect ? base : { ...base, opacity: 0 }}>
        <div className="morph-content" ref={contentRef}>{children}</div>
      </div>
      {face && <div className="morph-face" ref={faceRef} aria-hidden="true" style={{ ...base, borderRadius: face.r }}>
        <div className="morph-face-fit" dangerouslySetInnerHTML={{ __html: face.html }}></div>
      </div>}
    </div>
  );
}
/* 点击元素 → MorphLayer 的 rect/face 捕获（相对 host 容器，壳被缩放时按比例还原） */
function morphOrigin(e, hostSel) {
  const el = e && e.currentTarget;
  const host = el && el.closest(hostSel);
  if (!host) return { rect: null, face: null };
  const hr = host.getBoundingClientRect();
  const cr = el.getBoundingClientRect();
  const s = hr.width ? host.offsetWidth / hr.width : 1;
  return {
    rect: { top: (cr.top - hr.top) * s, left: (cr.left - hr.left) * s,
            right: (hr.right - cr.right) * s, bottom: (hr.bottom - cr.bottom) * s },
    face: { html: el.outerHTML, r: parseFloat(getComputedStyle(el).borderRadius) || 22 },
  };
}
Object.assign(window, { MorphLayer, morphOrigin, MORPH_CLOSE_WAIT: CLOSE_MS + 40 }); /* dbview 品牌展开复用 */

function HomeApp({ brandVisible = true }) {
  const rawTool = q('tool');
  const tool0 = rawTool === 'compose' ? 'room' : ['about', 'room', 'light', 'inspect', 'setup', 'db', 'agent'].includes(rawTool) ? rawTool : null;
  const [tab, setTab] = useState(['room', 'light', 'inspect', 'setup'].includes(tool0) || q('tab') === 'tools' ? 'tools' : 'home');
  const [tool, setTool] = useState(tool0);
  const [agentAsk, setAgentAsk] = useState(CMD);
  const [rect, setRect] = useState(null);
  const [face, setFace] = useState(null);
  const [closing, setClosing] = useState(false);
  const [morphKey, setMorphKey] = useState(0); /* 每次打开换 key：全新实例，避免"关闭中重开"复用半状态 */
  const closeTimer = useRef(null);
  const originEl = useRef(null); /* morph 期间隐藏来源卡片（c0lor-MAG 同款）：克隆才是画面上唯一的"它" */
  const syncUrl = (fn) => { const u = new URL(location); fn(u.searchParams); history.replaceState(null, '', u); };
  const pick = (t) => {
    if (t === tab) return;
    syncUrl((p) => { if (t === 'tools') p.set('tab', 'tools'); else p.delete('tab'); });
    setTab(t);
  };
  /* 记录点击卡片相对手机屏的 rect + 卡片复制面，morph 从这里长出 */
  const openTool = (k, e) => {
    clearTimeout(closeTimer.current); /* 关闭动画中重开：撤掉待命的卸载定时器 */
    if (originEl.current) originEl.current.style.visibility = '';
    const o = morphOrigin(e, '.m-app');
    originEl.current = (e && e.currentTarget) || null;
    if (originEl.current) originEl.current.style.visibility = 'hidden';
    setRect(o.rect); setFace(o.face);
    syncUrl((p) => p.set('tool', k)); setClosing(false); setTool(k); setMorphKey((v) => v + 1);
  };
  const closeTool = () => {
    /* 连工具私有深链参数一起清（rp/lp/ip/sp/…，同 mobile.jsx Stage.pick 的清单）：
       只删 tool 的话，下次点卡片重开会把 result 等旧相位状态原样复原。
       'm' 不清 —— 与主页真机模式(?m=1/0)共用一个参数名。 */
    syncUrl((p) => { p.delete('tool'); ['rp', 'sz', 'lp', 'slots', 'pv', 's', 'g', 'ip', 'sp', 'src', 'scene', 'b', 'dbv'].forEach((k) => p.delete(k)); });
    setClosing(true);
    clearTimeout(closeTimer.current);
    closeTimer.current = setTimeout(() => {
      setTool(null); setClosing(false);
      if (originEl.current) { originEl.current.style.visibility = ''; originEl.current = null; }
    }, REDUCE ? 60 : CLOSE_MS + 40);
  };
  const openToolsFromDb = () => {
    syncUrl((p) => {
      p.delete('tool');
      ['b', 'dbv'].forEach((k) => p.delete(k));
      p.set('tab', 'tools');
    });
    setTab('tools');
    setClosing(true);
    clearTimeout(closeTimer.current);
    closeTimer.current = setTimeout(() => {
      setTool(null); setClosing(false);
      if (originEl.current) { originEl.current.style.visibility = ''; originEl.current = null; }
    }, REDUCE ? 60 : CLOSE_MS + 40);
  };
  return (
    <div className="m-app home">
      <div className="hm-bg"><span className="hb b1"></span><span className="hb b2"></span><span className="hb b3"></span></div>
      <HomeScreen on={tab === 'home'} openAbout={(e) => openTool('about', e)} openDb={(e) => openTool('db', e)}
        openAgent={(el, text) => { setAgentAsk(text); openTool('agent', el && { currentTarget: el }); }}
        frozen={!!tool} brandVisible={brandVisible} />
      <ToolsScreen on={tab === 'tools'} openTool={openTool} />
      {tool && (
        <MorphLayer key={morphKey} rect={rect} face={face} closing={closing}>
          {tool === 'about' ? <AboutScreen onBack={closeTool} />
            : tool === 'db' ? <DbScreen onBack={closeTool} onTools={openToolsFromDb} />
            : tool === 'agent' ? <AgentChat onBack={closeTool} ask={agentAsk} />
            : tool === 'light' ? <LightApp onBack={closeTool} />
            : tool === 'inspect' ? <InspectApp onBack={closeTool} />
            : tool === 'setup' ? <SetupApp onBack={closeTool} />
            : <RoomApp onBack={closeTool} />}
        </MorphLayer>
      )}
      <div className="tabbar" style={{ '--tab-active': tab === 'tools' ? 1 : 0 }}>
        <span className="tab-thumb"></span>
        {[['home', '首页', 'database'], ['tools', '小工具', 'grid']].map(([k, l, ic]) => (
          <button key={k} className={tab === k ? 'on' : ''} onClick={() => pick(k)}>
            <Icon name={ic} size={19} />{l}
          </button>
        ))}
      </div>
    </div>
  );
}

function StageHome({ brandVisible = true }) {
  return (
    <div className="stage">
      <div className="stage-head">
        <h1>电视数据库 · 移动端主页</h1>
      </div>
      <div className="frames"><Phone><HomeApp brandVisible={brandVisible} /></Phone></div>
    </div>
  );
}

/* 真机模式：小屏（真手机/窄窗）全屏直跑，不套假手机壳；?m=1 强制真机、?m=0 强制评审舞台 */
const REAL = q('m') === '1' || (q('m') !== '0' && window.matchMedia('(max-width: 620px)').matches);
if (REAL) document.documentElement.classList.add('real');

function RootExperience() {
  /* 工具/详情深链保持直达；主页根路径才播放品牌首页。?intro=0 可直接验收落地页。 */
  const shouldShowIntro = typeof window.EntryIntro === 'function'
    && q('intro') !== '0' && !q('tab') && !q('tool');
  const [introVisible, setIntroVisible] = useState(shouldShowIntro);
  const [revealing, setRevealing] = useState(!shouldShowIntro);
  const [brandVisible, setBrandVisible] = useState(!shouldShowIntro);
  const EntryComponent = window.EntryIntro;

  const completeIntro = () => {
    setIntroVisible(false);
    setRevealing(true);
    window.requestAnimationFrame(() => {
      document.querySelector('.entry-brand-target')?.focus({ preventScroll: true });
    });
  };

  return (
    <>
      <div
        className={cx('entry-app-layer', introVisible && (revealing ? 'is-revealing' : 'is-veiled'))}
        aria-hidden={introVisible ? true : undefined}
      >
        {REAL
          ? <div className="real-shell"><HomeApp brandVisible={brandVisible} /></div>
          : <StageHome brandVisible={brandVisible} />}
      </div>
      {introVisible && EntryComponent && (
        <EntryComponent
          onRevealStart={() => setRevealing(true)}
          onHandoff={() => setBrandVisible(true)}
          onComplete={completeIntro}
        />
      )}
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<RootExperience />);
