/* ============================================================
   ROOY · Onboarding 五步 + 共享 form 组件
   ───────────────────────────────────────────────────────────
   需求：新用户画像采集 + 配对入口。完成后进 Home / Ski。

     OnboardingWelcome    — 大字 wordmark + 三按钮（微信 / 手机号 / 先看看）
     OnboardingBasicInfo  — 7 字段，三组 InsetGroup（身份 / 滑行 / 体型）
                            italic anchor = 昵称（personality moment）
     OnboardingGoal       — 滑行目标 4 单选
     OnboardingLevel      — 滑雪水平 4 单选
     OnboardingPair       — 现在配对 / 等到了再说

   Skip 路径：
     · Welcome "先看看" → home（出 flow）
     · BasicInfo / Goal / Level NavBar 右上"稍后" → home（出 flow）
     · Pair "等到了再说" → home（出 flow）

   accent budget：≤ 1 per screen。Welcome 不画图、不放
   illustration —— 靠 typography 大字 + 全屏极淡 topo wallpaper
   传达"这是雪场工具"。

   本文件也是几个跨屏 form 组件的家：FieldRow / FieldValue /
   NameInput / GenderPills / ChipRow / ChipsRow / PickerSheet /
   WheelPicker 等 —— edit-profile.jsx 也消费这些。
   ============================================================ */
/* welcome contour styles — injected once. */
if (typeof document !== 'undefined' && !document.getElementById('rooy-welcome-contour-style')) {
  const s = document.createElement('style');
  s.id = 'rooy-welcome-contour-style';
  s.textContent = `
    .welcome-contours {
      -webkit-mask-image: radial-gradient(115% 70% at 50% 30%, #000 38%, transparent 92%);
              mask-image: radial-gradient(115% 70% at 50% 30%, #000 38%, transparent 92%);
    }
    /* Each contour starts invisible (dashoffset = pathLength). When the
       group toggles to is-visible we animate dashoffset back to 0, drawing
       the stroke from start to end. Per-path inline transitionDelay
       cascades the lines from outer (low elevation) to inner (summit). */
    .welcome-contours path {
      stroke-dasharray: 1;
      stroke-dashoffset: 1;
    }
    .welcome-contours.is-visible path {
      stroke-dashoffset: 0;
      transition: stroke-dashoffset 2400ms cubic-bezier(0.45, 0.05, 0.2, 1);
    }
  `;
  document.head.appendChild(s);
}

/* Topographic contours — real marching-squares level sets, not hand-drawn.
   ────────────────────────────────────────────────────────────────────────
   构造一个 400×900 的 height field h(x,y)：3 座各向异性高斯峰 + 2 个低频
   固定相位 sin/cos 扰动 (≈±0.14 的大尺度起伏)。然后在 96×216 网格上离散
   采样，对 3 个等高线 level (0.30..0.88) 跑标准 marching squares (saddle
   用 cell-center value 消歧)，再把分散的线段按端点 hash 拼成折线。
   结果是"真正的等高线"——同一峰嵌套但每条形状不同、山脊处向下凹、
   山谷处向上凹、密度随坡度变化——和真实 topo map 同源。
   主峰故意放在 (200, 258)：屏上 1/3，logo 上方，等高线形成框选效果。
   仅保留两个低频 octave，砍掉高频扰动——高频扰动让相邻等高线非平行、
   出现局部"咯噔"，违反真实地形"邻线大致平行"的视觉规律。 */
const WELCOME_CONTOURS = (() => {
  const W = 400, H = 900, cols = 96, rows = 216;
  const dx = W / cols, dy = H / rows, stride = cols + 1;

  /* 峰位对角分布在屏两角，避开 logo 中心区域 (200, 440)。
     要让等高线"看起来是等高线"，必须能看到嵌套+平行的同心结构——这是
     地形图的视觉语义。但若主峰在屏中央，嵌套环会"框住 logo"成装饰画
     框。对角放峰：屏左上 + 屏右下各一座山，嵌套结构清晰可读，logo 区
     是两峰之间的鞍部留白。 */
  const peaks = [
    { cx:  50, cy:  70,  sx: 130, sy: 140, a:  1.00 }, // 屏左上嵌套
    { cx: 240, cy: 680,  sx: 120, sy: 105, a:  0.95 }, // 屏下偏中：近圆形嵌套（3 圈可读），左端延伸到屏左边
  ];
  const h = (x, y) => {
    let v = 0;
    for (const p of peaks) {
      const ax = (x - p.cx) / p.sx, ay = (y - p.cy) / p.sy;
      v += p.a * Math.exp(-0.5 * (ax * ax + ay * ay));
    }
    // 2 个低频 octave —— 制造大尺度不对称但不破坏"邻线大致平行"。
    v += 0.090 * Math.sin(x * 0.0145 + y * 0.0195 + 1.31);
    v += 0.050 * Math.cos(x * 0.0285 - y * 0.0235 + 4.22);
    return v;
  };

  const field = new Float32Array(stride * (rows + 1));
  for (let j = 0; j <= rows; j++)
    for (let i = 0; i <= cols; i++)
      field[j * stride + i] = h(i * dx, j * dy);

  // Marching squares on one elevation level → array of line segments.
  const levelSegments = (level) => {
    const segs = [];
    for (let j = 0; j < rows; j++) {
      for (let i = 0; i < cols; i++) {
        const tl = field[j * stride + i];
        const tr = field[j * stride + i + 1];
        const br = field[(j + 1) * stride + i + 1];
        const bl = field[(j + 1) * stride + i];
        let code = 0;
        if (tl >= level) code |= 1;
        if (tr >= level) code |= 2;
        if (br >= level) code |= 4;
        if (bl >= level) code |= 8;
        if (code === 0 || code === 15) continue;
        const x0 = i * dx, y0 = j * dy, x1 = x0 + dx, y1 = y0 + dy;
        const lerp = (va, vb, ax, ay, bx, by) => {
          const t = (level - va) / (vb - va);
          return [ax + t * (bx - ax), ay + t * (by - ay)];
        };
        const eT = () => lerp(tl, tr, x0, y0, x1, y0);
        const eR = () => lerp(tr, br, x1, y0, x1, y1);
        const eB = () => lerp(bl, br, x0, y1, x1, y1);
        const eL = () => lerp(tl, bl, x0, y0, x0, y1);
        const center = (tl + tr + br + bl) * 0.25;
        let pairs;
        switch (code) {
          case 1:  pairs = [[eL(), eT()]]; break;
          case 2:  pairs = [[eT(), eR()]]; break;
          case 3:  pairs = [[eL(), eR()]]; break;
          case 4:  pairs = [[eR(), eB()]]; break;
          /* saddle — center >= level: high corners (tl,br) connected through
             cell center, so contour wraps low corners (tr,bl). */
          case 5:  pairs = (center >= level)
                      ? [[eT(), eR()], [eL(), eB()]]
                      : [[eL(), eT()], [eR(), eB()]]; break;
          case 6:  pairs = [[eT(), eB()]]; break;
          case 7:  pairs = [[eL(), eB()]]; break;
          case 8:  pairs = [[eB(), eL()]]; break;
          case 9:  pairs = [[eB(), eT()]]; break;
          case 10: pairs = (center >= level)
                      ? [[eL(), eT()], [eR(), eB()]]
                      : [[eT(), eR()], [eL(), eB()]]; break;
          case 11: pairs = [[eB(), eR()]]; break;
          case 12: pairs = [[eR(), eL()]]; break;
          case 13: pairs = [[eR(), eT()]]; break;
          case 14: pairs = [[eT(), eL()]]; break;
        }
        for (const [a, b] of pairs) segs.push([a[0], a[1], b[0], b[1]]);
      }
    }
    return segs;
  };

  // Stitch raw segments end-to-end into continuous polylines by endpoint hash.
  const stitch = (segs) => {
    const k = (x, y) => `${Math.round(x * 10)}_${Math.round(y * 10)}`;
    const map = new Map();
    segs.forEach((s, idx) => {
      const k1 = k(s[0], s[1]), k2 = k(s[2], s[3]);
      if (!map.has(k1)) map.set(k1, []);
      if (!map.has(k2)) map.set(k2, []);
      map.get(k1).push({ idx, side: 0 });
      map.get(k2).push({ idx, side: 1 });
    });
    const used = new Array(segs.length).fill(false);
    const walk = (startIdx, fromSide) => {
      const pts = [];
      let cur = startIdx, side = fromSide;
      while (cur !== -1 && !used[cur]) {
        used[cur] = true;
        const s = segs[cur];
        const [ax, ay, bx, by] = side === 0
          ? [s[0], s[1], s[2], s[3]]
          : [s[2], s[3], s[0], s[1]];
        if (pts.length === 0) pts.push([ax, ay]);
        pts.push([bx, by]);
        const cand = map.get(k(bx, by)) || [];
        let next = -1, nextSide = 0;
        for (const c of cand) {
          if (c.idx !== cur && !used[c.idx]) {
            next = c.idx; nextSide = c.side; break;
          }
        }
        cur = next; side = nextSide;
      }
      return pts;
    };
    const polys = [];
    for (let i = 0; i < segs.length; i++) {
      if (used[i]) continue;
      const fwd = walk(i, 0);
      const head = fwd[0];
      const cand = map.get(k(head[0], head[1])) || [];
      let backIdx = -1, backSide = 0;
      for (const c of cand) {
        if (!used[c.idx]) { backIdx = c.idx; backSide = c.side; break; }
      }
      let poly = fwd;
      if (backIdx !== -1) {
        const back = walk(backIdx, backSide);
        back.reverse();
        poly = [...back.slice(0, -1), ...fwd];
      }
      polys.push(poly);
    }
    return polys;
  };

  /* Catmull-Rom-to-cubic-Bézier — turns a polyline of grid-cell crossings
     into a smooth curve that still passes through every point. For each
     segment Pi → Pi+1, the cubic's control points are derived from the
     neighbouring tangents (Pi+1 - Pi-1)/6 and (Pi+2 - Pi)/6. Result: real-
     map smoothness instead of marching-squares polygonal steps. Handles
     open polylines (endpoints clamped) and closed loops (wrap-around). */
  const toPath = (p) => {
    if (p.length < 2) return '';
    if (p.length < 3) {
      return `M${p[0][0].toFixed(1)},${p[0][1].toFixed(1)}L${p[1][0].toFixed(1)},${p[1][1].toFixed(1)}`;
    }
    const closed = Math.abs(p[0][0] - p[p.length - 1][0]) < 0.5
                && Math.abs(p[0][1] - p[p.length - 1][1]) < 0.5;
    const pts = closed ? p.slice(0, -1) : p;
    const n = pts.length;
    const get = (i) => closed
      ? pts[((i % n) + n) % n]
      : pts[Math.max(0, Math.min(n - 1, i))];
    const segs = closed ? n : n - 1;
    let s = `M${pts[0][0].toFixed(1)},${pts[0][1].toFixed(1)}`;
    for (let i = 0; i < segs; i++) {
      const p0 = get(i - 1), p1 = get(i), p2 = get(i + 1), p3 = get(i + 2);
      const c1x = p1[0] + (p2[0] - p0[0]) / 6;
      const c1y = p1[1] + (p2[1] - p0[1]) / 6;
      const c2x = p2[0] - (p3[0] - p1[0]) / 6;
      const c2y = p2[1] - (p3[1] - p1[1]) / 6;
      s += `C${c1x.toFixed(1)},${c1y.toFixed(1)} ${c2x.toFixed(1)},${c2y.toFixed(1)} ${p2[0].toFixed(1)},${p2[1].toFixed(1)}`;
    }
    if (closed) s += 'Z';
    return s;
  };

  /* 3 levels picked so the resulting ring radii are roughly evenly spaced —
     for a Gaussian peak, r ∝ √(−ln level), so equal r-gaps need geometrically
     spread level values, not linearly spread. 比上版稍微外推/内缩——把环间距
     拉宽一档；最外圈仍安全高于扰动天花板 (≈±0.14)，不会被噪声切碎。 */
  const levels = [0.30, 0.58, 0.88];
  /* Length filter: drop polylines shorter than 40 vertices — those are noise
     specks (a single perturbation crest poking above the level inside one
     or two grid cells), not real contours. 阈值随网格密度同步放大 (96/64 ≈ 1.5x)。*/
  const paths = [];
  for (const lv of levels) {
    for (const p of stitch(levelSegments(lv))) {
      if (p.length >= 40) paths.push(toPath(p));
    }
  }
  return paths;
})();

function OnboardingWelcome({ onNav }) {
  const [visible, setVisible] = React.useState(false);
  React.useEffect(() => {
    const t = setTimeout(() => setVisible(true), 120);
    return () => clearTimeout(t);
  }, []);

  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, color: R.ink, ...T.subhead,
      position: 'relative', overflow: 'hidden',
    }}>
      {/* Topo contour wallpaper — marching-squares level sets (see
          WELCOME_CONTOURS above). ROOY 色 stroke @ 36% alpha + radial
          mask 边缘 fade，opacity 0→1 在 mount 后 220ms 缓出 1.4s。 */}
      <svg
        className={`welcome-contours${visible ? ' is-visible' : ''}`}
        viewBox="0 0 400 900" preserveAspectRatio="xMidYMid slice"
        aria-hidden="true"
        style={{
          position: 'absolute', inset: 0,
          width: '100%', height: '100%',
          pointerEvents: 'none',
          color: `color-mix(in srgb, ${R.rooy} 36%, transparent)`,
        }}>
        <g fill="none" stroke="currentColor" strokeWidth="1.2"
           strokeLinejoin="round" strokeLinecap="round"
           vectorEffect="non-scaling-stroke">
          {WELCOME_CONTOURS.map((d, i) => (
            <path key={i} d={d} pathLength="1"
              style={{ transitionDelay: `${220 + i * 130}ms` }}/>
          ))}
        </g>
      </svg>

      {/* Soft ROOY-tinted glow up top — 跟 closing__before 同款氛围渲染 */}
      <div style={{
        position: 'absolute', inset: 0, pointerEvents: 'none',
        background: `radial-gradient(60% 50% at 50% 28%, color-mix(in srgb, ${R.rooy} 7%, transparent) 0%, transparent 70%)`,
      }}/>

      <StatusBar/>

      <div style={{ flex: 1 }}/>

      {/* Logo + slogan — center column. */}
      <div style={{
        position: 'relative', padding: '0 28px',
        display: 'flex', flexDirection: 'column', alignItems: 'center',
        textAlign: 'center',
      }}>
        <img
          src={(window.__ROOY_ASSET_BASE || '') + 'assets/logo-wordmark.png'} alt="ROOY"
          style={{ height: 48, display: 'block' }}
        />
        <h1 style={{
          ...T.title, color: R.ink, marginTop: 32,
          fontSize: 40, lineHeight: '46px', letterSpacing: '-0.02em',
        }}>让滑雪更简单</h1>
        <p style={{ ...T.para, color: R.ink2, marginTop: 14, maxWidth: 280 }}>
          每一次滑行，都是一场为你定制的运动体验
        </p>
      </div>

      <div style={{ flex: 1.2 }}/>

      {/* 按钮区 —— 三层 affordance 分清。 */}
      <div style={{ position: 'relative', padding: '0 16px 8px' }}>
        <PrimaryCTA onClick={() => onNav && onNav('onboarding-basicinfo')}>
          <WeChatGlyph/>
          <span>用微信继续</span>
        </PrimaryCTA>
        <button onClick={() => onNav && onNav('onboarding-basicinfo')} style={{
          width: '100%', padding: '14px 0',
          background: R.systemFill, color: R.ink,
          border: 'none',
          borderRadius: B.card, cursor: 'pointer',
          ...T.headline, fontWeight: 500,
          marginTop: 10,
        }}>用手机号继续</button>
        <button onClick={() => onNav && onNav('home')} style={{
          width: '100%', padding: '14px 0',
          background: 'transparent', color: R.ink3, border: 'none',
          cursor: 'pointer', ...T.subhead,
          marginTop: 4,
        }}>不登录，先看看 ›</button>
      </div>
      <HomeIndicator/>
    </div>
  );
}

/* WeChatGlyph —— 极简对话气泡，不模仿微信品牌色（保持中性） */
function WeChatGlyph() {
  return (
    <svg width="20" height="18" viewBox="0 0 20 18" fill="none">
      <ellipse cx="7.5" cy="7" rx="6.5" ry="5.4" stroke="#fff" strokeWidth="1.5"/>
      <path d="M 4 13.4 L 5.6 11.2" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/>
      <ellipse cx="13.4" cy="10.6" rx="5.2" ry="4.3" stroke="#fff" strokeWidth="1.5"/>
      <path d="M 17 15.6 L 15.8 14" stroke="#fff" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

function OnboardingBasicInfo({ onNav }) {
  const [name, setName] = React.useState('Yuki');
  const [gender, setGender] = React.useState('f');
  const [age, setAge] = React.useState(29);
  const [height, setHeight] = React.useState(168);
  const [weight, setWeight] = React.useState(54);
  const [openPicker, setOpenPicker] = React.useState(null);

  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, color: R.ink, ...T.subhead,
      position: 'relative',
    }}>
      <StatusBar/>
      <NavBar
        onBack={() => onNav && onNav('onboarding-welcome')}
        title=""
        right={<SkipLink onClick={() => onNav && onNav('home')}>稍后</SkipLink>}
      />

      <div style={{ padding: '0 22px 14px' }}>
        <StepProgress total={4} step={1}/>
      </div>

      <div style={{ flex: 1, overflowY: 'auto', paddingBottom: 32 }}>
        <div style={{ padding: '0 22px 0' }}>
          <h1 style={{ ...T.title, color: R.ink }}>聊聊你</h1>
          <p style={{ ...T.body, color: R.ink3, marginTop: 10 }}>
            几条基础信息，ROOY 用来给你定起点。
          </p>
        </div>

        {/* HERO · 昵称 ──
            italic anchor 让"取名 = 身份"成为第一屏 personality moment.
            其他字段都是客观数据, 用普通行/chip 表达, 名字 ONLY 享受
            italic 大字 + 下划线 input 待遇.
            ⚠ 标签 "你叫" 用 body 17 ink (匹配下方 FieldRow label
            字号), 不再是 eyebrow 13. */}
        <div style={{ padding: '22px 22px 0' }}>
          <span style={{ ...T.body, color: R.ink }}>你叫</span>
          <div style={{
            marginTop: 8, paddingBottom: 14,
            borderBottom: `1.5px solid ${R.ink}`,
            display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 12,
          }}>
            <input
              value={name}
              onChange={(e) => setName(e.target.value)}
              maxLength={20}
              style={{
                ...N.italic, fontSize: 56, color: R.ink,
                lineHeight: 1, letterSpacing: '-0.02em',
                background: 'transparent', border: 'none', outline: 'none',
                padding: 0, flex: 1, minWidth: 0,
                fontWeight: 900,
              }}
            />
            <svg width="22" height="22" viewBox="0 0 22 22" style={{ marginBottom: 6, flexShrink: 0, opacity: 0.5 }}>
              <path d="M14 3.5l4.5 4.5L8 18.5H3.5V14L14 3.5z"
                stroke={R.ink} strokeWidth="1.6" fill="none" strokeLinejoin="round"/>
            </svg>
          </div>
          <p style={{ ...T.footnote, color: R.ink3, marginTop: 8 }}>
            后面 ROOY 都这样喊你。
          </p>
        </div>

        {/* 4 editorial fields — full-bleed FieldRow list */}
        <div style={{ marginTop: 28, padding: '0 22px' }}>
          <FieldRow l="性别" first>
            <GenderPills value={gender} onChange={setGender}/>
          </FieldRow>
          <FieldRow l="年龄" onClick={() => setOpenPicker('age')}>
            <FieldValue v={`${age}`} u="岁"/>
          </FieldRow>
          <FieldRow l="身高" onClick={() => setOpenPicker('height')}>
            <FieldValue v={`${height}`} u="cm"/>
          </FieldRow>
          <FieldRow l="体重" onClick={() => setOpenPicker('weight')}>
            <FieldValue v={`${weight}`} u="kg"/>
          </FieldRow>
        </div>
      </div>

      <div style={{ padding: '0 16px 24px' }}>
        <PrimaryCTA onClick={() => onNav && onNav('onboarding-goal')}>下一步</PrimaryCTA>
      </div>
      <HomeIndicator/>

      {/* Picker bottom sheet — iOS-wheel-style scroll-snap picker.
          年龄/身高/体重 用同一个 sheet shell, content 切换. */}
      {openPicker && (
        <PickerSheet
          title={
            openPicker === 'age' ? '年龄' :
            openPicker === 'height' ? '身高' : '体重'
          }
          value={
            openPicker === 'age' ? age :
            openPicker === 'height' ? height : weight
          }
          options={
            openPicker === 'age'
              ? Array.from({ length: 81 }, (_, i) => 10 + i)    // 10-90 岁
              : openPicker === 'height'
              ? Array.from({ length: 81 }, (_, i) => 140 + i)   // 140-220 cm
              : Array.from({ length: 91 }, (_, i) => 35 + i)    // 35-125 kg
          }
          unit={
            openPicker === 'age' ? '岁' :
            openPicker === 'height' ? 'cm' : 'kg'
          }
          onChange={(v) => {
            if (openPicker === 'age') setAge(v);
            else if (openPicker === 'height') setHeight(v);
            else setWeight(v);
          }}
          onClose={() => setOpenPicker(null)}
        />
      )}
    </div>
  );
}

/* NameInput — value slot for 昵称 row. Right-aligned text input
   styled identically to FieldValue (italic 22, ink, weight 800,
   tight tracking) so the row reads as a coherent label+value pair
   even though it's a live text field. */
function NameInput({ value, onChange }) {
  return (
    <input
      value={value}
      onChange={(e) => onChange(e.target.value)}
      maxLength={20}
      style={{
        ...N.italic, fontSize: 22, color: R.ink, lineHeight: 1,
        fontWeight: 800, letterSpacing: '-0.02em',
        background: 'transparent', border: 'none', outline: 'none',
        padding: 0, textAlign: 'right',
        width: 140,
      }}
    />
  );
}

/* GenderPills — value slot for 性别 row. iOS systemFill 灰底未选,
   ink 填充选中 — 跟下方 ChipRow / iOS segmented 一致, 不再用 outline
   pill. Extracted so 11 + 15a share the exact pill styling. */
function GenderPills({ value, onChange }) {
  return (
    <div style={{ display: 'flex', gap: 6 }}>
      {[
        { id: 'f', l: '女' },
        { id: 'm', l: '男' },
      ].map(o => {
        const on = value === o.id;
        return (
          <button key={o.id} onClick={() => onChange(o.id)} style={{
            padding: '7px 18px', borderRadius: 999,
            background: on ? R.ink : R.systemFill,
            color: on ? '#fff' : R.ink2,
            border: 'none',
            cursor: 'pointer',
            ...T.subhead, fontWeight: on ? 600 : 400,
            minHeight: 36,
            transition: 'background .15s, color .15s',
          }}>{o.l}</button>
        );
      })}
    </div>
  );
}

/* ChipRow — single-select chip group. iOS systemFill bg by default,
   selected = ink fill. flex-wrap for ≥4 options. */
function ChipRow({ value, onChange, options }) {
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
      {options.map(o => {
        const on = value === o.id;
        return (
          <button key={o.id} onClick={() => onChange(o.id)} style={{
            padding: '10px 18px', borderRadius: 999,
            background: on ? R.ink : R.systemFill,
            color: on ? '#fff' : R.ink,
            border: 'none', cursor: 'pointer',
            ...T.subhead, fontWeight: on ? 600 : 400,
            minHeight: 38,
            transition: 'background .15s, color .15s',
          }}>{o.l}</button>
        );
      })}
    </div>
  );
}

/* ChipsRow — settings-style row whose right side is a small chip
   group instead of a single value. Used for low-cardinality picks
   (性别) that belong with other "objective attributes" in the same
   InsetGroup. */
function ChipsRow({ l, value, onChange, options }) {
  return (
    <div style={{
      padding: '12px 16px', minHeight: 48,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
    }}>
      <span style={{ ...T.body, color: R.ink }}>{l}</span>
      <div style={{ display: 'flex', gap: 6 }}>
        {options.map(o => {
          const on = value === o.id;
          return (
            <button key={o.id} onClick={() => onChange(o.id)} style={{
              padding: '6px 14px', borderRadius: 999,
              background: on ? R.ink : R.systemFill,
              color: on ? '#fff' : R.ink,
              border: 'none', cursor: 'pointer',
              ...T.footnote, fontWeight: on ? 600 : 400,
              minHeight: 30,
              transition: 'background .15s, color .15s',
            }}>{o.l}</button>
          );
        })}
      </div>
    </div>
  );
}

/* PickerRow — settings-style row that opens a wheel picker.
   Tap target = entire row. Right side: big tabular value + small
   unit + chevron. */
function PickerRow({ l, v, u, onClick }) {
  return (
    <div onClick={onClick} style={{
      padding: '14px 16px', minHeight: 48,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      cursor: 'pointer', gap: 12,
    }}>
      <span style={{ ...T.body, color: R.ink }}>{l}</span>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
        <span style={{ ...N.reg, ...T.body, color: R.ink, fontWeight: 500 }}>{v}</span>
        {u && <span style={{ ...T.footnote, color: R.ink3 }}>{u}</span>}
        <Chevron style={{ marginLeft: 4 }}/>
      </div>
    </div>
  );
}

/* PickerSheet — iOS-style bottom sheet wrapping a WheelPicker. Scrim
   dim + drag handle + title row + wheel below. 完成 按钮去掉：drag handle
   下滑 / 点击 scrim 都已经能关，"完成" 是 chrome 冗余。 */
function PickerSheet({ title, value, options, unit, onChange, onClose }) {
  return (
    <div onClick={onClose} style={{
      position: 'absolute', inset: 0, zIndex: 100,
      ...Scrim,
      display: 'flex', alignItems: 'flex-end',
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%',
        background: R.bg,
        borderTopLeftRadius: B.sheet,
        borderTopRightRadius: B.sheet,
        padding: '10px 0 24px',
        boxShadow: Shadow.sheet,
      }}>
        <div style={{ display: 'flex', justifyContent: 'center', paddingBottom: 10 }}>
          <div style={{ width: 36, height: 5, borderRadius: 2.5, background: R.hair2 }}/>
        </div>
        <div style={{
          padding: '0 20px 10px',
          display: 'flex', alignItems: 'center', justifyContent: 'center',
        }}>
          <span style={{ ...T.headline, color: R.ink }}>{title}</span>
        </div>
        <WheelPicker value={value} onChange={onChange} options={options} unit={unit}/>
      </div>
    </div>
  );
}

/* WheelPicker — iOS-style vertical scroll picker. CSS scroll-snap-y
   handles snap; onScroll updates selection from scrollTop.
     - ITEM_H 40 (each row height)
     - VISIBLE 5 rows (200px total)
     - center band: 1.5px hairline top/bottom (iOS picker tray)
     - selected: N.hero 28, ink, weight 800
     - others: N.reg 20, ink3, fading toward edges
   */
function WheelPicker({ value, onChange, options, unit }) {
  const ref = React.useRef(null);
  const ITEM_H = 40;
  const VISIBLE = 5;
  const center = Math.floor(VISIBLE / 2);

  // Position scroll at the current value on mount / value change
  React.useEffect(() => {
    if (!ref.current) return;
    const idx = options.indexOf(value);
    if (idx >= 0) ref.current.scrollTop = idx * ITEM_H;
  }, []);

  const handleScroll = () => {
    if (!ref.current) return;
    const idx = Math.round(ref.current.scrollTop / ITEM_H);
    const next = options[Math.max(0, Math.min(options.length - 1, idx))];
    if (next !== value) onChange(next);
  };

  return (
    <div style={{ position: 'relative', height: ITEM_H * VISIBLE }}>
      {/* iOS picker tray — hairline above and below the center row */}
      <div style={{
        position: 'absolute', left: 20, right: 20,
        top: ITEM_H * center, height: ITEM_H,
        borderTop: `0.5px solid ${R.hair2}`,
        borderBottom: `0.5px solid ${R.hair2}`,
        pointerEvents: 'none', zIndex: 1,
      }}/>
      <div
        ref={ref}
        onScroll={handleScroll}
        style={{
          height: '100%', overflowY: 'scroll',
          scrollSnapType: 'y mandatory',
          paddingTop: ITEM_H * center,
          paddingBottom: ITEM_H * center,
        }}>
        {options.map((o) => {
          const selected = o === value;
          return (
            <div key={o} style={{
              height: ITEM_H,
              scrollSnapAlign: 'center',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              gap: 6,
            }}>
              <span style={{
                ...N.reg,
                fontSize: selected ? 28 : 20,
                fontWeight: selected ? 700 : 400,
                color: selected ? R.ink : R.ink3,
                letterSpacing: '-0.02em',
                transition: 'font-size .15s, color .15s',
              }}>{o}</span>
              {unit && (
                <span style={{
                  ...T.footnote, color: R.ink3,
                  opacity: selected ? 1 : 0,
                }}>{unit}</span>
              )}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function OnboardingGoal({ onNav }) {
  const [selected, setSelected] = React.useState('hobby');
  const goals = [
    { id: 'start', label: '刚刚入门', desc: '想顺利滑下蓝道' },
    { id: 'hobby', label: '休闲爱好', desc: '想滑得更舒服' },
    { id: 'skill', label: '技术提升', desc: '想把动作练到位' },
    { id: 'race',  label: '备赛训练', desc: '为比赛做准备' },
  ];

  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>
      <NavBar
        onBack={() => onNav && onNav('onboarding-basicinfo')}
        title=""
        right={<SkipLink onClick={() => onNav && onNav('home')}>稍后</SkipLink>}
      />

      <div style={{ padding: '0 22px 18px' }}>
        <StepProgress total={4} step={2}/>
      </div>

      <div style={{ flex: 1, overflowY: 'auto', paddingBottom: 20 }}>
        <div style={{ padding: '0 22px 0' }}>
          <h1 style={{ ...T.title, color: R.ink }}>你的目标?</h1>
          <p style={{ ...T.body, color: R.ink3, marginTop: 10 }}>
            选一个最贴的，之后随时能改。
          </p>
        </div>

        <div style={{ height: 30 }}/>

        <div style={{ padding: '0 16px' }}>
          {goals.map(g => (
            <PickRow key={g.id} on={selected === g.id} onClick={() => setSelected(g.id)}
              label={g.label} desc={g.desc}/>
          ))}
        </div>
      </div>

      <div style={{ padding: '0 16px 24px' }}>
        <PrimaryCTA onClick={() => onNav && onNav('onboarding-level')}>下一步</PrimaryCTA>
      </div>
      <HomeIndicator/>
    </div>
  );
}

function OnboardingLevel({ onNav }) {
  const [selected, setSelected] = React.useState('mid');
  const levels = [
    { id: 'beginner', label: '初学', desc: '第一次穿雪板' },
    { id: 'novice',   label: '入门', desc: '能滑下蓝道' },
    { id: 'mid',      label: '中阶', desc: '红道流畅' },
    { id: 'adv',      label: '高阶', desc: '黑道能下' },
  ];

  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>
      <NavBar
        onBack={() => onNav && onNav('onboarding-goal')}
        title=""
        right={<SkipLink onClick={() => onNav && onNav('home')}>稍后</SkipLink>}
      />

      <div style={{ padding: '0 22px 18px' }}>
        <StepProgress total={4} step={3}/>
      </div>

      <div style={{ flex: 1, overflowY: 'auto', paddingBottom: 20 }}>
        <div style={{ padding: '0 22px 0' }}>
          <h1 style={{ ...T.title, color: R.ink }}>你的水平?</h1>
          <p style={{ ...T.body, color: R.ink3, marginTop: 10 }}>
            凭感觉选就好，滑两次 ROOY 会校准。
          </p>
        </div>

        <div style={{ height: 30 }}/>

        <div style={{ padding: '0 16px' }}>
          {levels.map(lv => (
            <PickRow key={lv.id} on={selected === lv.id} onClick={() => setSelected(lv.id)}
              label={lv.label} desc={lv.desc}/>
          ))}
        </div>
      </div>

      <div style={{ padding: '0 16px 24px' }}>
        <PrimaryCTA onClick={() => onNav && onNav('onboarding-pair')}>下一步</PrimaryCTA>
      </div>
      <HomeIndicator/>
    </div>
  );
}

function OnboardingPair({ onNav }) {
  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>
      <NavBar onBack={() => onNav && onNav('onboarding-level')} title=""/>

      <div style={{ padding: '0 22px 18px' }}>
        <StepProgress total={4} step={4}/>
      </div>

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column' }}>
        <div style={{ padding: '0 22px 0' }}>
          <h1 style={{ ...T.title, color: R.ink }}>传感器要连吗?</h1>
          <p style={{ ...T.body, color: R.ink3, marginTop: 10 }}>
            两枚卡在双板靴外侧搭扣上，开机后跟手机配对一次就行。
          </p>
        </div>

        {/* 直接复用配对页的 SensorPair（idle 态）—— 不再画靴子图。
            两枚 disc 是用户后面会反复见到的硬件视觉锚点；让它在 onboarding
            就提前认人。caption 说明位置，文字代替插图。 */}
        <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: '0 32px' }}>
          {typeof SensorPair === 'function' && <SensorPair state="idle" size={128}/>}
          <p style={{ ...T.footnote, color: R.ink3, marginTop: 28 }}>左右各一枚 · 蓝牙连接</p>
        </div>
      </div>

      <div style={{ padding: '0 16px 16px' }}>
        <PrimaryCTA onClick={() => onNav && onNav('pairing-searching')}>现在配对</PrimaryCTA>
        <button onClick={() => onNav && onNav('home')} style={{
          width: '100%', padding: '12px 0',
          background: 'transparent', color: R.ink3, border: 'none',
          cursor: 'pointer', ...T.subhead, marginTop: 2,
        }}>等到了再说 <Chevron size={12} color={R.ink3} style={{ verticalAlign: 'middle', marginLeft: 2 }}/></button>
      </div>
      <HomeIndicator/>
    </div>
  );
}

/* ── shared local primitives ────────────────────────────── */

function PickRow({ on, onClick, label, desc }) {
  return (
    <button onClick={onClick} style={{
      width: '100%', textAlign: 'left',
      background: R.card, border: 'none',
      borderRadius: B.card, padding: '16px 18px',
      marginBottom: 10, cursor: 'pointer',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      boxShadow: on ? `inset 0 0 0 1.5px ${R.ink}` : 'none',
    }}>
      <div>
        <p style={{ ...T.headline, color: R.ink }}>{label}</p>
        <p style={{ ...T.footnote, color: R.ink3, marginTop: 3 }}>{desc}</p>
      </div>
      {on ? (
        <svg width="22" height="22" viewBox="0 0 22 22">
          <circle cx="11" cy="11" r="10" fill={R.ink}/>
          <path d="M6 11.5l3.5 3.5L16 8" stroke="#fff" strokeWidth="1.7" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      ) : (
        <svg width="22" height="22" viewBox="0 0 22 22">
          <circle cx="11" cy="11" r="10" fill="none" stroke={R.hair2}/>
        </svg>
      )}
    </button>
  );
}

/* FieldRow — onboarding editorial form row. Full-bleed on paper bg,
   hairline top (except first), tap-target = entire row.
     - label: body 17pt ink (iOS Settings list row standard)
     - value slot: arbitrary content right (italic number, chips, etc)
     - chevron only when onClick present (picker affordance) */
function FieldRow({ l, children, onClick, first }) {
  return (
    <div onClick={onClick} style={{
      padding: '18px 0', minHeight: 56,
      borderTop: first ? 'none' : `0.5px solid ${R.hair}`,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      gap: 14,
      cursor: onClick ? 'pointer' : 'default',
    }}>
      <span style={{ ...T.body, color: R.ink }}>{l}</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        {children}
        {onClick && <Chevron/>}
      </div>
    </div>
  );
}

/* FieldValue — italic anchor for editorial onboarding rows.
   Mid-size italic number + small unit subscript, matching the
   "你叫 Yuki" italic vibe in the hero. */
function FieldValue({ v, u }) {
  return (
    <div style={{ display: 'inline-flex', alignItems: 'baseline', gap: 4 }}>
      <span style={{
        ...N.italic, fontSize: 22, color: R.ink, lineHeight: 1,
        fontWeight: 800, letterSpacing: '-0.02em',
      }}>{v}</span>
      {u && <span style={{ ...T.subhead, color: R.ink3 }}>{u}</span>}
    </div>
  );
}

function InfoRow({ l, r, placeholder = '未填', onClick }) {
  return (
    <div onClick={onClick} style={{
      padding: '14px 16px', minHeight: 48,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      cursor: 'pointer', gap: 12,
    }}>
      <span style={{ ...T.body, color: R.ink }}>{l}</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
        <span style={{ ...T.body, color: r ? R.ink3 : R.ink4 }}>{r || placeholder}</span>
        <Chevron/>
      </div>
    </div>
  );
}

function SkipLink({ onClick, children }) {
  return (
    <button onClick={onClick} style={{
      background: 'transparent', border: 'none', cursor: 'pointer',
      ...T.footnote, color: R.ink3, padding: '8px 10px',
      minHeight: 44,
    }}>{children}</button>
  );
}

/* StepProgress —— onboarding 底部 4 段细 progress。
   每段 4pt 高、6pt 间距；已完成段 ink 实心，当前段 ROOY 色，未来段
   hair。视觉负担轻，但用户能感知"快完了"。 */
/* StepProgress — page-indicator 风格：current 段拉长 + 其它短点。
   不全宽（不抢镜），同时颜色对比 ink/rooy/hair2 加强易读性。 */
function StepProgress({ total = 4, step = 1 }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: 6 }}>
      {Array.from({ length: total }).map((_, i) => {
        const done = i < step - 1;
        const cur = i === step - 1;
        const bg = done ? R.ink : cur ? R.rooy : R.hair2;
        const w = cur ? 32 : 8;
        return (
          <div key={i} style={{
            width: w, height: 4, borderRadius: 2,
            background: bg,
            transition: 'all .25s',
          }}/>
        );
      })}
    </div>
  );
}

Object.assign(window, {
  OnboardingWelcome, OnboardingBasicInfo,
  OnboardingGoal, OnboardingLevel, OnboardingPair,
  PickRow, InfoRow, SkipLink, StepProgress,
  ChipRow, ChipsRow, PickerRow, PickerSheet, WheelPicker,
  FieldRow, FieldValue, NameInput, GenderPills,
  WeChatGlyph,
});
