/* ============================================================
   ROOY · 04 对话 + 04a 镜头切换 + 输入相关组件
   ───────────────────────────────────────────────────────────
   04 需求：用户与 ROOY 教练的全部交互空间，也是系统通知统一入口
   （成就 / Session 总结 / 课程推荐 / 缆车检测都以 ROOY 第一人称
   发出）。

   消息节奏：
   - 跨 group 16px 呼吸，group 内 4px 紧凑
   - DayMarker = thin hairline + centered date pill（上 18 / 下 14）
   - ROOY group 顶部 kicker timestamp 跟气泡左缘对齐
   - Session summary embed 卡是独立全宽白卡，不假装气泡
   - LiveCard 是"刚刚" notification 节拍（白卡 + hairline outline）

   气泡视觉：
   - ROOY (RBub) = R.systemFill 灰底 + B.bubble (18/18/18/6 tail)
   - 用户 (UBub) = R.ink 黑底白字 + B.bubbleU (18/18/6/18 tail)
   - **聊天页不用 ROOY 绿染气泡** —— tail 形状已经做方向语义，
     色彩做"轻 vs 重"语义就够（教练听得轻、用户说得重）

   quick reply chips：白卡 + hairline outline（vs 灰气泡），跟
   LiveCard 同语言（白卡 = 可操作浮空层 / 灰 = 对话内容）。

   04a LensSheet 需求：从对话流筛出成长档案（按时间 / 课程过滤
   + 关键词搜索）。底部弹窗形态，drag handle 下滑关闭，**不放显式
   "取消"按钮**（chrome 冗余）。
   ============================================================ */

function RooyScreen({ onNav }) {
  const [draft, setDraft] = React.useState('');
  const [actionOpen, setActionOpen] = React.useState(false);
  const [voiceMode, setVoiceMode] = React.useState(false);

  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: R.bg, position: 'relative', color: R.ink, ...T.subhead,
    }}>
      <StatusBar/>

      {/* ── Header — restful, iMessage-grade ────── */}
      <div style={{
        padding: '8px 12px 12px',
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        position: 'relative', zIndex: 2,
        borderBottom: `0.5px solid ${R.hair}`,
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
          {/* iOS chevron 返回 —— 触发栈弹出 / tab 回退（同滑动手势）.
              chevron 比 iOS 标 (12×21) 稍大，跟 38pt avatar 视觉重心更平衡. */}
          <button onClick={() => onNav && onNav('back')} aria-label="返回" style={{
            width: 36, height: 44, padding: 0,
            background: 'transparent', border: 'none', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            flexShrink: 0,
          }}>
            <svg width="13" height="22" viewBox="0 0 13 22" fill="none">
              <path d="M 10 2 L 2 11 L 10 20"
                stroke={R.ink} strokeWidth="2"
                strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </button>

          <div onClick={() => onNav && onNav('rooy-profile')} style={{
            display: 'flex', alignItems: 'center', gap: 12,
            cursor: 'pointer',
          }}>
            <div style={{
              position: 'relative', width: 38, height: 38, borderRadius: '50%',
              background: R.ink, display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <span style={{ ...N.hero, fontSize: 15, color: '#fff' }}>R</span>
              <span style={{
                position: 'absolute', right: -1, bottom: -1,
                width: 12, height: 12, borderRadius: '50%',
                background: R.rooy, border: `2.5px solid ${R.bg}`,
              }}/>
            </div>
            <div>
              <p style={{ ...T.headline, color: R.ink, lineHeight: 1 }}>ROOY</p>
              <p style={{ ...T.caption1, color: R.rooy, fontWeight: 500, marginTop: 2 }}>教学中</p>
            </div>
          </div>

        </div>

        {/* header 右侧组：升级 Pro 入口 + 历程回顾。平行四边形 pill（skew）
            实色 rooy + 白字，运动感；靠右放。 */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
          <button onClick={() => onNav && onNav('rooy-pro')} style={{
            background: R.rooy, border: 'none', borderRadius: 4,
            padding: '6px 12px', cursor: 'pointer', flexShrink: 0,
            transform: 'skewX(-12deg)',
          }}>
            <span style={{
              display: 'inline-flex', alignItems: 'center',
              transform: 'skewX(12deg)',
              ...T.caption1, color: '#fff', fontWeight: 600,
            }}>
              升级 PRO
            </span>
          </button>

          {/* "历程回顾" entry — clock face (semantic carried by the icon
              being a clock, label disambiguates direction). */}
          <button onClick={() => onNav && onNav('rooy-lens')} aria-label="历程回顾" style={{
            width: 44, height: 44, background: 'transparent', border: 'none', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            padding: 0,
          }}>
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none">
              <circle cx="12" cy="12" r="8.6"
                stroke={R.ink} strokeWidth="1.7" fill="none"/>
              <path d="M 12 7 L 12 12 L 15.8 14"
                stroke={R.ink} strokeWidth="1.7" fill="none"
                strokeLinecap="round" strokeLinejoin="round"/>
            </svg>
          </button>
        </div>
      </div>

      {/* ── Messages scroll ─────────────────────── */}
      <div style={{ flex: 1, overflowY: 'auto', position: 'relative' }}>

        <DayMarker label="今天 · 5 月 3 日"/>

        <MsgGroup who="rooy" time="09:12">
          <RBub>昨晚下了 12cm 新雪。先去 Furi 那条蓝道试感觉，不急着上 Stockhorn。</RBub>
        </MsgGroup>

        <SessionSummaryCard
          headline="Zermatt"
          date="4 月 30 日"
          metrics={[
            { l: '趟数', v: '13' },
            { l: '垂降', v: '18.7', u: 'km' },
            { l: '用时', v: '2:18' },
          ]}
        />

        <MsgGroup who="rooy">
          <RBub>立刃首次稳定破 40°，连续三趟都保持得很好。</RBub>
        </MsgGroup>

        <MsgGroup who="user">
          <UBub>感觉了！但右弯还是有点飘。</UBub>
        </MsgGroup>

        <MsgGroup who="rooy" time="09:15">
          <RBub>对，我看到了。</RBub>
          <RBub embed>
            <p style={{ ...T.eyebrow, color: R.rooy, marginBottom: 6 }}>观察</p>
            <p style={{ ...T.body, color: R.ink }}>
              右弯入弯重心慢 <span style={{ ...N.hero, fontSize: 17, color: R.data, letterSpacing: '-0.02em' }}>0.15</span>
              <span style={{ ...T.caption1, color: R.data, fontWeight: 600, marginLeft: 1 }}>s</span>
            </p>
          </RBub>
          <RBub>
            今天试一件事：右弯入弯时，想象把脚趾往雪里
            <span style={{ color: R.ink, fontWeight: 600 }}>踩一下</span>。
          </RBub>
        </MsgGroup>

        <MsgGroup who="user">
          <UBub>好。试试。</UBub>
        </MsgGroup>

        <DayMarker label="刚刚"/>

        <MsgGroup who="rooy">
          <LessonInviteCard onStart={() => onNav && onNav('ski-c')}/>
        </MsgGroup>

        <MsgGroup who="rooy">
          <LiveCard text="这趟比上趟稳。聊聊？" kicker="缆车上 · ROOY 看见了"/>
        </MsgGroup>

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

      {/* ── Quick replies ──────────────────────────
          iOS SiriKit / iMessage Suggestion 风格：systemFill 灰底 pill，无
          shadow、无 outline。不再用 R.card + Shadow.card——白卡 + drop
          shadow 在 .bg 上读起来"重量过头"，像浮空 LiveCard 而不是输入辅助
          条。systemFill 灰底跟 ROOY 灰气泡同色但形状/位置不同（pill 在
          input 上方一条，气泡是消息流内），不会语义混淆。 */}
      <div style={{
        padding: '12px 16px 8px', display: 'flex', gap: 8, overflowX: 'auto', flexShrink: 0,
        background: R.bg,
      }}>
        {['整体怎么样?', '明天练什么', '继续上次的课'].map((q, i) => (
          <button key={i} style={{
            flexShrink: 0, ...T.subhead,
            padding: '7px 14px', borderRadius: 999,
            background: R.systemFill, border: 'none', color: R.ink, cursor: 'pointer',
          }}>{q}</button>
        ))}
      </div>

      {/* ── Input bar ────────────────────────────── */}
      <ChatInput
        value={draft}
        onChange={setDraft}
        onActionTap={() => setActionOpen(true)}
        onVoiceStart={() => setVoiceMode(true)}
        onVoiceEnd={() => setVoiceMode(false)}
        onSend={() => setDraft('')}
      />

      {/* Voice overlay — visible while user holds the + button past
          the long-press threshold. Centered modal-ish over the chat. */}
      {voiceMode && <VoiceOverlay/>}

      {/* Action sheet — bottom sheet with 拍照 / 录像 / 相册 options. */}
      {actionOpen && <ChatActionSheet onClose={() => setActionOpen(false)}/>}
    </div>
  );
}

/* ChatInput — iOS chat input with state-driven right button.
   Empty draft: "+" button.
     · TAP    → onActionTap (opens action sheet)
     · LONG-PRESS (≥ 320ms) → onVoiceStart, hold = recording.
       Release → onVoiceEnd. (Long-press fires BEFORE tap intent
       is decided; on release we check which fired.)
   Non-empty draft: "+" swaps to filled 发送 button (single tap).
*/
function ChatInput({ value, onChange, onActionTap, onVoiceStart, onVoiceEnd, onSend }) {
  const hasText = !!value.trim();
  const pressTimer = React.useRef(null);
  const longPressFired = React.useRef(false);
  const [recording, setRecording] = React.useState(false);

  /* Long-press 走 pointer events，tap (action sheet) 走 onClick。
     原因：startPress 之前调 e.preventDefault() 阻止默认手势，
     副作用是 Android Chrome 不再合成 click —— pointerup 之后什么
     都没发生，action sheet 永远打不开。改成 onClick 主导 tap，
     pointer events 只测长按 timer，最稳。touchAction:'none' 仍
     防双击放大。 */
  const startPress = () => {
    if (hasText) return;
    longPressFired.current = false;
    pressTimer.current = setTimeout(() => {
      longPressFired.current = true;
      setRecording(true);
      onVoiceStart && onVoiceStart();
    }, 320);
  };

  const endPress = () => {
    if (hasText) return;
    clearTimeout(pressTimer.current);
    if (longPressFired.current) {
      setRecording(false);
      onVoiceEnd && onVoiceEnd();
    }
  };

  const cancelPress = () => {
    clearTimeout(pressTimer.current);
    if (longPressFired.current) {
      setRecording(false);
      onVoiceEnd && onVoiceEnd();
    }
  };

  const handleTap = () => {
    if (hasText) return;
    /* onClick fires after pointerup; if long-press fired, swallow
       the tap. Reset flag so the next tap isn't suppressed. */
    if (longPressFired.current) {
      longPressFired.current = false;
      return;
    }
    onActionTap && onActionTap();
  };

  return (
    <div style={{
      padding: '8px 12px 12px', display: 'flex', gap: 8, alignItems: 'center', flexShrink: 0,
      background: R.bg,
    }}>
      {/* Input pill — 48pt 高 + 0.5px hairline。input lineHeight=46 占
          满垂直空间, baseline 自然居中. */}
      <div style={{
        flex: 1, background: '#fff', borderRadius: 999,
        padding: '0 18px', display: 'flex', alignItems: 'center',
        border: `0.5px solid ${R.hair}`,
        minHeight: 48,
      }}>
        <input
          value={value}
          onChange={(e) => onChange(e.target.value)}
          placeholder="Ask ROOY…"
          style={{
            ...T.body, color: R.ink, flex: 1,
            background: 'transparent', border: 'none', outline: 'none',
            padding: 0, minWidth: 0, lineHeight: '46px',
          }}
        />
      </div>

      {/* 右侧按钮 — 48×48 圆按钮，跟 input pill 同高，外置 Telegram /
          WeChat 模式（iMessage 是嵌入 pill 内的小 28pt，但 ROOY 的 +
          要承担长按录音语义，独立大按钮更易触达）。
          Send icon 用 iOS 标准上箭头 (▲)，不是右箭头——发送是"往上发出
          去"的语义，跟 iMessage / Messages / WhatsApp 一致. */}
      {hasText ? (
        <button onClick={onSend} aria-label="发送" style={{
          width: 48, height: 48, borderRadius: '50%', background: R.ink,
          border: 'none', cursor: 'pointer',
          display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
        }}>
          <svg width="20" height="20" viewBox="0 0 20 20">
            <path d="M10 16V5M5 10l5-5 5 5" stroke="#fff" strokeWidth="2.2"
              fill="none" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
      ) : (
        <button
          onClick={handleTap}
          onPointerDown={startPress}
          onPointerUp={endPress}
          onPointerCancel={cancelPress}
          aria-label="更多 / 长按说话"
          style={{
            width: 48, height: 48, borderRadius: '50%',
            background: recording ? R.alert : R.ink,
            border: 'none', cursor: 'pointer',
            display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            touchAction: 'none', userSelect: 'none',
            transition: 'background .15s, transform .15s',
            transform: recording ? 'scale(1.06)' : 'scale(1)',
          }}>
          {recording ? (
            <MicGlyph size={22} color="#fff"/>
          ) : (
            <svg width="20" height="20" viewBox="0 0 20 20">
              <path d="M10 4v12M4 10h12" stroke="#fff" strokeWidth="2.2"
                fill="none" strokeLinecap="round"/>
            </svg>
          )}
        </button>
      )}
    </div>
  );
}

/* VoiceOverlay — full-card visual cue while holding to record.
   Sits over chat content but below input bar (so user can see the
   button they're holding). Mic glyph + animated waveform + hint. */
function VoiceOverlay() {
  return (
    <div style={{
      position: 'absolute', left: '50%', top: '46%',
      transform: 'translate(-50%, -50%)',
      width: 200, padding: '28px 24px 20px', borderRadius: B.cardL,
      background: 'rgba(14,17,22,0.88)',
      backdropFilter: 'blur(6px)',
      display: 'flex', flexDirection: 'column',
      alignItems: 'center', justifyContent: 'center', gap: 14,
      zIndex: 60, pointerEvents: 'none',
      boxShadow: Shadow.modal,
    }}>
      <MicGlyph size={44} color="#fff"/>
      <WaveformBars/>
      <p style={{ ...T.footnote, color: R.inkD2, marginTop: 2 }}>松开发送 · 上滑取消</p>
    </div>
  );
}

/* WaveformBars — purely decorative animated bars to suggest input
   level. Pure CSS keyframes. */
function WaveformBars() {
  if (typeof document !== 'undefined' && !document.getElementById('rooy-wave-style')) {
    const s = document.createElement('style');
    s.id = 'rooy-wave-style';
    s.textContent = `
      @keyframes rooyWave {
        0%, 100% { transform: scaleY(0.4); }
        50% { transform: scaleY(1); }
      }
      .rooy-wave-bar {
        width: 3px; height: 22px; background: #fff; border-radius: 1.5px;
        transform-origin: center;
        animation: rooyWave 0.9s ease-in-out infinite;
      }
    `;
    document.head.appendChild(s);
  }
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 4, height: 24 }}>
      {[0, 0.18, 0.06, 0.34, 0.12, 0.22, 0.04].map((d, i) => (
        <span key={i} className="rooy-wave-bar"
          style={{ animationDelay: `${d}s` }}/>
      ))}
    </div>
  );
}

/* ChatActionSheet — iMessage 17-style inline tray.
   ════════════════════════════════════════════════════════════════
   Renders as a full-width strip directly above the ChatInput bar,
   sharing the paper bg + sitting on a hairline divider — feels like
   the input area "extended upward" rather than a floating popover.
   Each option: 56pt circular systemFill disc + icon + caption below.
   Replaces the quick-replies row visually while open; tap outside
   to dismiss.
   Slide-up + fade entrance via injected keyframes (same pattern as
   WaveformBars). */
function ChatActionSheet({ onClose }) {
  if (typeof document !== 'undefined' && !document.getElementById('rooy-tray-style')) {
    const s = document.createElement('style');
    s.id = 'rooy-tray-style';
    s.textContent = `
      @keyframes rooyTrayUp {
        from { transform: translateY(12px); opacity: 0; }
        to   { transform: translateY(0);    opacity: 1; }
      }
    `;
    document.head.appendChild(s);
  }
  const opts = [
    { id: 'photo', l: '拍照', glyph: <CameraGlyph size={26}/> },
    { id: 'video', l: '录像', glyph: <VideoGlyph size={26}/> },
    { id: 'lib',   l: '相册', glyph: <LibraryGlyph size={26}/> },
  ];
  return (
    <div onClick={onClose} style={{
      position: 'absolute', inset: 0, zIndex: 70,
      background: 'transparent',
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        position: 'absolute', left: 0, right: 0,
        /* Bottom = ChatInput 总高 (padding 8 + 48 input + 12 = 68) — tray
           底边贴 input bar 顶，视觉上读作输入区"上扩". */
        bottom: 68,
        background: R.bg,
        borderTop: `0.5px solid ${R.hair}`,
        padding: '20px 16px 22px',
        display: 'flex',
        justifyContent: 'space-around',
        animation: 'rooyTrayUp 0.22s cubic-bezier(0.32, 0.72, 0.18, 1)',
      }}>
        {opts.map((o) => (
          <button key={o.id} onClick={onClose} style={{
            background: 'transparent', border: 'none', cursor: 'pointer',
            padding: 0,
            display: 'flex', flexDirection: 'column',
            alignItems: 'center', gap: 8,
            minWidth: 72,
            color: R.ink,
          }}>
            <div style={{
              width: 56, height: 56, borderRadius: '50%',
              background: R.systemFill,
              display: 'flex', alignItems: 'center', justifyContent: 'center',
              color: R.ink,
            }}>
              {o.glyph}
            </div>
            <span style={{ ...T.footnote, color: R.ink2 }}>{o.l}</span>
          </button>
        ))}
      </div>
    </div>
  );
}

function MicGlyph({ size = 20, color = '#fff' }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <rect x="9" y="3" width="6" height="11" rx="3"
        stroke={color} strokeWidth="1.8" fill={color}/>
      <path d="M5 11a7 7 0 0 0 14 0" stroke={color} strokeWidth="1.8"
        fill="none" strokeLinecap="round"/>
      <path d="M12 18v3M9 21h6" stroke={color} strokeWidth="1.8"
        strokeLinecap="round"/>
    </svg>
  );
}

function CameraGlyph({ size = 20, color }) {
  const c = color || 'currentColor';
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <path d="M4 8 H8 L9.5 6 H14.5 L16 8 H20 a1 1 0 0 1 1 1 V18 a1 1 0 0 1 -1 1 H4 a1 1 0 0 1 -1 -1 V9 a1 1 0 0 1 1 -1 Z"
        stroke={c} strokeWidth="1.6" fill="none" strokeLinejoin="round"/>
      <circle cx="12" cy="13" r="3.4" stroke={c} strokeWidth="1.6" fill="none"/>
    </svg>
  );
}

function VideoGlyph({ size = 20, color }) {
  const c = color || 'currentColor';
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <rect x="3" y="7" width="13" height="10" rx="1.5"
        stroke={c} strokeWidth="1.6" fill="none"/>
      <path d="M16 11 L21 8 V16 L16 13 Z"
        stroke={c} strokeWidth="1.6" fill="none" strokeLinejoin="round"/>
    </svg>
  );
}

function LibraryGlyph({ size = 20, color }) {
  const c = color || 'currentColor';
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none">
      <rect x="4" y="5" width="14" height="14" rx="2"
        stroke={c} strokeWidth="1.6" fill="none"/>
      <path d="M4 15 L9 11 L13 14 L17 10 L21 14"
        stroke={c} strokeWidth="1.6" fill="none" strokeLinejoin="round" strokeLinecap="round"/>
      <circle cx="14" cy="9.5" r="1.4" fill={c}/>
    </svg>
  );
}

/* LessonInviteCard — ROOY's session-start message with an inline
   lesson-mode CTA. v2 corrections per review:
     · no Dot kicker (too "notification" feel for a chat bubble)
     · max-width 300 matches RBub (not full-width like LiveCard)
     · grey systemFill bg (green tint at 7% was too saturated for
       repeating chat use); CTA itself still carries ROOY 色 to
       anchor the lesson-mode signal
   One per session — restraint-compliant. */
function LessonInviteCard({ onStart }) {
  return (
    <div style={{
      maxWidth: 300,
      background: R.card,
      borderRadius: B.bubble,
      padding: '12px 16px 14px',
      boxShadow: Shadow.card,
    }}>
      <p style={{ ...T.para, color: R.ink }}>
        今天先松一段试试感觉。想直接进上课，我陪你练
        <span style={{ color: R.ink, fontWeight: 600 }}>立刃 · 40°</span>。
      </p>
      {/* CTA 移到气泡右侧 — iOS chat-bubble inline-action 标准位置（消息内
          的 action 永远右对齐，类似 iMessage 的 "Reply" suggestion）。
          升一档到 md：T.subhead 600 + 10/18 padding，比原 sm（T.footnote
          500 + 8/16）更有"主行动"分量，跟 ChatInput 48pt 同尺度生态对齐. */}
      <div style={{ marginTop: 10, display: 'flex', justifyContent: 'flex-end' }}>
        <InlineCTA size="md" onClick={onStart}>开始上课</InlineCTA>
      </div>
    </div>
  );
}

/* ── Day marker — hairline + centered pill ───────────────── */
function DayMarker({ label: l }) {
  return (
    <div style={{ padding: '22px 24px 16px', display: 'flex', alignItems: 'center', gap: 14 }}>
      <div style={{ flex: 1, height: 0.5, background: R.hair }}/>
      <span style={{ ...T.eyebrow, color: R.ink3 }}>{l}</span>
      <div style={{ flex: 1, height: 0.5, background: R.hair }}/>
    </div>
  );
}

/* ── MsgGroup — generous breath between speakers ─────────── */
function MsgGroup({ who, time, children }) {
  const isRooy = who === 'rooy';
  return (
    <div style={{ padding: '0 16px', marginBottom: 16 }}>
      {time && (
        <p style={{
          ...T.eyebrow, color: R.ink4,
          textAlign: 'center', marginBottom: 6,
        }}>{time}</p>
      )}
      <div style={{
        display: 'flex', flexDirection: 'column',
        alignItems: isRooy ? 'flex-start' : 'flex-end',
        gap: 5,
      }}>
        {children}
      </div>
    </div>
  );
}

/* ROOY bubble — systemFill 灰底。embed 变体跟普通气泡用 leading
   "观察" eyebrow 区分，不靠绿色 tint —— 聊天页不染 ROOY 绿。
   padding 9/14/10 跟 iMessage / WhatsApp 节奏对齐（17/26 T.para 已经
   自带 lineHeight，padding 不需要太厚）。
   maxWidth 312 ≈ 在 390pt iPhone 上占 ~80%、在 430pt Pro Max 上占 ~73%，
   是 iMessage 标准上限——比 v1 的 282 大 30pt，长段落不再过早换行. */
function RBub({ children, embed }) {
  return (
    <div style={{
      maxWidth: 312,
      background: R.systemFill,
      padding: embed ? '10px 14px 12px' : '9px 14px 10px',
      borderRadius: B.bubble,
      ...T.para, color: R.ink,
    }}>
      {children}
    </div>
  );
}

/* User bubble — ink */
function UBub({ children }) {
  return (
    <div style={{
      maxWidth: 296,
      background: R.ink, color: '#fff',
      padding: '9px 14px 10px',
      borderRadius: B.bubbleU,
      ...T.para,
    }}>{children}</div>
  );
}

/* LiveCard — "just now" notification beat（缆车上 / 自动消息）。
   ════════════════════════════════════════════════════════════════
   ⛔ DO NOT add a ROOY-green Dot / pulsing indicator to this card.
   ════════════════════════════════════════════════════════════════
   该屏的"ROOY 在场"信号已经由多处承担：kicker 文本 (ROOY 色)
   + ROOY-tail bubble 形状 + header avatar pip + "教学中" eyebrow。
   再加一个 Dot 是绿色泛滥 —— kicker 已经是 lede，不需要复述。
   多个 agent 已经反复犯过这个错，banner 长留。 */
function LiveCard({ text, kicker }) {
  return (
    <div style={{
      maxWidth: 300,
      background: R.card,
      borderRadius: B.bubble,
      padding: '12px 16px 14px',
      boxShadow: Shadow.card,
    }}>
      <p style={{ ...T.eyebrow, color: R.rooy, marginBottom: 4 }}>{kicker}</p>
      <p style={{ ...T.para, color: R.ink }}>{text}</p>
    </div>
  );
}

/* Session summary card —— 今日总结 editorial magazine 风。
   italic anchor 仍然落在 chat 屏的 inline 0.15s；此卡 italic 是
   editorial 装饰，3 个数字 + 标题 italic 让"今日总结"这个 moment
   读起来像杂志封面。kicker 中文化。 */
function SessionSummaryCard({ headline, date, metrics, onOpen }) {
  return (
    <div style={{ padding: '4px 16px 14px' }}>
      <div onClick={onOpen} style={{
        maxWidth: 312,
        background: R.card,
        borderRadius: 18,
        padding: '14px 18px 16px',
        cursor: onOpen ? 'pointer' : 'default',
        boxShadow: Shadow.card,
      }}>
        <p style={{
          ...T.eyebrow,
          marginBottom: 6,
        }}>
          <span style={{ color: R.rooy }}>今日总结</span>
          <span style={{ color: R.ink3 }}> · {date}</span>
        </p>

        <p style={{
          ...N.italic, fontSize: 40, color: R.ink, lineHeight: 0.95,
          letterSpacing: '-0.02em', fontWeight: 900,
          marginTop: 2,
        }}>{headline}<span>.</span></p>

        <div style={{ marginTop: 14, display: 'flex', gap: 24 }}>
          {metrics.map((m, i) => (
            <div key={i}>
              <p style={{ ...T.footnote, color: R.ink3, marginBottom: 2 }}>{m.l}</p>
              <p style={{ display: 'flex', alignItems: 'baseline', gap: 3 }}>
                <span style={{
                  ...N.italic, fontSize: 24, color: R.ink, lineHeight: 1,
                  fontWeight: 800, letterSpacing: '-0.02em',
                }}>{m.v}</span>
                {m.u && (
                  <span style={{ ...T.caption1, color: R.ink3, fontWeight: 500 }}>{m.u}</span>
                )}
              </p>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   RooyLensSheet — 04a 镜头切换 · 历程回顾
   ───────────────────────────────────────────────────────────
   需求：从对话流筛出成长档案。底部弹窗形态。

   过滤维度：时间（按月跳转）+ 课程 + 总结类型 + 关键词搜索。
   每条记录：左侧 type 图标 + 标题/副信息 + 右侧日期 + chevron。

   Sheet header：drag handle + 居中标题 "消息回顾"，**不放显式
   "取消"按钮**（drag handle 下滑关闭已经够，再加是 chrome 冗余）。
   ============================================================ */

/* Single source of truth — each session has its place / km /
   duration / optional lesson topic / ROOY's post-session note.
   Season is the surface for cross-year navigation (see MonthJump). */
const LENS_ITEMS = [
  { id: 1,  season: '2025 — 26', m: 5, day: 3,  place: 'Zermatt',  km: 8.4, duration: '2:18 h',
    lesson: '立刃路径 · 第 3 节',
    summary: '右弯入弯快了 0.08s，比昨天稳。立刃连三趟稳在 40°。继续保持。' },
  { id: 11, season: '2025 — 26', m: 4, day: 29, place: 'Verbier',  km: 5.6, duration: '1:32 h',
    lesson: '立刃路径 · 第 2 节',
    summary: '7 趟里 5 趟节奏稳在 0.95s 上下。右弯立刃还有点迟疑，下次专门练这个。' },
  { id: 4,  season: '2025 — 26', m: 4, day: 23, place: 'Saas-Fee', km: 6.2, duration: '1:48 h',
    lesson: null,
    summary: '雪面偏硬，立刃角度普遍保守了 5°。这是对的，硬雪上太激进容易打滑。' },
  { id: 5,  season: '2025 — 26', m: 4, day: 18, place: 'Verbier',  km: 9.0, duration: '2:04 h',
    lesson: '节奏路径 · 第 1 节',
    summary: '节奏路径起点不错，重心切换偏晚 0.12s。下次做一组节奏拍子练习。' },
  { id: 12, season: '2025 — 26', m: 4, day: 12, place: 'Zermatt',  km: 7.1, duration: '1:52 h',
    lesson: null,
    summary: '今天节奏稳定，无大问题。' },
  { id: 13, season: '2025 — 26', m: 3, day: 27, place: 'Zermatt',  km: 9.8, duration: '2:24 h',
    lesson: '立刃路径 · 第 1 节',
    summary: '立刃路径开课。基础数据：左弯 28°，右弯 25°。下次从右弯开始练。' },
  { id: 8,  season: '2025 — 26', m: 3, day: 8,  place: 'Zermatt',  km: 4.9, duration: '1:36 h',
    lesson: null,
    summary: '热身性质的滑行，状态稳定。' },
  { id: 14, season: '2025 — 26', m: 2, day: 19, place: 'Saas-Fee', km: 7.8, duration: '2:08 h',
    lesson: null,
    summary: '雪季中段，节奏比 12 月稳定很多。' },
  { id: 15, season: '2025 — 26', m: 1, day: 14, place: 'Zermatt',  km: 5.5, duration: '1:42 h',
    lesson: null,
    summary: '今年第一次到 Zermatt，先适应雪场。' },
];

const LENS_SEASONS = ['2025 — 26', '2024 — 25', '2023 — 24']; /* newest first */
const SEASON_MONTHS = [11, 12, 1, 2, 3, 4, 5]; /* a ski season runs Nov→May */

/* '2025 — 26' → [2025, 2026]. Nov / Dec belong to the first year,
   Jan – May to the second. */
function seasonYears(season) {
  const parts = season.split(/[—\-]/).map(s => s.trim());
  const y1 = parseInt(parts[0], 10);
  const tail = parts[1] || '';
  const y2 = tail.length === 2
    ? parseInt(String(y1).slice(0, 2) + tail, 10)
    : parseInt(tail, 10);
  return [y1, y2];
}
function yearFor(season, m) {
  const [y1, y2] = seasonYears(season);
  return (m === 11 || m === 12) ? y1 : y2;
}

const LENS_MONTHS  = [11, 12, 1, 2, 3, 4, 5];
const LENS_MONTH_DAYS = { 1: 31, 2: 28, 3: 31, 4: 30, 5: 31, 6: 30, 7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31 };

/* Mock course list for the 课程 filter chip.
   range is a human-readable label; in production these come from
   the curriculum/progress store. */
const LENS_COURSES = [
  { id: 'parallel', l: '平行式',    range: '2024-12 — 2025-01' },
  { id: 'carving',  l: '刃滑路径',  range: '2025-01 — 现在' },
  { id: 'short',    l: '短弯节奏',  range: '2025-02 — 现在' },
];

/* ============================================================
   RooyLensSheet — 04a · 历程回顾
   ───────────────────────────────────────────────────────────
   Rebuilt against iOS HIG + ui-ux-pro-max checklist:
     - Scrim opacity 0.45 (HIG: 40–60% black for modal scrim)
     - Sheet corner radius 20 (iOS 17 sheet style)
     - 36×5pt drag handle (Apple system spec)
     - Header: 56pt, navigation-bar-style left-Done button +
       centered 17pt semibold title + trailing toggle
     - iOS Search Field: 36pt, systemFill background
       rgba(120,120,128,0.12), 15pt callout text, 13pt placeholder
     - Month strip: iOS Camera-style underline tabs, callout 16pt,
       active = bold + 2pt underline
     - Day cells: 44×44pt circular highlight, 17pt regular,
       record-dot 4pt under, weekday eyebrow above (HIG calendar)
     - List rows: 60pt minHeight, separator inset 56pt, SF-Symbol-
       style 22pt glyph leading, headline title + subhead meta,
       trailing date (monospaced) + 11pt chevron
     - Single "只看总结" toggle replaces 4-pill filter row,
       living in the trailing nav-bar slot (iOS Mail "Filter" UX)
   ============================================================ */

const WEEKDAYS = ['日', '一', '二', '三', '四', '五', '六'];
/* Anchor a calendar day-of-week mapping. 2025-11-01 was a Saturday;
   we treat the season as if it starts there so weekday rendering
   is deterministic across the strip without needing a real Date. */
const SEASON_ANCHOR = { m: 11, dayOfWeek: 6 /* Sat */ };
function weekdayFor(m, d) {
  /* months in season order: 11,12,1,2,3,4,5 — accumulate days */
  const order = [11, 12, 1, 2, 3, 4, 5];
  let offset = 0;
  for (const mm of order) {
    if (mm === m) break;
    offset += LENS_MONTH_DAYS[mm];
  }
  return (SEASON_ANCHOR.dayOfWeek + offset + (d - 1)) % 7;
}

/* `embedded` prop: when true (used by prototype/app.jsx), the
   sheet body fills its parent container and StatusBar + the
   conversation peek + the self-rendered scrim are all skipped —
   the prototype's stack-based router supplies a real 04 RooyScreen
   underneath, and supplies its own backdrop + slide-up transition.
   When false (Design Canvas), the component stays self-contained
   so it can be previewed as a standalone 390×844 artboard. */
function RooyLensSheet({ onNav, embedded }) {
  const [season, setSeason] = React.useState(LENS_SEASONS[0]);
  const [showSummary, setShowSummary] = React.useState(false);
  const [keyword, setKeyword] = React.useState('');
  const [jumpOpen, setJumpOpen] = React.useState(false);
  const [courseOpen, setCourseOpen] = React.useState(false);
  const [selectedCourse, setSelectedCourse] = React.useState(null);
  /* displayMonth — last-jumped month; powers the date chip's label.
     Defaults to the latest month with content. */
  const [displayMonth, setDisplayMonth] = React.useState(null);
  const listRef = React.useRef(null);
  const sectionRefs = React.useRef({});

  const grouped = React.useMemo(() => {
    const k = keyword.trim();
    const matches = (it) => {
      if (it.season !== season) return false;
      if (!k) return true;
      return it.place.includes(k)
        || (it.lesson && it.lesson.includes(k))
        || (showSummary && it.summary && it.summary.includes(k));
    };
    const list = LENS_ITEMS.filter(matches);
    const monthOrder = [5, 4, 3, 2, 1, 12, 11];
    const out = [];
    for (const m of monthOrder) {
      const inMonth = list.filter(it => it.m === m);
      if (inMonth.length) out.push({ m, items: inMonth });
    }
    return out;
  }, [season, keyword, showSummary]);

  const totalCount = grouped.reduce((acc, g) => acc + g.items.length, 0);
  const availableMonths = grouped.map(g => g.m);

  const jumpTo = (s, m) => {
    setJumpOpen(false);
    setDisplayMonth(m);
    if (s !== season) {
      /* picking a month in another season switches season first;
         the section will be in view once that re-render lands */
      setSeason(s);
      return;
    }
    const el = sectionRefs.current[m];
    if (!el || !listRef.current) return;
    listRef.current.scrollTo({ top: el.offsetTop - 4, behavior: 'smooth' });
  };

  /* derive the date-chip label: prefer user-picked month, else latest
     month with content, else fallback */
  const dateLabel = (() => {
    const m = displayMonth ?? (grouped[0] && grouped[0].m);
    return m ? `${m} 月` : '时间';
  })();

  return (
    <div style={{
      height: '100%', display: 'flex', flexDirection: 'column',
      background: embedded ? 'transparent' : R.bg,
      position: 'relative', color: R.ink, ...T.subhead,
    }}>
      {!embedded && <>
      <StatusBar/>

      {/* Conversation peek */}
      <div style={{
        position: 'relative', padding: '4px 16px 12px',
        opacity: 0.6, pointerEvents: 'none',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
          <div style={{
            width: 38, height: 38, borderRadius: '50%',
            background: R.ink, display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <span style={{ ...N.hero, fontSize: 15, color: '#fff' }}>R</span>
          </div>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
            <p style={{ ...T.headline, color: R.ink, lineHeight: 1 }}>ROOY</p>
            <p style={{ ...T.footnote, color: R.rooy, fontWeight: 500 }}>· 教学中</p>
          </div>
        </div>
      </div>
      </>}

      {/* iOS sheet with HIG-conforming scrim — scrim is dropped in
          embedded mode because the prototype router puts a real
          backdrop in front of the live RooyScreen underneath. */}
      <div style={{
        flex: 1, position: 'relative',
        background: embedded ? 'transparent' : Scrim.background,
      }}>
        <div style={{
          position: 'absolute', left: 0, right: 0, bottom: 0,
          top: embedded ? 0 : 56,
          background: R.bg,
          borderRadius: '20px 20px 0 0',
          display: 'flex', flexDirection: 'column',
          overflow: 'hidden',
        }}>
          {/* Drag handle */}
          <div style={{ display: 'flex', justifyContent: 'center', paddingTop: 8, flexShrink: 0 }}>
            <div style={{ width: 36, height: 5, borderRadius: 2.5, background: R.hair2 }}/>
          </div>

          {/* iOS Nav bar: centered title only — drag handle 已经承担
              "下滑关闭"信号, "取消"按钮 chrome 冗余, 已删除.
              Filter affordances 在下方 chip row. */}
          <div style={{
            height: 44, padding: '0 8px', flexShrink: 0,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
            <p style={{
              ...T.headline, color: R.ink, letterSpacing: '-0.01em',
            }}>消息回顾</p>
          </div>

          {/* iOS Search field — clean, single-purpose. */}
          <div style={{ padding: '4px 16px 8px', flexShrink: 0 }}>
            <div style={{
              background: R.systemFill, borderRadius: 10,
              height: 36, padding: '0 8px',
              display: 'flex', alignItems: 'center', gap: 4,
            }}>
              <svg width="15" height="15" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
                <circle cx="7" cy="7" r="5" stroke={R.ink3} strokeWidth="1.6"/>
                <path d="M11 11l3 3" stroke={R.ink3} strokeWidth="1.6" strokeLinecap="round"/>
              </svg>
              <input
                value={keyword}
                onChange={(e) => setKeyword(e.target.value)}
                placeholder="搜索"
                style={{
                  ...T.subhead, color: R.ink, flex: 1,
                  background: 'transparent', border: 'none', outline: 'none',
                  padding: '0 4px', minWidth: 0,
                }}
              />
              {keyword && (
                <button onClick={() => setKeyword('')} aria-label="清除" style={{
                  width: 17, height: 17, borderRadius: '50%',
                  background: R.hair2, border: 'none', cursor: 'pointer',
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  flexShrink: 0,
                }}>
                  <svg width="8" height="8" viewBox="0 0 8 8">
                    <path d="M1.5 1.5l5 5M6.5 1.5l-5 5" stroke="#fff" strokeWidth="1.4" strokeLinecap="round"/>
                  </svg>
                </button>
              )}
            </div>
          </div>

          {/* Filter chip row — 时间 · 课程 · 显示总结.
              Each chip is its own affordance with active-state ink fill.
              Layout uses flex-wrap so popovers below aren't clipped by
              an overflow:auto container. */}
          <div style={{
            padding: '2px 16px 10px', flexShrink: 0,
            display: 'flex', gap: 8, flexWrap: 'wrap',
          }}>
            <LensDateChip
              open={jumpOpen}
              onOpen={() => { setJumpOpen(o => !o); setCourseOpen(false); }}
              label={dateLabel}
              seasons={LENS_SEASONS}
              season={season}
              onSeasonChange={setSeason}
              recordedByseason={LENS_ITEMS.reduce((acc, it) => {
                acc[it.season] = acc[it.season] || new Set();
                acc[it.season].add(it.m);
                return acc;
              }, {})}
              onPick={jumpTo}
            />
            <LensCourseChip
              open={courseOpen}
              onOpen={() => { setCourseOpen(o => !o); setJumpOpen(false); }}
              courses={LENS_COURSES}
              selected={selectedCourse}
              onSelect={(c) => { setSelectedCourse(c); setCourseOpen(false); }}
            />
            {/* 显示总结是 toggle（语义和前两个 dropdown 不同）。
                variant="toggle" → 透明 outline 样式 + ROOY 绿 active，
                跟 filter chip 视觉上拉开档次。marginLeft auto 推到行尾
                做语义分组的视觉区分. */}
            <div style={{ marginLeft: 'auto' }}>
              <FilterChip
                variant="toggle"
                icon={
                  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
                    <rect x="3" y="2.5" width="10" height="11" rx="1.8"
                      stroke="currentColor" strokeWidth="1.5" fill="none"/>
                    <path d="M5.5 6h5M5.5 8.5h5M5.5 11h3"
                      stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/>
                  </svg>
                }
                label="显示总结"
                active={showSummary}
                onClick={() => setShowSummary(v => !v)}
              />
            </div>
          </div>

          {/* List — cards, grouped by month with sticky headers */}
          <div ref={listRef} style={{
            flex: 1, overflowY: 'auto',
            paddingBottom: 28, position: 'relative',
          }}>
            {totalCount === 0 ? (
              <div style={{ padding: '60px 24px', textAlign: 'center' }}>
                <p style={{ ...T.body, color: R.ink3 }}>没有匹配的记录</p>
                {keyword && (
                  <button onClick={() => setKeyword('')} style={{
                    marginTop: 12, ...T.body, color: R.ink, fontWeight: 500,
                    background: 'transparent', border: 'none', cursor: 'pointer', minHeight: 44, padding: 8,
                  }}>清除搜索</button>
                )}
              </div>
            ) : (
              grouped.map((g) => (
                <div key={g.m} ref={(el) => { sectionRefs.current[g.m] = el; }}>
                  <SectionHeader year={yearFor(season, g.m)} month={g.m}/>
                  <div style={{ padding: '8px 0 4px' }}>
                    {g.items.map((it) => (
                      <LensDayCard
                        key={it.id} item={it}
                        showSummary={showSummary}
                      />
                    ))}
                  </div>
                </div>
              ))
            )}
          </div>
        </div>
      </div>
    </div>
  );
}

/* SectionHeader — iOS sticky group header with weight + scale.
   Month leads (17pt semibold ink, the thing the eye finds first),
   year trails baseline-aligned in 13pt mono ink3 — small enough
   to read as context, present enough to disambiguate across the
   season boundary (Nov 2025 vs Nov 2024). */
function SectionHeader({ year, month }) {
  return (
    <div style={{
      position: 'sticky', top: 0, zIndex: 2,
      background: 'color-mix(in srgb, var(--r-bg) 92%, transparent)',
      backdropFilter: 'saturate(180%) blur(20px)',
      WebkitBackdropFilter: 'saturate(180%) blur(20px)',
      padding: '14px 20px 8px',
      display: 'flex', alignItems: 'baseline', gap: 6,
    }}>
      <span style={{ ...T.subhead, color: R.ink2, fontWeight: 500 }}>{month} 月</span>
      <span style={{ ...T.footnote, ...N.reg, color: R.ink3 }}>{year}</span>
    </div>
  );
}




/* FilterChip — generic iOS-pill filter affordance.
   Two variants:
   - 'filter' (default): systemFill grey → ink fill on active. Pair with
     hasDropdown for chips that open a popover (iOS Mail filter bar 风格)。
   - 'toggle': transparent + hairline outline → ink fill on active.
     Used for view options (e.g. 显示总结)，inactive 时跟 filter 拉开
     archive（outline vs filled），active 时统一进入 ink 主导态。
   hasDropdown adds a small caret to suggest a popover follows. */
function FilterChip({ icon, label, active, onClick, hasDropdown, ariaLabel, variant = 'filter' }) {
  const isToggle = variant === 'toggle';
  const bg = isToggle
    ? (active ? R.ink : 'transparent')
    : (active ? R.ink : 'rgba(118,118,128,0.14)');
  const fg = active ? '#fff' : R.ink;
  const border = isToggle
    ? `0.5px solid ${active ? R.ink : R.hair2}`
    : 'none';
  return (
    <button onClick={onClick}
      aria-label={ariaLabel || label}
      aria-pressed={active}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 5,
        padding: '7px 12px', minHeight: 32,
        borderRadius: 999,
        background: bg,
        color: fg,
        border,
        cursor: 'pointer',
        ...T.footnote, fontWeight: active ? 600 : 400,
        whiteSpace: 'nowrap', flexShrink: 0,
        transition: 'background .15s, color .15s, border-color .15s',
      }}>
      {icon}
      <span>{label}</span>
      {hasDropdown && (
        <svg width="10" height="7" viewBox="0 0 9 6"
          style={{ marginLeft: 1, opacity: 0.7 }}>
          <path d="M1 1l3.5 3.5L8 1"
            stroke="currentColor" strokeWidth="1.5"
            fill="none" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      )}
    </button>
  );
}

/* LensDateChip — date jump chip + popover.
   Triggers MonthJump-style popover anchored LEFT. The chip's label
   shows the current focused month (e.g. "5 月"); icon is permanent. */
function LensDateChip({ open, onOpen, label, seasons, season, onSeasonChange, recordedByseason, onPick }) {
  const popRef = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (popRef.current && !popRef.current.contains(e.target)) onOpen();
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open, onOpen]);

  const cal = (
    <svg width="14" height="14" viewBox="0 0 20 20" fill="none" style={{ flexShrink: 0 }}>
      <rect x="3" y="5" width="14" height="12.5" rx="2.5"
        stroke="currentColor" strokeWidth="1.6" fill="none"/>
      <path d="M3 9h14" stroke="currentColor" strokeWidth="1.6"/>
      <path d="M7 3v3M13 3v3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/>
    </svg>
  );

  return (
    <div ref={popRef} style={{ position: 'relative', flexShrink: 0 }}>
      <FilterChip icon={cal} label={label || '时间'} active={open}
        hasDropdown onClick={onOpen}/>
      {open && (
        <MonthJumpPanel
          anchor="left"
          seasons={seasons}
          season={season}
          onSeasonChange={onSeasonChange}
          recordedByseason={recordedByseason}
          onPick={onPick}
        />
      )}
    </div>
  );
}

/* LensCourseChip — course filter chip + popover.
   Popover lists mock LENS_COURSES with their date-range labels.
   selected: null | course object. */
function LensCourseChip({ open, onOpen, courses, selected, onSelect }) {
  const popRef = React.useRef(null);
  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (popRef.current && !popRef.current.contains(e.target)) onOpen();
    };
    document.addEventListener('mousedown', onDoc);
    return () => document.removeEventListener('mousedown', onDoc);
  }, [open, onOpen]);

  const book = (
    <svg width="14" height="14" viewBox="0 0 16 16" fill="none" style={{ flexShrink: 0 }}>
      <path d="M3 3.5h7a2 2 0 012 2V13a1.5 1.5 0 00-1.5-1.5H3z"
        stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinejoin="round"/>
      <path d="M3 3.5v9.5a1.5 1.5 0 011.5-1.5H3" stroke="currentColor" strokeWidth="1.5"
        fill="none" strokeLinejoin="round"/>
    </svg>
  );

  return (
    <div ref={popRef} style={{ position: 'relative', flexShrink: 0 }}>
      <FilterChip icon={book}
        label={selected ? selected.l : '课程'}
        active={!!selected || open}
        hasDropdown onClick={onOpen}/>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 6px)', left: 0, zIndex: 20,
          background: R.card, borderRadius: B.card,
          boxShadow: Shadow.modal,
          width: 240, padding: '6px 0',
        }}>
          <CourseRow l="全部课程" range="" picked={!selected}
            onClick={() => onSelect(null)} first/>
          {courses.map((c) => (
            <CourseRow key={c.id} l={c.l} range={c.range}
              picked={selected && selected.id === c.id}
              onClick={() => onSelect(c)}/>
          ))}
        </div>
      )}
    </div>
  );
}

function CourseRow({ l, range, picked, onClick, first }) {
  return (
    <button onClick={onClick} style={{
      width: '100%', textAlign: 'left',
      padding: '10px 14px',
      background: 'transparent', border: 'none', cursor: 'pointer',
      borderTop: first ? 'none' : `0.5px solid ${R.hair}`,
      display: 'flex', alignItems: 'center', gap: 10,
      fontFamily: '"PingFang SC", -apple-system, system-ui',
    }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <p style={{ ...T.subhead, color: R.ink }}>{l}</p>
        {range && (
          <p style={{ ...T.footnote, color: R.ink3, marginTop: 2 }}>{range}</p>
        )}
      </div>
      {picked && (
        <svg width="14" height="11" viewBox="0 0 14 11" style={{ flexShrink: 0 }}>
          <path d="M1 5.5l4 4L13 1" stroke={R.ink} strokeWidth="2"
            fill="none" strokeLinecap="round" strokeLinejoin="round"/>
        </svg>
      )}
    </button>
  );
}

/* MonthJumpPanel — extracted popover content from MonthJump so it can
   be triggered by chip elsewhere. Anchor: 'left' | 'right'. */
function MonthJumpPanel({ anchor = 'right', seasons, season, onSeasonChange, recordedByseason, onPick }) {
  const [draftSeason, setDraftSeason] = React.useState(season);
  React.useEffect(() => { setDraftSeason(season); }, [season]);

  const idx = seasons.indexOf(draftSeason);
  const canPrev = idx < seasons.length - 1;
  const canNext = idx > 0;
  const recorded = (recordedByseason && recordedByseason[draftSeason]) || new Set();

  return (
    <div style={{
      position: 'absolute', top: 'calc(100% + 6px)',
      [anchor]: 0, zIndex: 20,
      background: R.card, borderRadius: B.card,
      boxShadow: Shadow.modal,
      width: 268, padding: '10px 12px 12px',
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        marginBottom: 10, padding: '0 2px',
      }}>
        <button
          onClick={() => canPrev && setDraftSeason(seasons[idx + 1])}
          disabled={!canPrev}
          aria-label="上一雪季"
          style={{
            width: 28, height: 28, borderRadius: '50%',
            background: 'transparent', border: 'none',
            cursor: canPrev ? 'pointer' : 'default',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            opacity: canPrev ? 1 : 0.25,
          }}>
          <svg width="8" height="13" viewBox="0 0 8 13">
            <path d="M7 1L1 6.5l6 5.5" stroke={R.ink} strokeWidth="1.6"
              fill="none" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
        <span style={{
          ...T.subhead, color: R.ink, fontWeight: 500, letterSpacing: '-0.01em',
        }}>{draftSeason} 雪季</span>
        <button
          onClick={() => canNext && setDraftSeason(seasons[idx - 1])}
          disabled={!canNext}
          aria-label="下一雪季"
          style={{
            width: 28, height: 28, borderRadius: '50%',
            background: 'transparent', border: 'none',
            cursor: canNext ? 'pointer' : 'default',
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            opacity: canNext ? 1 : 0.25,
          }}>
          <svg width="8" height="13" viewBox="0 0 8 13">
            <path d="M1 1l6 5.5L1 12" stroke={R.ink} strokeWidth="1.6"
              fill="none" strokeLinecap="round" strokeLinejoin="round"/>
          </svg>
        </button>
      </div>
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
        gap: 4, padding: '0 2px',
      }}>
        {LENS_MONTHS.map(m => {
          const has = recorded.has(m);
          return (
            <button key={m}
              disabled={!has}
              onClick={() => has && onPick(draftSeason, m)}
              style={{
                aspectRatio: '1', borderRadius: 10,
                background: has ? 'rgba(118,118,128,0.10)' : 'transparent',
                border: 'none',
                cursor: has ? 'pointer' : 'default',
                ...T.footnote, fontWeight: 500, color: has ? R.ink : R.ink4,
                opacity: has ? 1 : 0.4,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}>{m}月</button>
          );
        })}
      </div>
    </div>
  );
}

/* LensDayCard — white rounded card.
   Layout mirrors the original Day row (Image #8 / #10): 56×56 date
   pill on the left (月 + day), place title at T.headline, single
   meta line uniformly separated by "|" — "8.4 km | 2:18 h | 立刃
   路径 · 第 3 节" (inner "·" inside the lesson topic itself stays).
   When the global "显示总结" switch is on AND this session has a
   ROOY note, the card extends downward to reveal that note —
   hairline-separated, green ROOY dot + body. */
function LensDayCard({ item, showSummary, onClick }) {
  const expanded = showSummary && !!item.summary;
  const clickable = !!onClick;
  const Inner = clickable ? 'button' : 'div';
  return (
    <div style={{
      margin: '0 16px 10px',
      background: R.card, borderRadius: B.cardL,
      boxShadow: '0 1px 3px rgba(14,17,22,0.05), 0 0.5px 1px rgba(14,17,22,0.04)',
      overflow: 'hidden',
    }}>
      <Inner onClick={onClick} style={{
        width: '100%', textAlign: 'left',
        background: 'transparent', border: 'none',
        cursor: clickable ? 'pointer' : 'default',
        padding: '12px 14px',
        display: 'flex', gap: 14, alignItems: 'center',
      }}>
        <div style={{
          position: 'relative',
          width: 56, height: 56, borderRadius: B.card,
          background: R.systemFill,
          display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'center',
          flexShrink: 0,
        }}>
          <span style={{
            ...T.footnote, color: R.ink3, lineHeight: 1,
          }}>{item.m} 月</span>
          <span style={{
            ...T.h2, color: R.ink, marginTop: 4, lineHeight: 1,
          }}>{item.day}</span>
        </div>

        <div style={{ flex: 1, minWidth: 0 }}>
          <p style={{ ...T.headline, color: R.ink, letterSpacing: '-0.01em' }}>{item.place}</p>
          <p style={{
            ...T.footnote, color: R.ink3, marginTop: 6,
            display: 'flex', alignItems: 'center', gap: 8,
            whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
          }}>
            <span>{item.km} km</span>
            {item.lesson && (
              <>
                <span style={{ color: R.ink4 }}>|</span>
                <span style={{
                  color: R.rooy, fontWeight: 500,
                  overflow: 'hidden', textOverflow: 'ellipsis', minWidth: 0,
                }}>{item.lesson}</span>
              </>
            )}
          </p>
        </div>

        {clickable && <Chevron style={{ flexShrink: 0 }}/>}
      </Inner>

      {expanded && (
        <div>
          <div style={{ height: 0.5, background: R.hair, marginLeft: 14, marginRight: 14 }}/>
          <div style={{ padding: '12px 14px 14px' }}>
            <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
              <div style={{ paddingTop: 7, flexShrink: 0 }}>
                <Dot size={6} color={R.rooy}/>
              </div>
              <p style={{
                ...T.body, color: R.ink2, flex: 1,
              }}>{item.summary}</p>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

Object.assign(window, {
  RooyScreen, RooyLensSheet,
  LensDayCard,
  FilterChip, LensDateChip, LensCourseChip, MonthJumpPanel, CourseRow,
  SectionHeader,
  ChatInput, ChatActionSheet, VoiceOverlay, LessonInviteCard,
  MicGlyph, CameraGlyph, VideoGlyph, LibraryGlyph,
});
