/* ============================================================
   ROOY · 05 复盘 · 变体资料室
   ───────────────────────────────────────────────────────────
   主线（canvas · 滑雪学院）：
     05n（六边形宝石 hero，IQ 降级）＝ 收敛案：把 01q Home 的
         HexGemBadge（反射宝石，顶点半径=六维能力值，与 05 雷达同形）直接
         提到 hero，放大 200 居中、复用 01q 原件一笔不改，六个顶点外侧补
         「能力名+数值」标签，从「徽标」升级成「可读形状图」。ROOY 指数 173
         降级为同卡角注（eyebrow + 纯 span 173 黑 + 近 7 天 IQSpark 走势）。
         能力卡行留卡外、可点击展开（CapRowN，展开机制源自已删的 05 CapRow；
         delta 用 data 蓝而非 gold，gold budget=0）。判断行已删。accent 对齐
         01q（gold 0）：rooy 主导（宝石整体 + 173 + 节奏焦点标签），data 蓝降级（cap delta）。

   ── 废案（canvas · 滑雪学院末尾「废案」分区，代码保留备查）────
     05d RadarRose — 圆瓣弧缘：tint 楔形 + 外缘 3px 实色弧。径向读法的
         另一基准型，与 05i 二选一时让位给 05i（蛛网三层读法更明确）。
     05i RadarSpokesWebbed — 圆辐条 + 蛛网：灰 track / 实色粗棒 /
         托底多边形 三层读法。径向读法基准型，现退入废案。
     05o（黑卡 · 宝石空间延伸）＝ 只把 05n 的六边形 hero 白卡翻成 R.ink 黑
         卡，能力明细仍退回 05n 浅底列表（用户：只把六边形做成黑卡）。黑卡不
         再是平黑底——加线条 + 光影，让整张卡读作「宝石 / 六边形的空间延伸」：
         HexField 以宝石为中心辐射 rooy 环境辉光 + 蜂窝晶格（六边形 tessellation，
         径向淡出），卡面再叠左上对角 sheen（同宝石光源 -3π/4，给晶面体积
         感），宝石是这块晶体最亮的一个面。accent 同 05n（gold 0，rooy 唯一主
         色）。HexGemO 沿用 HexGemN 几何，只调黑底着色（辉光更强 / rooy 准星
         框 / 琢面白混范围拉宽）。

   ── 已删除（病因备查）────────────────────────────────────────
     05  原 flat 雷达基础（旧 review.jsx）—— 雷达与 ROOY 指数争 hero、
         主次乱；05n 把六维数据升为宝石 hero 后，flat 基础版无存在必要，
         整个 review.jsx 删除（其 RadarChart/Legend/CapRow 早已被各变体自带
         版本取代，无残留依赖）。
     05p gem 受光卡 + 05o 宝石光影（白卡版）—— 05o 档光影搬上白卡太亮、
         受光与宝石争焦点，与 05n 拉不开足够差距，删。
     05j–m 四个方向均已淘汰（共同教训：六边形当小徽标读不清、当雷达又与
         数字争主次，不如直接升为 hero 形状图）：
       05j 六边形小徽标 + 横向区间条 —— 六边形当小图标「读不清」。
       05k 值驱动徽标 + 堆叠填充条 —— 区间条深浅两段「含义有歧义」。
       05l 段位徽章（边数=等级 + 液位填充）—— 偏游戏化，与复盘语境不符。
       05m flat 数字 hero + 雷达 —— 数字与雷达争 hero，主次仍乱。
   ============================================================ */

const RCAPS = [
  { id: 'balance',  l: '平衡', v: 78, prev: 74, dv: '+4',  hi: false, verdict: '',
    explainer: '滑行中重心稳定的能力。衡量来自不平地形、转弯中、高速的 IMU 偏移。',
    next: '不是当前重点。保持即可。',
    subs: [ ['站位', 82], ['动态偏移', 74], ['恢复率', 77] ] },
  { id: 'pressure', l: '压力', v: 71, prev: 68, dv: '+3',  hi: false, verdict: '',
    explainer: '踏板压载的控制量。高者能让雪板"吃着"雪面，不发飘。',
    next: '随节奏变好会自然提高。',
    subs: [ ['入弯压力', 68], ['出弯压力', 75], ['均匀度', 69] ] },
  { id: 'rotation', l: '旋转', v: 55, prev: 55, dv: '—',   hi: false, verdict: '',
    explainer: '上体转动带动全身的能力。这是短弯、雪沟场景的基础。',
    next: '短弯关卡解锁后会上上去。',
    subs: [ ['上体领转', 52], ['下体跟转', 60], ['轴心稳定', 53] ] },
  { id: 'edge',     l: '立刃', v: 84, prev: 73, dv: '+11', hi: false, verdict: '已稳定 40°',
    explainer: '雪板与雪面的立刃角。越高越能"划"出干净弧线。',
    next: '平台期。接下来不是"再压"，是节奏。',
    subs: [ ['最高立刃角', 41], ['稳定率', 88], ['双腕对称', 80] ] },
  { id: 'tempo',    l: '节奏', v: 62, prev: 61, dv: '+1',  hi: true,  verdict: 'ROOY 下一站',
    explainer: '转弯间隔与身体动作的同步。节奏好 = 状态好。',
    next: '右弯入弯踩脚趾·3 趟连续。',
    subs: [ ['左右对称', 58], ['转换用时', 64], ['心跳同步', 65] ] },
  { id: 'coord',    l: '协调', v: 68, prev: 66, dv: '+2',  hi: false, verdict: '',
    explainer: '上下肢动作设计与眼睛/路线预判的配合。',
    next: '节奏上去后高会随之提升。',
    subs: [ ['眼手同步', 70], ['预判路线', 64], ['动作连缝', 70] ] },
];

const RAD_ANGLE = (i) => -Math.PI / 2 + i * (Math.PI / 3);
const radPt = (cx, cy, i, r) => [
  cx + Math.cos(RAD_ANGLE(i)) * r,
  cy + Math.sin(RAD_ANGLE(i)) * r,
];

/* shared polar geometry — one set of metrics so the variants
   compare purely on rendering style */
const RG = { W: 326, H: 256, cx: 163, cy: 130, rIn: 13, rOut: 98, half: Math.PI * 26 / 180 };
const rOf = (v) => RG.rIn + (RG.rOut - RG.rIn) * (v / 100);
const ppt = (r, a) => `${RG.cx + Math.cos(a) * r} ${RG.cy + Math.sin(a) * r}`;
const sectorPath = (r0, r1, a0, a1) =>
  `M ${ppt(r0, a0)} L ${ppt(r1, a0)} A ${r1} ${r1} 0 0 1 ${ppt(r1, a1)} ` +
  `L ${ppt(r0, a1)} A ${r0} ${r0} 0 0 0 ${ppt(r0, a0)} Z`;
const arcPath = (r, a0, a1) => `M ${ppt(r, a0)} A ${r} ${r} 0 0 1 ${ppt(r, a1)}`;

/* ── shared screen shell (05d/05i) ────────────────────────── */
function RadarShellScreen({ chart, onNav }) {
  const [tab, setTab] = React.useState('review');
  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, position: 'relative', color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>
      <NavBar onBack={() => onNav && onNav('home')} title="滑雪学院"/>
      <div style={{ padding: '0 16px 6px', position: 'relative', zIndex: 1 }}>
        <Segmented value={tab} items={ACADEMY_TABS} onChange={(v) => {
          setTab(v);
          if (v === 'course') onNav && onNav('course');
          if (v === 'medals') onNav && onNav('achievement');
        }}/>
      </div>
      <div style={{ flex: 1, overflowY: 'auto', position: 'relative', zIndex: 1, paddingBottom: 36 }}>
        {chart}
        <div style={{ height: 28 }}/>
        <div style={{ padding: '0 22px', display: 'flex', flexDirection: 'column' }}>
          {RCAPS.map((c, i) => <VCapRow key={c.id} c={c} first={i === 0}/>)}
        </div>
      </div>
    </div>
  );
}

function VLegend({ swatch, label: l }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
      {swatch}
      <span style={{ ...T.footnote, color: R.ink3 }}>{l}</span>
    </div>
  );
}

/* white-card chart wrapper + legend — identical across variants */
function RoseCard({ children, legendPrev }) {
  return (
    <div style={{
      margin: '18px 16px 0', padding: '18px 0 12px',
      background: R.card, borderRadius: B.cardL, boxShadow: Shadow.card,
    }}>
      {children}
      <div style={{ display: 'flex', justifyContent: 'center', gap: 18, marginTop: 10 }}>
        <VLegend swatch={legendPrev} label="上季"/>
        <VLegend swatch={<Dot size={5} color={R.rooy}/>} label="ROOY 焦点"/>
      </div>
    </div>
  );
}

/* labels — inline name + number outside each axis, shared */
function RoseLabels({ caps }) {
  return caps.map((c, i) => {
    const [lx, ly] = radPt(RG.cx, RG.cy, i, RG.rOut + 14);
    let anchor = 'middle';
    if (Math.abs(lx - RG.cx) > 30) anchor = lx > RG.cx ? 'start' : 'end';
    return (
      <text key={i} x={lx} y={ly + 4} textAnchor={anchor}>
        <tspan style={{ ...T.footnote, fontWeight: 600, fill: c.hi ? R.rooy : R.ink }}>
          {c.l}
        </tspan>
        <tspan dx="5" style={{ ...N.reg, fontSize: 14, fontWeight: 700, fill: c.hi ? R.rooy : R.ink }}>
          {c.v}
        </tspan>
      </text>
    );
  });
}

/* 05 CapRow 的收起态复刻 — 展开逻辑留在 05 */
function VCapRow({ c, first }) {
  return (
    <div style={{ borderTop: first ? 'none' : `0.5px solid ${R.hair}` }}>
      <div style={{ padding: '20px 0' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              {c.hi && <Dot size={6} color={R.rooy}/>}
              <span style={{ ...T.headline, color: R.ink }}>{c.l}</span>
              {c.verdict && (
                <span style={{ ...T.footnote, color: c.hi ? R.rooy : R.ink3 }}>{c.verdict}</span>
              )}
            </div>
          </div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, flexShrink: 0 }}>
            <span style={{
              ...N.reg, ...T.footnote, fontWeight: 700,
              color: c.dv.startsWith('+') ? R.gold : R.ink3,
              width: 44, textAlign: 'right',
            }}>
              {c.dv !== '—' ? '↑' : ''} {c.dv}
            </span>
            <div style={{ width: 60, textAlign: 'right' }}>
              <StatNumber value={c.v} unit="%" size={28}/>
            </div>
          </div>
        </div>
        <div style={{ marginTop: 12 }}>
          <Bar pct={c.v} color={c.hi ? R.rooy : R.ink2} height={2.5}/>
        </div>
      </div>
    </div>
  );
}

/* ── 05d · RadarRose — 圆瓣弧缘（基准型）──────────────────
   六片 52° 环形扇区：灰 track 到满量程，数值 = 平涂 tint
   楔形 + 外缘 3px 实色弧（进度读法）。上季 = 楔形内的虚线
   弧刻度。 */
function RadarRose({ caps }) {
  return (
    <svg width={RG.W} height={RG.H} viewBox={`0 0 ${RG.W} ${RG.H}`} style={{ display: 'block', margin: '0 auto' }}>
      {caps.map((c, i) => {
        const ac = RAD_ANGLE(i);
        const a0 = ac - RG.half, a1 = ac + RG.half;
        const rv = rOf(c.v);
        const fill = c.hi
          ? 'color-mix(in srgb, var(--r-rooy) 20%, transparent)'
          : 'color-mix(in srgb, var(--r-data) 20%, transparent)';
        const edge = c.hi ? 'var(--r-rooy)' : 'var(--r-data)';
        return (
          <g key={i}>
            <path d={sectorPath(RG.rIn, RG.rOut, a0, a1)} fill="rgba(14,17,22,0.055)"/>
            <path d={sectorPath(RG.rIn, rv, a0, a1)} fill={fill}/>
            <path d={arcPath(rv, a0, a1)} fill="none"
              stroke={edge} strokeWidth="3" strokeLinecap="round"/>
            {c.prev !== c.v && (
              <path d={arcPath(rOf(c.prev), a0 + 0.06, a1 - 0.06)} fill="none"
                stroke="rgba(14,17,22,0.3)" strokeWidth="1"
                strokeDasharray="2 3"/>
            )}
          </g>
        );
      })}
      <RoseLabels caps={caps}/>
    </svg>
  );
}

/* ── 05i · RadarSpokesWebbed — 圆辐条 + 蛛网 ──────────────
   05b 的圆辐条叠到 05h 的蛛网托底上。读法明确三层：灰 track =
   满量程 0→100；实色粗棒 = 本季值；浅填多边形连"蓝棒尖"（顶点
   = 本季值，含圆帽 w/2 偏移，so 面积正好套住蓝棒、明显外于上季
   虚线）；上季 = 虚线多边形（负空间里显形）+ 棒上 ink 刻痕（顶点
   藏在棒后，靠刻痕补上棒身的精确位；两者同色）。棒身 14，track 0.10。 */
function RadarSpokesWebbed({ caps }) {
  const r0 = 20, w = 14;
  const prevInk = 'rgba(14,17,22,0.35)';   // 上季统一色：虚线多边形 + 棒上刻痕同色
  /* fill vertices reach the round-cap tip (rOf(v)+w/2), not the stroke
     end (rOf(v)) — otherwise the fill sits inside the bar tips and
     collapses onto the prev dashed polygon. */
  const poly = caps.map((c, i) => radPt(RG.cx, RG.cy, i, rOf(c.v) + w / 2).join(',')).join(' ');
  const prevPoly = caps.map((c, i) => radPt(RG.cx, RG.cy, i, rOf(c.prev)).join(',')).join(' ');
  return (
    <svg width={RG.W} height={RG.H} viewBox={`0 0 ${RG.W} ${RG.H}`} style={{ display: 'block', margin: '0 auto' }}>
      {[46, 76, 105].map(r => (
        <circle key={r} cx={RG.cx} cy={RG.cy} r={r}
          fill="none" stroke="rgba(14,17,22,0.05)" strokeWidth="1"/>
      ))}
      <polygon points={poly} fill="color-mix(in srgb, var(--r-data) 8%, transparent)"/>
      <polygon points={prevPoly} fill="none"
        stroke={prevInk} strokeWidth="1" strokeDasharray="3 3"/>
      {caps.map((c, i) => {
        const a = RAD_ANGLE(i);
        const [x0, y0] = radPt(RG.cx, RG.cy, i, r0);
        const [xt, yt] = radPt(RG.cx, RG.cy, i, RG.rOut - 5);
        const [xv, yv] = radPt(RG.cx, RG.cy, i, rOf(c.v));
        const [xp, yp] = radPt(RG.cx, RG.cy, i, rOf(c.prev));
        const px = -Math.sin(a), py = Math.cos(a);
        const bar = c.hi ? 'var(--r-rooy)' : 'var(--r-data)';
        return (
          <g key={i}>
            <line x1={x0} y1={y0} x2={xt} y2={yt}
              stroke="rgba(14,17,22,0.10)" strokeWidth={w} strokeLinecap="round"/>
            <line x1={x0} y1={y0} x2={xv} y2={yv}
              stroke={bar} strokeWidth={w} strokeLinecap="round"/>
            <line
              x1={xp + px * 10} y1={yp + py * 10}
              x2={xp - px * 10} y2={yp - py * 10}
              stroke={prevInk} strokeWidth="1.2"/>
          </g>
        );
      })}
      <RoseLabels caps={caps}/>
    </svg>
  );
}

/* ── screens (05d / 05i) ──────────────────────────────────── */
function ReviewScreenD({ onNav }) {
  return (
    <RadarShellScreen onNav={onNav} chart={
      <RoseCard legendPrev={<span style={{ width: 12, height: 0, borderTop: `1.5px dashed ${R.ink4}` }}/>}>
        <RadarRose caps={RCAPS}/>
      </RoseCard>
    }/>
  );
}

function ReviewScreenI({ onNav }) {
  return (
    <RadarShellScreen onNav={onNav} chart={
      <RoseCard legendPrev={<span style={{ width: 12, height: 0, borderTop: `1.5px dashed ${R.ink4}` }}/>}>
        <RadarSpokesWebbed caps={RCAPS}/>
      </RoseCard>
    }/>
  );
}

/* CapRowN — 05n 能力卡行，可点击展开（复用 05 基础 CapRow 的展开机制：
   ROOY 下一步 + 解释段 + 详细数据 sub-bars +「和 ROOY 试一试」CTA）。
   收起态：能力名 + verdict / delta + 值 + 细条 + trailing chevron（展开时
   旋转 90°，暗示可点）；行间 hairline 分隔、padding 20（与 05 一致）。
   与 05 的差别：① delta 用 data 蓝而非 gold（守 05n gold-0 budget）
   ② trailing chevron（05 收起态无暗示，05n 补展开信号）。 */
function CapRowN({ c, first, expanded, onClick, onTry }) {
  return (
    <div style={{ borderTop: first ? 'none' : `0.5px solid ${R.hair}` }}>
      <div onClick={onClick} style={{ padding: '20px 0', cursor: 'pointer' }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              {c.hi && <Dot size={6} color={R.rooy}/>}
              <span style={{ ...T.headline, color: R.ink }}>{c.l}</span>
              {c.verdict && (
                <span style={{ ...T.footnote, color: c.hi ? R.rooy : R.ink3 }}>{c.verdict}</span>
              )}
            </div>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexShrink: 0 }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
              <span style={{
                ...N.reg, ...T.footnote, fontWeight: 700,
                color: c.dv.startsWith('+') ? R.data : R.ink3,
                width: 44, textAlign: 'right',
              }}>
                {c.dv !== '—' ? '↑' : ''} {c.dv}
              </span>
              <div style={{ width: 60, textAlign: 'right' }}>
                <StatNumber value={c.v} unit="%" size={28}/>
              </div>
            </div>
            <svg width="8" height="13" viewBox="0 0 8 13" fill="none" style={{
              flexShrink: 0, color: R.ink4,
              transform: expanded ? 'rotate(90deg)' : 'none',
              transition: 'transform 200ms ease',
            }}>
              <path d="M2 2 L6 6.5 L2 11" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </div>
        </div>
        <div style={{ marginTop: 12 }}>
          <Bar pct={c.v} color={c.hi ? R.rooy : R.ink2} height={2.5}/>
        </div>
      </div>

      {expanded && (
        <div style={{ marginBottom: 14, padding: '18px 18px 16px', background: R.card, borderRadius: B.cardL }}>
          {/* ROOY 下一步 — kicker + 结论句 */}
          <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginBottom: 10 }}>
            <Dot size={5} color={R.rooy}/>
            <span style={{ ...T.eyebrow, color: R.rooy }}>ROOY · 下一步</span>
          </div>
          <p style={{ ...T.headline, color: R.ink, lineHeight: 1.5 }}>{c.next}</p>

          {/* 解释段 */}
          <p style={{ ...T.para, color: R.ink2, marginTop: 12 }}>{c.explainer}</p>

          {/* 详细数据 — 子参数 sub-bars（高度 3 与上面的 2.5 拉开层级） */}
          <div style={{ marginTop: 18, paddingTop: 16, borderTop: `0.5px solid ${R.hair}` }}>
            <p style={{ ...T.eyebrow, color: R.ink3, marginBottom: 12 }}>详细数据</p>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
              {c.subs.map((s, i) => (
                <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                  <span style={{ ...T.footnote, color: R.ink3, width: 80, flexShrink: 0 }}>{s[0]}</span>
                  <div style={{ flex: 1 }}>
                    <Bar pct={s[1]} color={R.ink2} height={3}/>
                  </div>
                  <span style={{ ...N.reg, ...T.footnote, color: R.ink, fontWeight: 700, width: 26, textAlign: 'right' }}>{s[1]}</span>
                </div>
              ))}
            </div>
          </div>

          {/* CTA */}
          <div style={{ marginTop: 18, display: 'flex', justifyContent: 'flex-end' }}>
            <InlineCTA onClick={onTry}>和 ROOY 试一试</InlineCTA>
          </div>
        </div>
      )}
    </div>
  );
}

/* ============================================================
   05n · 六边形 hero（01q 宝石 · 浅 rooy 轮廓框 · 上部分白卡）
   ============================================================
   05n 翻转之前方向的主次——把 01q Home 的反射宝石提到 hero 位（200）。
   宝石本体（rooy 辉光 + 6 琢面 + 双层高光、顶点按数据 min/max 归一化）
   沿用 01q HexGemBadge 语法，另起 HexGemN（不动 01q 原件）：「背景轮廓
   线」（外圈 rim 深一档 60% 读作满量程边界 + 6 辐条/内刻度环 浅 35%）、
   统一 0.6 粗细，在数据形内凹处露一截。
   六个顶点外补「能力名+数值」标签（节奏=rooy 焦点）；平衡/立刃（顶/底顶
   点）标签半径收小、更贴近宝石（用户：更紧凑），wrap 改非方形（横向让侧
   标签、纵向收紧顶/底）。

   整个上部分白卡（用户：整个上部分内容做成一个白卡）IQHeroCardN：卡内 =
   六边形宝石 hero + hairline + ROOY 指数 行。ROOY 行两栏（中间 hairline，
   同 01q InsetGroup 两列语法）：左=ROOY 指数 eyebrow + 173（纯 span、不
   走 StatNumber、IQ Oswald Reg 400 正体、黑色；↑6·本季 文案已删），右=
   提升趋势历史记录（近 7 天滑行 IQSpark：rooy 折线+逐点小圆，末点加粗=
   本季，走势即 ↑6）。形状 + 分数 + 走势 归成一块「本期总结」，能力卡
   行留卡外、可点击展开（CapRowN，复用 05；行间 hairline、卡↔列表无分隔）。判断行已删（用户）。
   accent 对齐 01q（gold 0）：rooy 主导（宝石+sparkline+节奏标签），
   173 用黑、data 蓝降级（cap delta）。
   ============================================================ */

/* HexGemN — 05n hero 宝石。宝石本体（rooy 辉光底盘 + 外层 halo 氛围 + 6 琢面（按数据
   min/max 归一化）+ 双层高光）一字不改地沿用 01q HexGemBadge 语法；满量
   程骨架（外圈 rim + 6 辐条 + 内刻度环）的「背景轮廓线」外圈 rim 60%、
   辐条 + 内刻度环 35%（皆比 rooy 浅）、统一 0.6 粗细，在数据
   形内凹的负空间里露出一截、读作量纲刻度。宝石仍是宝石（辉光/琢面/高
   光/归一化不动）。01q 的 HexGemBadge 不动（那里是单圈 rim 的纯 rooy
   徽标）。 */
function HexGemN({ caps, size = 200 }) {
  const uid = React.useId().replace(/[:]/g, '');
  const cx = 50, cy = 50;
  const rMin = 20, rMax = 44, rOut = 47;
  const ang = (i) => -Math.PI / 2 + i * (Math.PI / 3);
  const polyAt = (r) => Array.from({ length: 6 }, (_, i) =>
    [cx + Math.cos(ang(i)) * r, cy + Math.sin(ang(i)) * r].map(n => n.toFixed(2)).join(',')).join(' ');
  const vs = caps.map(c => c.v);
  const vmin = Math.min(...vs), vmax = Math.max(...vs);
  const rv = (v) => rMin + (rMax - rMin) * ((v - vmin) / (vmax - vmin));
  const vp = (i) => [cx + Math.cos(ang(i)) * rv(caps[i].v), cy + Math.sin(ang(i)) * rv(caps[i].v)];
  const pts = caps.map((_, i) => vp(i).map(n => n.toFixed(2)).join(',')).join(' ');
  const frame = 'color-mix(in srgb, var(--r-rooy) 35%, white)';     // 辐条 + 内刻度环：比 rooy 浅（35% rooy + 65% 白）
  const frameRim = 'color-mix(in srgb, var(--r-rooy) 60%, white)';  // 外圈 rim：深一档（60% rooy + 40% 白）—读作满量程边界

  const lightAng = -3 * Math.PI / 4;
  const lx = Math.cos(lightAng), ly = Math.sin(lightAng);
  const facetBright = (i) => Math.max(0,
    Math.cos(ang(i) + Math.PI / 6) * lx + Math.sin(ang(i) + Math.PI / 6) * ly);
  const mixFor = (i) => {                                   // 05o 光影（白卡版）：38..92
    const vf = (caps[i].v - vmin) / (vmax - vmin);
    const b = facetBright(i) * 0.68 + vf * 0.32;
    return Math.round(92 - b * 54);
  };

  return (
    <svg width={size} height={size} viewBox="0 0 100 100" style={{ flexShrink: 0, display: 'block', overflow: 'visible' }}>
      <defs>
        <radialGradient id={`haloN${uid}`} cx="50" cy="50" r="82" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="var(--r-rooy)" stopOpacity="0.05"/>
          <stop offset="55%"  stopColor="var(--r-rooy)" stopOpacity="0.02"/>
          <stop offset="100%" stopColor="var(--r-rooy)" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id={`glowN${uid}`} cx="50" cy="50" r="54" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="var(--r-rooy)" stopOpacity="0.22"/>
          <stop offset="55%"  stopColor="var(--r-rooy)" stopOpacity="0.07"/>
          <stop offset="100%" stopColor="var(--r-rooy)" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id={`gmsN${uid}`} cx="36" cy="30" r="40" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="#fff" stopOpacity="0.27"/>
          <stop offset="55%"  stopColor="#fff" stopOpacity="0.05"/>
          <stop offset="100%" stopColor="#fff" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id={`spkN${uid}`} cx="40" cy="27" r="13" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="#fff" stopOpacity="0.50"/>
          <stop offset="100%" stopColor="#fff" stopOpacity="0"/>
        </radialGradient>
        <clipPath id={`cmcN${uid}`}><polygon points={pts}/></clipPath>
      </defs>

      {/* ⓪ rooy 氛围：外层宽柔 halo（溢出宝石形成环境光）+ 内层辉光底盘（浮起感） */}
      <circle cx={cx} cy={cy} r="82" fill={`url(#haloN${uid})`}/>
      <circle cx={cx} cy={cy} r="54" fill={`url(#glowN${uid})`}/>

      {/* ① 背景轮廓：外圈 rim 深一档（60%）读作满量程边界，6 辐条 + 内刻度环浅（35%）；统一粗细 0.6 */}
      <polygon points={polyAt(rOut)} fill="none" stroke={frameRim} strokeWidth="0.6"/>
      <polygon points={polyAt(rOut * 0.66)} fill="none" stroke={frame} strokeWidth="0.6"/>
      {caps.map((_, i) => {
        const a = ang(i);
        return <line key={`ax${i}`} x1={cx} y1={cy}
          x2={cx + Math.cos(a) * rOut} y2={cy + Math.sin(a) * rOut}
          stroke={frame} strokeWidth="0.6"/>;
      })}

      {/* ② 6 琢面（光照 + 能力值深浅） */}
      {caps.map((_, i) => {
        const j = (i + 1) % 6;
        return (
          <polygon key={i}
            points={`${cx},${cy} ${vp(i).join(',')} ${vp(j).join(',')}`}
            fill={`color-mix(in srgb, var(--r-rooy) ${mixFor(i)}%, white)`}/>
        );
      })}

      {/* ③ 两层高光（柔光 + 锐 sparkle），clip 在剪影内 */}
      <g clipPath={`url(#cmcN${uid})`}>
        <rect x="0" y="0" width="100" height="100" fill={`url(#gmsN${uid})`}/>
        <rect x="0" y="0" width="100" height="100" fill={`url(#spkN${uid})`}/>
      </g>
    </svg>
  );
}

/* HexGemHero — HexGemN（rooy 刻度框宝石）放大居中作 hero，六个顶点外侧补
   「能力名 + 数值」标签（焦点=节奏 用 rooy，其余 ink）。标签位用同一套
   pointy-top 极坐标（ang(i) = -π/2 + i·π/3）算出、HTML 绝对定位叠在外圈。
   hero 整个住进 IQHeroCardN 白卡（与 ROOY 指数 同卡），margin 留卡内呼吸。 */
function HexGemHero({ caps, size = 200 }) {
  const padX = 46, padY = 32;        // 非方形 wrap：横向让侧标签、纵向收紧顶/底
  const wrapW = size + padX * 2, wrapH = size + padY * 2;
  const cX = wrapW / 2, cY = wrapH / 2;
  const frameR = size * 0.47;        // 外缘六边形框半径（同 HexGemN rOut=47%）
  const rTopBot = frameR + 18;       // 平衡/立刃（顶/底顶点）更贴近宝石（用户：更紧凑）
  const rSide = frameR + 30;         // 侧标签保持
  const ang = (i) => -Math.PI / 2 + i * (Math.PI / 3);
  return (
    <div style={{ position: 'relative', width: wrapW, height: wrapH, margin: '8px auto 4px' }}>
      <div style={{ position: 'absolute', left: cX - size / 2, top: cY - size / 2 }}>
        <HexGemN caps={caps} size={size}/>
      </div>
      {caps.map((cap, i) => {
        const a = ang(i);
        const r = (i === 0 || i === 3) ? rTopBot : rSide;
        const x = cX + Math.cos(a) * r;
        const y = cY + Math.sin(a) * r;
        const focus = cap.hi;
        const isTopBot = (i === 0 || i === 3);
        return (
          <div key={cap.id} style={{
            position: 'absolute', left: x, top: y,
            transform: 'translate(-50%, -50%)',
            display: 'flex',
            flexDirection: isTopBot ? 'row' : 'column',
            alignItems: isTopBot ? 'baseline' : 'center',
            gap: isTopBot ? 5 : 2,
            whiteSpace: isTopBot ? 'nowrap' : 'normal',
            lineHeight: 1.15,
          }}>
            <span style={{ ...T.footnote, fontWeight: 600, color: focus ? R.rooy : R.ink }}>{cap.l}</span>
            <span style={{ ...N.reg, fontSize: 15, fontWeight: 700, color: focus ? R.rooy : R.ink, lineHeight: 1 }}>{cap.v}</span>
          </div>
        );
      })}
    </div>
  );
}

/* IQ_HISTORY — ROOY 指数 近 7 天滑行走势（末点=本季 173，倒数第二 167 →
   本季 ↑6）。卡片右侧「提升趋势历史记录」sparkline 的数据源。 */
const IQ_HISTORY = [150, 155, 159, 162, 164, 167, 173];

/* IQSpark — ROOY 指数 提升趋势历史记录：rooy 折线 + 浅面积 + 逐点小圆
   （离散的「历史记录」，不是平滑曲线），末点加粗=当前季。width:100% 撑
   满卡片右半。 */
function IQSpark({ data = IQ_HISTORY, w = 150, h = 46 }) {
  const uid = React.useId().replace(/[:]/g, '');
  const min = Math.min(...data), max = Math.max(...data);
  const padX = 4, padY = 5;
  const x = (i) => padX + (w - 2 * padX) * (i / (data.length - 1));
  const y = (v) => padY + (h - 2 * padY) * (1 - (v - min) / (max - min || 1));
  const line = data.map((v, i) => `${i ? 'L' : 'M'} ${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(' ');
  const area = `${line} L ${x(data.length - 1).toFixed(1)} ${(h - padY).toFixed(1)} L ${x(0).toFixed(1)} ${(h - padY).toFixed(1)} Z`;
  return (
    <svg viewBox={`0 0 ${w} ${h}`} preserveAspectRatio="none"
      style={{ display: 'block', width: '100%', height: h, overflow: 'visible' }}>
      <defs>
        <linearGradient id={`iqg${uid}`} x1="0" y1="0" x2="0" y2="1">
          <stop offset="0%" stopColor="var(--r-rooy)" stopOpacity="0.18"/>
          <stop offset="100%" stopColor="var(--r-rooy)" stopOpacity="0"/>
        </linearGradient>
      </defs>
      <path d={area} fill={`url(#iqg${uid})`}/>
      <path d={line} fill="none" stroke="var(--r-rooy)" strokeWidth="1.6"
        strokeLinejoin="round" strokeLinecap="round" vectorEffect="non-scaling-stroke"/>
      {data.map((v, i) => (
        <circle key={i} cx={x(i).toFixed(1)} cy={y(v).toFixed(1)}
          r={i === data.length - 1 ? 2.6 : 1.6} fill="var(--r-rooy)"/>
      ))}
    </svg>
  );
}

/* IQHeroCardN — 整个「上部分」白卡（用户：整个上部分内容做成一个白卡）：
   卡内 = 六边形宝石 hero（HexGemHero）+ hairline 分隔 + ROOY 指数 行。
   ROOY 行两栏（中间 hairline，同 01q InsetGroup 两列语法）：左 = ROOY
   指数 eyebrow + 173（IQ Oswald Reg 400 正体、不走 StatNumber、
   黑色），右 = 提升趋势历史记录（近 7 天滑行 caption 13px +
   IQSpark）。hero 与 ROOY 指数 同卡——形状 + 分数 + 走势 归成一块「本期
   总结」，cap 行留卡外做明细。 */
function IQHeroCardN() {
  return (
    <div style={{
      margin: '8px 16px 0', background: R.card, borderRadius: B.cardL, boxShadow: Shadow.card,
    }}>
      <HexGemHero caps={RCAPS}/>
      <div style={{ height: 0.5, background: R.hair, margin: '2px 16px 0' }}/>
      <div style={{ display: 'flex', alignItems: 'center' }}>
        <div style={{ flex: 1, minWidth: 0, padding: '14px 16px' }}>
          <p style={{ ...T.eyebrow, color: R.rooy, marginBottom: 4 }}>ROOY 指数</p>
          <span style={{ fontFamily: '"IQ Oswald Reg","SF Pro Display",sans-serif', fontWeight: 400, fontStyle: 'normal', letterSpacing: '0', fontSize: 46, color: R.ink, lineHeight: 0.95 }}>173</span>
        </div>
        <div style={{ width: 1, background: R.hair, alignSelf: 'stretch', margin: '12px 0' }}/>
        <div style={{ flex: 1, minWidth: 0, padding: '14px 16px' }}>
          <p style={{ ...T.footnote, color: R.ink3, marginBottom: 6 }}>近 7 天滑行</p>
          <IQSpark/>
        </div>
      </div>
    </div>
  );
}

/* ReviewScreenN（05n）— 整个上部分白卡 IQHeroCardN（六边形宝石 hero + ROOY
   指数 173 + 近 7 季走势 同卡）→ 能力卡行（可点击展开）。顶部不再放 ROOY
   角注，宝石与 ROOY 指数 一起住进白卡；判断行已删。cap 行用 CapRowN（复用
   05 CapRow 展开；行间 hairline，卡↔列表无分隔）。 */
function ReviewScreenN({ onNav }) {
  const [tab, setTab] = React.useState('review');
  const [expanded, setExpanded] = React.useState(null);
  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, position: 'relative', color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>
      <NavBar onBack={() => onNav && onNav('home')} title="滑雪学院"/>
      <div style={{ padding: '0 16px 6px', position: 'relative', zIndex: 1 }}>
        <Segmented value={tab} items={ACADEMY_TABS} onChange={(v) => {
          setTab(v);
          if (v === 'course') onNav && onNav('course');
          if (v === 'medals') onNav && onNav('achievement');
        }}/>
      </div>
      <div style={{ flex: 1, overflowY: 'auto', position: 'relative', zIndex: 1, paddingBottom: 36 }}>
        <IQHeroCardN/>
        <div style={{ marginTop: 18, padding: '0 22px' }}>
          {RCAPS.map((c, i) => (
            <CapRowN key={c.id} c={c} first={i === 0}
              expanded={expanded === c.id}
              onClick={() => setExpanded(expanded === c.id ? null : c.id)}
              onTry={() => {}}
            />
          ))}
        </div>

        {/* ROOY Pro 入口 — 复盘底部，对齐设置页最新设计（rooy 描边 +
            PRO 字标 + 贴边光晕），文案适配复盘语境。 */}
        <div style={{ marginTop: 24, padding: '0 16px' }}>
          <button onClick={() => onNav && onNav('rooy-pro')} style={{
            width: '100%', textAlign: 'left',
            background: R.card, border: `1.5px solid ${R.rooy}`,
            borderRadius: B.cardL, padding: '14px 16px', cursor: 'pointer',
            display: 'flex', alignItems: 'center', gap: 14,
            boxShadow: '0 0 14px ' + R.rooyTint12,
          }}>
            <span style={{
              flexShrink: 0,
              fontFamily: '"SF Pro Display", -apple-system, system-ui, sans-serif',
              fontWeight: 800, fontSize: 22,
              letterSpacing: '0.03em', color: R.rooy, lineHeight: 1,
            }}>PRO</span>
            <div style={{ flex: 1, minWidth: 0 }}>
              <p style={{ ...T.body, color: R.ink, fontWeight: 600 }}>解锁深度复盘</p>
              <p style={{ ...T.footnote, color: R.ink3, marginTop: 3 }}>
                完整能力雷达 · 历史趋势 · 更细分析
              </p>
            </div>
            <Chevron/>
          </button>
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   05o · 黑卡变体（科技 / 运动感）— 05n 的反相：白卡 → 黑卡
   ============================================================
   05n 是「近白底 + 单张白卡 hero」；05o 把同一份内容（六边形宝石
   hero + ROOY 指数 + 六维明细）整体翻到 R.ink 黑卡上，靠「近黑底 +
   单一 rooy 辉光 + 高对比白字」做运动手表 / 仪表盘的科技运动感。
   bg 仍是 R.bg 近白——黑卡在白底上 pop，保留 iOS app 身份，不做成
   全屏暗色 fitness dashboard（那是 Carv 类 anti-reference）。

   两张黑卡并排叠放（用户：白卡换成黑卡）：
     ① IQCoreCardO — 宝石 hero + ROOY 指数（05n IQHeroCardN 的反相）
     ② CapConsoleO — 6 条能力明细行（05n 卡外列表的反相，收进黑卡）

   accent 与 05n 同源（gold 0）：rooy 是唯一发光主色（宝石辉光 +
   sparkline + 节奏焦点 + ROOY 下一步 + CTA 反相白丸）。delta 在黑卡
   上降级为中性白字（inkD2 + ↑），不再用 data 蓝——黑底单色读数更像
   遥测，也避免 data 蓝在近黑底上发闷。
   ============================================================ */

/* HexGemO — 05o 黑卡上的宝石。几何（按 min/max 归一化的 6 顶点 + 双层
   高光）与 HexGemN 一字一致「宝石仍是宝石」；只把着色调成黑底版：
     · 辉光更强（黑底上的发光体，0.34 起跳）
     · 背景轮廓框改 rooy 24% 透明（读作 HUD 准星 / 量纲刻度，不再是
       白底版的 rooy+白淡框）
     · 琢面白混范围拉宽（28..82 → 深青到亮青，切割感更强）
   HexGemN 不动（白底版）。 */
function HexGemO({ caps, size = 200 }) {
  const uid = React.useId().replace(/[:]/g, '');
  const cx = 50, cy = 50;
  const rMin = 20, rMax = 44, rOut = 47;
  const ang = (i) => -Math.PI / 2 + i * (Math.PI / 3);
  const polyAt = (r) => Array.from({ length: 6 }, (_, i) =>
    [cx + Math.cos(ang(i)) * r, cy + Math.sin(ang(i)) * r].map(n => n.toFixed(2)).join(',')).join(' ');
  const vs = caps.map(c => c.v);
  const vmin = Math.min(...vs), vmax = Math.max(...vs);
  const rv = (v) => rMin + (rMax - rMin) * ((v - vmin) / (vmax - vmin));
  const vp = (i) => [cx + Math.cos(ang(i)) * rv(caps[i].v), cy + Math.sin(ang(i)) * rv(caps[i].v)];
  const pts = caps.map((_, i) => vp(i).map(n => n.toFixed(2)).join(',')).join(' ');
  const frame = 'color-mix(in srgb, var(--r-rooy) 24%, transparent)';  // 黑底轮廓框：rooy 24%（准星 / 刻度感）

  const lightAng = -3 * Math.PI / 4;
  const lx = Math.cos(lightAng), ly = Math.sin(lightAng);
  const facetBright = (i) => Math.max(0,
    Math.cos(ang(i) + Math.PI / 6) * lx + Math.sin(ang(i) + Math.PI / 6) * ly);
  const mixFor = (i) => {                                   // 黑底琢面白混范围拉宽（18..90 → 深青到近白青）
    const vf = (caps[i].v - vmin) / (vmax - vmin);
    const b = facetBright(i) * 0.68 + vf * 0.32;
    return Math.round(90 - b * 72);
  };

  return (
    <svg width={size} height={size} viewBox="0 0 100 100" style={{ flexShrink: 0, display: 'block', overflow: 'visible' }}>
      <defs>
        <radialGradient id={`glowO${uid}`} cx="50" cy="50" r="54" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="var(--r-rooy)" stopOpacity="0.38"/>
          <stop offset="55%"  stopColor="var(--r-rooy)" stopOpacity="0.13"/>
          <stop offset="100%" stopColor="var(--r-rooy)" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id={`gmsO${uid}`} cx="36" cy="30" r="40" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="#fff" stopOpacity="0.34"/>
          <stop offset="55%"  stopColor="#fff" stopOpacity="0.06"/>
          <stop offset="100%" stopColor="#fff" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id={`spkO${uid}`} cx="40" cy="27" r="13" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="#fff" stopOpacity="0.60"/>
          <stop offset="100%" stopColor="#fff" stopOpacity="0"/>
        </radialGradient>
        <clipPath id={`cmcO${uid}`}><polygon points={pts}/></clipPath>
      </defs>

      {/* ⓪ rooy 辉光底盘 — 黑底上的发光体 */}
      <circle cx={cx} cy={cy} r="54" fill={`url(#glowO${uid})`}/>

      {/* ① rooy 准星框：外圈 rim + 内圈 + 6 辐条，统一 0.6 / rooy 24% */}
      <polygon points={polyAt(rOut)} fill="none" stroke={frame} strokeWidth="0.6"/>
      <polygon points={polyAt(rOut * 0.66)} fill="none" stroke={frame} strokeWidth="0.6"/>
      {caps.map((_, i) => {
        const a = ang(i);
        return <line key={`ax${i}`} x1={cx} y1={cy}
          x2={cx + Math.cos(a) * rOut} y2={cy + Math.sin(a) * rOut}
          stroke={frame} strokeWidth="0.6"/>;
      })}

      {/* ② 6 琢面（光照 + 能力值深浅，白混范围拉宽） */}
      {caps.map((_, i) => {
        const j = (i + 1) % 6;
        return (
          <polygon key={i}
            points={`${cx},${cy} ${vp(i).join(',')} ${vp(j).join(',')}`}
            fill={`color-mix(in srgb, var(--r-rooy) ${mixFor(i)}%, white)`}/>
        );
      })}

      {/* ③ 两层高光（柔光 + 锐 sparkle），clip 在剪影内 */}
      <g clipPath={`url(#cmcO${uid})`}>
        <rect x="0" y="0" width="100" height="100" fill={`url(#gmsO${uid})`}/>
        <rect x="0" y="0" width="100" height="100" fill={`url(#spkO${uid})`}/>
      </g>
    </svg>
  );
}

/* HexGemHeroO — HexGemO（黑底发光宝石）放大居中作 hero，六个顶点外侧
   补「能力名 + 数值」标签。定位数学与 HexGemHero 一致；标签颜色翻到
   暗底版：焦点（节奏）= rooy 发光，其余 = inkD 白。hero 住进 IQCoreCardO
   黑卡。 */
function HexGemHeroO({ caps, size = 200 }) {
  const padX = 46, padY = 32;
  const wrapW = size + padX * 2, wrapH = size + padY * 2;
  const cX = wrapW / 2, cY = wrapH / 2;
  const frameR = size * 0.47;
  const rTopBot = frameR + 18;
  const rSide = frameR + 30;
  const ang = (i) => -Math.PI / 2 + i * (Math.PI / 3);
  return (
    <div style={{ position: 'relative', width: wrapW, height: wrapH, margin: '8px auto 4px' }}>
      <div style={{ position: 'absolute', left: cX - size / 2, top: cY - size / 2 }}>
        <HexGemO caps={caps} size={size}/>
      </div>
      {caps.map((cap, i) => {
        const a = ang(i);
        const r = (i === 0 || i === 3) ? rTopBot : rSide;
        const x = cX + Math.cos(a) * r;
        const y = cY + Math.sin(a) * r;
        const focus = cap.hi;
        return (
          <div key={cap.id} style={{
            position: 'absolute', left: x, top: y,
            transform: 'translate(-50%, -50%)',
            display: 'flex', alignItems: 'baseline', gap: 5,
            whiteSpace: 'nowrap',
          }}>
            <span style={{ ...T.footnote, fontWeight: 600, color: focus ? R.rooy : R.inkD }}>{cap.l}</span>
            <span style={{ ...N.reg, fontSize: 15, fontWeight: 700, color: focus ? R.rooy : R.inkD }}>{cap.v}</span>
          </div>
        );
      })}
    </div>
  );
}

/* HexField — 黑卡①的「六边形空间场」。以宝石为中心（top 对齐宝石中心
   140px ＝ 卡顶 margin8 + wrap padY32 + 半径100），把宝石的六边形几何 + 光
   延伸到整张卡，让黑卡读作「宝石是这块晶体最亮的一个面」而非平黑底：
     · rooy 环境辉光（宝石照亮卡内空间）
     · 6 扇形琢面 wash（与宝石同源光源 -3π/4，卡面 = 被照亮的 6 瓣晶面）
     · 蜂窝晶格（六边形 tessellation，径向 mask 由内向外淡出）
   有晶格承担几何纹理，就不再叠全屏轮廓线（同心环 / 辐条已删）。晶格线条
   用预混实色 teal（rooy 往 #0E1116 混），不用 stroke-opacity——低透明度 rooy
   在黑底上发灰发糊，实色利落。光影层（辉光 / 琢面 wash / 卡面 sheen）才用
   透明度 / 渐变。宝石仍是唯一焦点。 */
function HexField({ size = 600 }) {
  const uid = React.useId().replace(/[:]/g, '');
  const cx = size / 2, cy = size / 2;
  const ang = (i) => -Math.PI / 2 + i * (Math.PI / 3);
  // 预混实色 teal（rooy 往 #0E1116 混）—— 晶格线条专用，不用透明度
  const inkHex = '#0E1116';
  const tealLat = `color-mix(in srgb, var(--r-rooy) 34%, ${inkHex})`;
  // 6 扇形琢面 wash：与宝石同源光源（-3π/4 左上），每个扇区按受光亮度给
  // 一层淡白 wash，卡面读作「被宝石同一束光照亮的 6 瓣晶面」。
  const lightAng = -3 * Math.PI / 4;
  const lx = Math.cos(lightAng), ly = Math.sin(lightAng);
  const facetBright = (i) => Math.max(0,
    Math.cos(ang(i) + Math.PI / 6) * lx + Math.sin(ang(i) + Math.PI / 6) * ly);
  const wedgeR = 292;
  // 蜂窝晶格 —— pointy-top 六边形 tessellation，圆心对齐到宝石中心
  const hr = 30;                                       // 单格半径
  const hw = Math.sqrt(3) * hr;                        // 同行圆心横距
  const vstep = 1.5 * hr;                              // 行间纵距
  const hexPts = (x, y) => Array.from({ length: 6 }, (_, k) => {
    const a = -Math.PI / 2 + k * (Math.PI / 3);
    return `${(x + Math.cos(a) * hr).toFixed(1)},${(y + Math.sin(a) * hr).toFixed(1)}`;
  }).join(' ');
  const cells = [];
  for (let row = -1; row * vstep < size + hr; row++) {
    const y = row * vstep + (cy % vstep);
    const xoff = (row % 2 === 0) ? 0 : hw / 2;
    for (let col = -1; col * hw < size + hw; col++) {
      const x = col * hw + xoff + (cx % hw);
      cells.push(<polygon key={`c${row}-${col}`} points={hexPts(x, y)} fill="none" stroke={tealLat} strokeWidth="0.8"/>);
    }
  }
  return (
    <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}
      style={{ position: 'absolute', left: '50%', top: 140, transform: 'translate(-50%, -50%)',
        pointerEvents: 'none', overflow: 'visible' }}>
      <defs>
        <radialGradient id={`hfGlow${uid}`} cx={cx} cy={cy} r="280" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="var(--r-rooy)" stopOpacity="0.26"/>
          <stop offset="55%"  stopColor="var(--r-rooy)" stopOpacity="0.07"/>
          <stop offset="100%" stopColor="var(--r-rooy)" stopOpacity="0"/>
        </radialGradient>
        <radialGradient id={`hfMaskG${uid}`} cx={cx} cy={cy} r="300" gradientUnits="userSpaceOnUse">
          <stop offset="0%"   stopColor="#fff" stopOpacity="0.9"/>
          <stop offset="55%"  stopColor="#fff" stopOpacity="0.45"/>
          <stop offset="82%"  stopColor="#fff" stopOpacity="0"/>
        </radialGradient>
        <mask id={`hfM${uid}`}>
          <rect width={size} height={size} fill={`url(#hfMaskG${uid})`}/>
        </mask>
      </defs>
      {/* 环境辉光 —— 宝石照亮卡内空间 */}
      <circle cx={cx} cy={cy} r="280" fill={`url(#hfGlow${uid})`}/>
      {/* 6 扇形琢面 wash —— 卡面 = 被宝石同源光照亮的 6 瓣晶面（光/影） */}
      {Array.from({ length: 6 }, (_, i) => {
        const j = (i + 1) % 6;
        const p = `${cx},${cy} ${(cx + Math.cos(ang(i)) * wedgeR).toFixed(1)},${(cy + Math.sin(ang(i)) * wedgeR).toFixed(1)} ${(cx + Math.cos(ang(j)) * wedgeR).toFixed(1)},${(cy + Math.sin(ang(j)) * wedgeR).toFixed(1)}`;
        return <polygon key={`wd${i}`} points={p} fill="#fff" fillOpacity={(facetBright(i) * 0.085).toFixed(3)}/>;
      })}
      {/* 蜂窝晶格 —— 径向 mask 由内向外淡出（几何纹理，无需再叠全屏轮廓线） */}
      <g mask={`url(#hfM${uid})`}>{cells}</g>
    </svg>
  );
}

/* IQCoreCardO — 05o 黑卡①（唯一的黑卡）：纯宝石 hero + HexField 六边形空
   间场，没有别的内容（用户：ROOY 指数行移出黑卡、不做卡片）。R.ink 底 +
   左上对角 sheen（linear-gradient 135°，同宝石光源，给晶面体积感）+
   HexField（辉光 + 琢面 wash + 蜂窝晶格）。ROOY 指数 173 +
   sparkline 平铺到卡外白底，见 ReviewScreenO。 */
function IQCoreCardO() {
  return (
    <div style={{
      margin: '8px 16px 0', color: '#fff',
      borderRadius: B.cardL, boxShadow: Shadow.hero,
      position: 'relative', overflow: 'hidden',
      background: `linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0) 42%), ${R.ink}`,
    }}>
      <HexField/>
      <div style={{ position: 'relative', paddingBottom: 12 }}>
        <HexGemHeroO caps={RCAPS}/>
      </div>
    </div>
  );
}

/* ReviewScreenO（05o）— 只把六边形 hero 做成黑卡（IQCoreCardO：纯宝石 +
   HexField 空间场）；ROOY 指数 173 + sparkline 平铺到黑卡外白底（不做卡
   片，两栏 + 中间 hairline）；能力明细退回 05n 浅底列表（CapRowN，hairline
   分隔、可点击展开）。bg 仍 R.bg 近白。 */
function ReviewScreenO({ onNav }) {
  const [tab, setTab] = React.useState('review');
  const [expanded, setExpanded] = React.useState(null);
  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, position: 'relative', color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>
      <NavBar onBack={() => onNav && onNav('home')} title="滑雪学院"/>
      <div style={{ padding: '0 16px 6px', position: 'relative', zIndex: 1 }}>
        <Segmented value={tab} items={ACADEMY_TABS} onChange={(v) => {
          setTab(v);
          if (v === 'course') onNav && onNav('course');
          if (v === 'medals') onNav && onNav('achievement');
        }}/>
      </div>
      <div style={{ flex: 1, overflowY: 'auto', position: 'relative', zIndex: 1, paddingBottom: 36 }}>
        <IQCoreCardO/>
        {/* ROOY 指数行 —— 不做卡片，平铺到黑卡外白底（两栏 + 中间 hairline） */}
        <div style={{ display: 'flex', alignItems: 'center', margin: '14px 16px 0' }}>
          <div style={{ flex: 1, minWidth: 0 }}>
            <p style={{ ...T.eyebrow, color: R.rooy, marginBottom: 4 }}>ROOY 指数</p>
            <span style={{ fontFamily: '"IQ Oswald Reg","SF Pro Display",sans-serif', fontWeight: 400, fontStyle: 'normal', letterSpacing: '0', fontSize: 40, color: R.ink, lineHeight: 0.95 }}>173</span>
          </div>
          <div style={{ width: 0.5, background: R.hair, alignSelf: 'stretch', margin: '6px 16px' }}/>
          <div style={{ flex: 1, minWidth: 0 }}>
            <p style={{ ...T.footnote, color: R.ink3, marginBottom: 6 }}>近 7 天滑行</p>
            <IQSpark/>
          </div>
        </div>
        <div style={{ marginTop: 18, padding: '0 22px' }}>
          {RCAPS.map((c, i) => (
            <CapRowN key={c.id} c={c} first={i === 0}
              expanded={expanded === c.id}
              onClick={() => setExpanded(expanded === c.id ? null : c.id)}
              onTry={() => {}}
            />
          ))}
        </div>
      </div>
    </div>
  );
}
