// components.jsx  v8  —  共用 UI 组件库：Sidebar · AthleteHeader · MetricGroupCard · KpiTile · Modal 等
// 职责：无业务逻辑的展示/交互组件，全 app 复用
// 依赖：无内部依赖 · 由所有视图文件引用

const { useState, useEffect, useMemo, useRef, useCallback } = React;

// ──────────────────────────────────────────────────────────────────────────
// Icons (small inline SVGs)
// ──────────────────────────────────────────────────────────────────────────
const Icon = ({ name, size = 14, stroke = 1.6 }) => {
  const paths = {
    search:   <><circle cx="11" cy="11" r="7"/><path d="m20 20-3-3"/></>,
    arrowUp:  <path d="M12 5v14M6 11l6-6 6 6"/>,
    arrowDown:<path d="M12 19V5M6 13l6 6 6-6"/>,
    minus:    <path d="M5 12h14"/>,
    plus:     <path d="M12 5v14M5 12h14"/>,
    x:        <path d="M6 6l12 12M18 6 6 18"/>,
    chevDown: <path d="m6 9 6 6 6-6"/>,
    chevRight:<path d="m9 6 6 6-6 6"/>,
    download: <><path d="M12 3v12"/><path d="m7 10 5 5 5-5"/><path d="M5 21h14"/></>,
    users:    <><circle cx="9" cy="8" r="4"/><path d="M3 21c0-3.3 2.7-6 6-6s6 2.7 6 6"/><circle cx="17" cy="9" r="3"/><path d="M15 15c2.5 0 5 1.5 5 5"/></>,
    flag:     <><path d="M4 21V4"/><path d="M4 4h14l-3 4 3 4H4"/></>,
    trend:    <><path d="M3 17l6-6 4 4 8-8"/><path d="M14 7h7v7"/></>,
    history:  <><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></>,
    sliders:  <><path d="M4 6h16M4 12h16M4 18h16"/><circle cx="9" cy="6" r="2" fill="currentColor" stroke="none"/><circle cx="15" cy="12" r="2" fill="currentColor" stroke="none"/><circle cx="7" cy="18" r="2" fill="currentColor" stroke="none"/></>,
    target:   <><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1" fill="currentColor" stroke="none"/></>,
    print:    <><path d="M7 8V3h10v5"/><path d="M7 16h10v5H7z"/><rect x="3" y="8" width="18" height="8" rx="1"/></>,
    refresh:  <><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/><path d="M3 21v-5h5"/></>,
    bolt:     <path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/>,
    star:     <path d="m12 2 3 7 7 1-5 5 1 7-6-3-6 3 1-7-5-5 7-1 3-7z"/>,
    info:     <><circle cx="12" cy="12" r="9"/><path d="M12 11v5"/><circle cx="12" cy="8" r=".5" fill="currentColor" stroke="none"/></>,
  };
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
         stroke="currentColor" strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round">
      {paths[name]}
    </svg>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// Logo / Brand
// ──────────────────────────────────────────────────────────────────────────
const BrandMark = () => (
  <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
    <div style={{
      width: 26, height: 26, borderRadius: 7,
      background: 'linear-gradient(135deg, #3b82f6, #1d4ed8)',
      display: 'grid', placeItems: 'center',
      boxShadow: '0 1px 0 rgba(15,23,42,.09) inset, 0 2px 8px rgba(59,130,246,.35)',
    }}>
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
        <path d="M4 18 L9 12 L13 15 L20 6"/>
        <circle cx="9" cy="12" r="1.4" fill="white" stroke="none"/>
        <circle cx="13" cy="15" r="1.4" fill="white" stroke="none"/>
      </svg>
    </div>
    <div style={{ display: 'flex', flexDirection: 'column', lineHeight: 1.1 }}>
      <div style={{ fontWeight: 600, fontSize: 13, letterSpacing: '.01em' }}>Axis Performance</div>
      <div style={{ fontSize: 10, color: 'var(--muted)', letterSpacing: '.06em', textTransform: 'uppercase' }}>Squad Analytics</div>
    </div>
  </div>
);

// ──────────────────────────────────────────────────────────────────────────
// FilterRow — labeled chip row used by the sidebar's cascading filters.
// Active chip uses the accent; inactive chips use neutral border.
// ──────────────────────────────────────────────────────────────────────────
const FilterRow = ({ label, options, value, onChange }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
    <div style={{
      fontSize: 9, color: 'var(--muted)',
      letterSpacing: '.12em', textTransform: 'uppercase', fontWeight: 600,
      paddingLeft: 1,
    }}>{label}</div>
    <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
      {options.map(o => (
        <button key={o} onClick={() => onChange(o)}
          style={{
            padding: '3px 8px', borderRadius: 4,
            background: value === o ? 'var(--ink)' : 'transparent',
            border: '1px solid',
            borderColor: value === o ? 'var(--ink)' : 'var(--border)',
            color: value === o ? '#fff' : 'var(--text-2)',
            font: '11px var(--font-sans)', fontWeight: value === o ? 600 : 400,
            cursor: 'pointer', whiteSpace: 'nowrap',
            transition: 'all .12s',
          }}>
          {o}
        </button>
      ))}
    </div>
  </div>
);

// ──────────────────────────────────────────────────────────────────────────
// Sidebar (athlete list + search + compare toggle)
// ──────────────────────────────────────────────────────────────────────────
// Tiny risk dot for sidebar (P3-4): 6px circle, color-coded by risk level
const SIDEBAR_RISK_COLORS = { green: '#10b981', amber: '#f59e0b', red: '#ef4444' };
const SidebarRiskDot = ({ level }) => {
  if (!level || level === 'gray') return null;
  const color = SIDEBAR_RISK_COLORS[level] || 'transparent';
  return (
    <div style={{
      width: 6, height: 6, borderRadius: '50%', background: color,
      flexShrink: 0, boxShadow: `0 0 4px ${color}99`,
    }} title={level === 'red' ? '风险' : level === 'amber' ? '注意' : '正常'} />
  );
};

// 全局顶部功能导航（方案 I）：横向铺开所有功能页，始终可见。
// 作为 .app grid 的整宽首行（gridColumn:1/-1）插入，不改既有滚动模型。
// ── nav model — shared by NavRail (desktop) + BottomNav (mobile ≤760px) ──────
const FORCE_NAV = ['cmj', 'sj', 'imtp', 'cmj-session', 'cmj-longitudinal', 'sj-longitudinal', 'imtp-longitudinal', 'force-compare', 'cmj-compare', 'dsi'];
const INJURY_NAV = ['injury', 'movement-screen'];
const NAV_ICON_PATHS = {
  team:         <React.Fragment><circle cx="9" cy="7" r="3.2"/><path d="M2.5 20c0-3.5 3-5.8 6.5-5.8s6.5 2.3 6.5 5.8"/><circle cx="17.5" cy="7.5" r="2.4"/><path d="M16.5 14.2c2.4.3 4 2.1 4 4.8"/></React.Fragment>,
  individual:   <React.Fragment><circle cx="12" cy="8" r="3.4"/><path d="M5 20c0-3.6 3.1-6 7-6s7 2.4 7 6"/></React.Fragment>,
  cmj:          <path d="M3 18 Q8 4 12 10 Q16 16 21 4"/>,
  injury:       <path d="M12 3v6m0 0 3-2m-3 2-3-2M6 21a6 6 0 0 1 12 0"/>,
  rehab:        <React.Fragment><path d="M4 12h5l2-5 3 10 2-5h4"/><path d="M5 21h14"/></React.Fragment>,
  training:     <path d="M4 7h16M4 12h16M4 17h10"/>,
  report:       <React.Fragment><path d="M7 3h8l4 4v14H7z"/><path d="M15 3v5h5"/><path d="M10 13h7M10 17h5"/></React.Fragment>,
  'review-database': <React.Fragment><rect x="4" y="4" width="16" height="16" rx="3"/><path d="M8 9h8M8 13h8M8 17h5"/></React.Fragment>,
  'field-test': <React.Fragment><rect x="5" y="4" width="14" height="17" rx="2"/><path d="M9 4V3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1"/><path d="M9 13l2 2 4-4"/></React.Fragment>,
  calendar:     <React.Fragment><rect x="3" y="4" width="18" height="17" rx="2"/><path d="M3 9h18M8 2v4M16 2v4"/></React.Fragment>,
  norms:        <React.Fragment><path d="M5 4h11a3 3 0 0 1 3 3v13H8a3 3 0 0 1-3-3z"/><path d="M8 4v16M11 9h5M11 13h5"/></React.Fragment>,
  knowledge:    <React.Fragment><path d="M4 5.5A2.5 2.5 0 0 1 6.5 3H11v17H6.5A2.5 2.5 0 0 0 4 22z"/><path d="M20 5.5A2.5 2.5 0 0 0 17.5 3H13v17h4.5A2.5 2.5 0 0 1 20 22z"/></React.Fragment>,
  settings:     <React.Fragment><circle cx="12" cy="12" r="3"/><path d="M19.4 13a1.6 1.6 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.6 1.6 0 0 0-2.7 1.1V21a2 2 0 0 1-4 0v-.2A1.6 1.6 0 0 0 6.7 19l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1A1.6 1.6 0 0 0 3 13a2 2 0 0 1 0-4 1.6 1.6 0 0 0 1.6-2.6l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1A1.6 1.6 0 0 0 11 3a2 2 0 0 1 4 0 1.6 1.6 0 0 0 2.6 1.6l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1A1.6 1.6 0 0 0 21 11a2 2 0 0 1 0 4Z"/></React.Fragment>,
};
const navIcon = (id, size) => <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round">{NAV_ICON_PATHS[id]}</svg>;
function getNavItems(view) {
  return [
    { id: 'team',       zh: '团队',           en: 'Squad',     on: view === 'team' },
    { id: 'individual', zh: '个人',           en: 'Athlete',   on: view === 'individual' },
    { id: 'cmj',        zh: '测力台',         en: 'Force',     on: FORCE_NAV.includes(view) }, // FORCE-WS-1a (2026-07-10): single 测力台工作区 entry (opens window.ForceWorkspace)
    { id: 'injury',     zh: '损伤筛查',       en: 'Screening', on: INJURY_NAV.includes(view) },
    { id: 'training',   zh: '训练',           en: 'Training',  on: view === 'training' },
    { id: 'rehab',      zh: '康复训练',       en: 'Rehab',     on: view === 'rehab' },
    { id: 'report',     zh: '报告编辑',       en: 'Report Editor', on: view === 'report' },
    { id: 'review-database', zh: '审核',      en: 'Review',    on: view === 'review-database' },
    { id: 'field-test', zh: '测试录入',       en: 'Test Entry',on: view === 'field-test' },
    { id: 'calendar',   zh: '日历',           en: 'Calendar',  on: view === 'calendar' },
    { id: 'norms',      zh: '常模库',         en: 'Norm Library', on: view === 'norms' },
    { id: 'knowledge',  zh: '知识库',         en: 'Knowledge', on: view === 'knowledge' },
  ];
}

// ── NavRail — 左侧图标导航(对照 mockup .rail) ──────────────────
function NavRail({ view, onNav, onOpenSettings, lang }) {
  const en = lang === 'en';
  const items = getNavItems(view);
  const Item = ({ it, on, onClick }) => (
    <button type="button" className="navrail-link" onClick={onClick} title={en ? it.en : it.zh} aria-label={en ? it.en : it.zh}
      style={{ position: 'relative', width: 44, height: 40, borderRadius: 9, display: 'grid', placeItems: 'center', cursor: 'pointer', border: 0, background: on ? 'var(--accent-soft)' : 'transparent', color: on ? 'var(--accent)' : 'var(--muted)' }}>
      {on && <span style={{ position: 'absolute', left: -10, width: 3, height: 20, borderRadius: '0 3px 3px 0', background: 'var(--accent)' }}/>}
      {navIcon(it.id, 19)}
      <span className="navrail-lab">{en ? it.en : it.zh}</span>
    </button>
  );
  return (
    <nav className="navrail" style={{ position: 'fixed', left: 0, top: 0, bottom: 0, width: 56, zIndex: 70, background: 'var(--panel)', borderRight: '1px solid var(--border)', display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6, padding: '10px 0' }}>
      <div style={{ width: 30, height: 30, borderRadius: 8, background: 'var(--accent)', display: 'grid', placeItems: 'center', color: '#fff', fontWeight: 700, fontSize: 13, marginBottom: 4 }}>A</div>
      {items.map(it => <Item key={it.id} it={it} on={it.on} onClick={() => onNav(it.id)} />)}
      <div style={{ marginTop: 'auto' }}>
        <Item it={{ id: 'settings', zh: '设置', en: 'Settings' }} on={view === 'settings'} onClick={onOpenSettings} />
      </div>
    </nav>
  );
}

// ── BottomNav — 移动端底栏(≤760px;rail ↔ 底部 Tab,显示 5 个核心入口) ──────
function BottomNav({ view, onNav, lang }) {
  const en = lang === 'en';
  const items = getNavItems(view).slice(0, 5);
  return (
    <nav className="botnav">
      {items.map(it => (
        <button key={it.id} type="button" className={`botnav-link${it.on ? ' on' : ''}`} onClick={() => onNav(it.id)} aria-label={en ? it.en : it.zh}>
          {navIcon(it.id, 20)}
          <span>{en ? it.en : it.zh}</span>
        </button>
      ))}
    </nav>
  );
}

// 顶栏精简:导航已移入左 NavRail;这里仅留 移动端汉堡(运动员名单)+ 品牌名 + 右侧操作位
function AppTopNav({ onMenu, rightActions, showMenu = true }) {
  return (
    <div className="app-top-nav" style={{ gridColumn: '1 / -1', position: 'sticky', top: 0, zIndex: 60, height: 52, display: 'flex', alignItems: 'center', gap: 12, padding: '0 18px', background: 'var(--panel)', borderBottom: '1px solid var(--border)' }}>
      {showMenu && (
      <button type="button" className="app-menu-btn" onClick={() => onMenu && onMenu()} aria-label="运动员名单"
        style={{ alignItems: 'center', justifyContent: 'center', width: 34, height: 34, borderRadius: 8, border: '1px solid var(--border)', background: 'var(--panel-hi)', color: 'var(--text-2)', cursor: 'pointer', flexShrink: 0, padding: 0 }}>
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M3 6h18M3 12h18M3 18h18"/></svg>
      </button>
      )}
      <span style={{ fontSize: 13, fontWeight: 700, whiteSpace: 'nowrap', color: 'var(--text)' }}>Axis Performance</span>
      <div style={{ flex: 1 }} />
      {rightActions}
    </div>
  );
}

const Sidebar = ({ athletes, selectedId, compareIds, onSelect, onToggleCompare, compareMode, onCompareModeChange, view, onViewChange, onAddAthlete, riskMap = {}, collapsed = false, onToggleCollapse }) => {
  const [q, setQ] = useState('');
  const [sport, setSport] = useState('All');
  const [group, setGroup] = useState('All');
  const [filter, setFilter] = useState('All');
  const [filterOpen, setFilterOpen] = useState(false);
  // Distinct values for filter chips
  const sports = useMemo(() => ['All', ...Array.from(new Set(athletes.map(a => a.sport || 'Unassigned')))], [athletes]);
  // Group choices cascade off the selected sport
  const groups = useMemo(() => {
    const pool = sport === 'All' ? athletes : athletes.filter(a => (a.sport || 'Unassigned') === sport);
    return ['All', ...Array.from(new Set(pool.map(a => a.group || 'Unassigned')))];
  }, [athletes, sport]);
  // Positions cascade off sport+group
  const positions = useMemo(() => {
    let pool = athletes;
    if (sport !== 'All') pool = pool.filter(a => (a.sport || 'Unassigned') === sport);
    if (group !== 'All') pool = pool.filter(a => (a.group || 'Unassigned') === group);
    return ['All', ...Array.from(new Set(pool.map(a => a.position)))];
  }, [athletes, sport, group]);
  // Reset cascaded filters when the parent filter changes and the current value
  // is no longer in the new option list.
  useEffect(() => { if (!groups.includes(group))    setGroup('All'); }, [groups]);
  useEffect(() => { if (!positions.includes(filter)) setFilter('All'); }, [positions]);

  const filtered = athletes.filter(a => {
    if (sport  !== 'All' && (a.sport  || 'Unassigned') !== sport)  return false;
    if (group  !== 'All' && (a.group  || 'Unassigned') !== group)  return false;
    if (filter !== 'All' && a.position !== filter) return false;
    if (q && !(a.name + a.position + a.country + (a.sport||'') + (a.group||'')).toLowerCase().includes(q.toLowerCase())) return false;
    return true;
  });
  const teamActive       = view === 'team';
  const forceActive      = view === 'cmj' || view === 'sj' || view === 'imtp';
  // FORCE-WS-1a (2026-07-10): the Force Compare active-state flag + its sub-button
  // were removed — 对比 is now a mode inside the 测力台工作区 (window.ForceWorkspace),
  // reached via the single 测力台 rail entry. No dedicated sidebar sub-button.
  const dsiActive        = view === 'dsi';
  const calendarActive   = view === 'calendar';
  const fieldTestActive  = view === 'field-test';
  const normsActive      = view === 'norms';
  const filterActiveCount = [sport, group, filter].filter(v => v && v !== 'All').length;

  // S4 Style A — collapsed avatar strip (expand button + clickable avatars).
  if (collapsed) {
    return (
      <aside className="app-sidebar app-sidebar-mini" style={{
        background: 'var(--panel)', borderRight: '1px solid var(--border)',
        display: 'flex', flexDirection: 'column', alignItems: 'center',
        position: 'sticky', top: 52, height: 'calc(100vh - 52px)',
        gap: 7, padding: '10px 0', overflowY: 'auto',
      }}>
        <button type="button" onClick={onToggleCollapse} title="展开名单" aria-label="展开名单"
          style={{ width: 32, height: 32, borderRadius: 8, border: '1px solid var(--border)', background: 'var(--panel)', color: 'var(--muted)', display: 'grid', placeItems: 'center', cursor: 'pointer', marginBottom: 4, flexShrink: 0 }}>
          <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 6l6 6-6 6"/></svg>
        </button>
        {athletes.map(a => {
          const active = view !== 'team' && a.id === selectedId;
          return (
            <button key={a.id} type="button" onClick={() => onSelect(a.id)} title={`${a.name} · ${a.position}`}
              style={{ position: 'relative', border: active ? '2px solid var(--accent)' : '2px solid transparent', background: 'transparent', cursor: 'pointer', padding: 0, borderRadius: '50%', flexShrink: 0, lineHeight: 0 }}>
              <AvatarMini name={a.name} accent={active ? 'var(--ink-2)' : '#eaeef3'} />
            </button>
          );
        })}
      </aside>
    );
  }

  return (
    <aside className="app-sidebar" style={{
      background: 'var(--panel)',
      borderRight: '1px solid var(--border)',
      display: 'flex', flexDirection: 'column',
      position: 'sticky', top: 52, height: 'calc(100vh - 52px)',
    }}>
      {/* 品牌已移至 AppTopNav；功能导航已移至顶栏，这里隐藏（保留 DOM 以最小化改动）。 */}
      <div style={{ display: 'none' }}>
        <button
          onClick={() => onViewChange && onViewChange('team')}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            padding: '9px 10px', borderRadius: 6,
            background: teamActive ? 'rgba(15,23,42,.05)' : 'transparent',
            border: '1px solid',
            borderColor: teamActive ? 'var(--border-strong)' : 'transparent',
            cursor: 'pointer', textAlign: 'left',
            color: 'inherit', font: 'inherit',
          }}
          onMouseEnter={(e) => { if (!teamActive) e.currentTarget.style.background = 'var(--panel-2)'; }}
          onMouseLeave={(e) => { if (!teamActive) e.currentTarget.style.background = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: 6,
            background: teamActive ? 'var(--ink)' : 'var(--panel-hi)',
            display: 'grid', placeItems: 'center',
            border: '1px solid var(--border)',
            color: teamActive ? '#fff' : 'var(--muted)',
            flexShrink: 0,
          }}>
            <Icon name="users" size={14}/>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{
              fontSize: 13, fontWeight: 500,
              color: teamActive ? 'var(--text)' : 'var(--text-2)',
            }}>Squad Dashboard</div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>
              Team-wide · 3 lenses
            </div>
          </div>
        </button>

        {/* Force Tests — unified CMJ / SJ / IMTP */}
        <button
          onClick={() => onViewChange && onViewChange(forceActive ? view : 'cmj')}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            padding: '9px 10px', borderRadius: 6,
            background: forceActive ? 'rgba(15,23,42,.05)' : 'transparent',
            border: '1px solid',
            borderColor: forceActive ? 'var(--border-strong)' : 'transparent',
            cursor: 'pointer', textAlign: 'left',
            color: 'inherit', font: 'inherit',
          }}
          onMouseEnter={(e) => { if (!forceActive) e.currentTarget.style.background = 'var(--panel-2)'; }}
          onMouseLeave={(e) => { if (!forceActive) e.currentTarget.style.background = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: 6,
            background: forceActive ? 'var(--ink)' : 'var(--panel-hi)',
            display: 'grid', placeItems: 'center',
            border: '1px solid var(--border)',
            color: forceActive ? '#fff' : 'var(--muted)',
            flexShrink: 0,
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <path d="M3 18 Q8 4 12 10 Q16 16 21 4"/>
            </svg>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 500, color: forceActive ? 'var(--text)' : 'var(--text-2)' }}>{window.t('Force Plate Import')}</div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>CMJ · SJ · IMTP</div>
          </div>
        </button>

        {/* Field Testing — 场地测试批量录入 */}
        <button
          onClick={() => onViewChange && onViewChange('field-test')}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            padding: '9px 10px', borderRadius: 6,
            background: fieldTestActive ? 'rgba(15,23,42,.05)' : 'transparent',
            border: '1px solid',
            borderColor: fieldTestActive ? 'var(--border-strong)' : 'transparent',
            cursor: 'pointer', textAlign: 'left',
            color: 'inherit', font: 'inherit',
          }}
          onMouseEnter={(e) => { if (!fieldTestActive) e.currentTarget.style.background = 'var(--panel-2)'; }}
          onMouseLeave={(e) => { if (!fieldTestActive) e.currentTarget.style.background = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: 6,
            background: fieldTestActive ? 'var(--ink)' : 'var(--panel-hi)',
            display: 'grid', placeItems: 'center',
            border: '1px solid var(--border)',
            color: fieldTestActive ? '#fff' : 'var(--muted)',
            flexShrink: 0,
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <rect x="5" y="4" width="14" height="17" rx="2"/>
              <path d="M9 4V3a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v1"/>
              <path d="M9 13l2 2 4-4"/>
            </svg>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 500, color: fieldTestActive ? 'var(--text)' : 'var(--text-2)' }}>{window.t('Test Entry')}</div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>场地测试 · 批量录入</div>
          </div>
        </button>

        {/* Norms Library — 常模库 */}
        <button
          onClick={() => onViewChange && onViewChange('norms')}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            padding: '9px 10px', borderRadius: 6,
            background: normsActive ? 'rgba(15,23,42,.05)' : 'transparent',
            border: '1px solid',
            borderColor: normsActive ? 'var(--border-strong)' : 'transparent',
            cursor: 'pointer', textAlign: 'left',
            color: 'inherit', font: 'inherit',
          }}
          onMouseEnter={(e) => { if (!normsActive) e.currentTarget.style.background = 'var(--panel-2)'; }}
          onMouseLeave={(e) => { if (!normsActive) e.currentTarget.style.background = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: 6,
            background: normsActive ? 'var(--ink)' : 'var(--panel-hi)',
            display: 'grid', placeItems: 'center',
            border: '1px solid var(--border)',
            color: normsActive ? '#fff' : 'var(--muted)',
            flexShrink: 0,
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/>
            </svg>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 500, color: normsActive ? 'var(--text)' : 'var(--text-2)' }}>常模库</div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>Norms · 文献参考</div>
          </div>
        </button>

        {/* Testing Calendar */}
        <button
          onClick={() => onViewChange && onViewChange('calendar')}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            padding: '9px 10px', borderRadius: 6,
            background: calendarActive ? 'rgba(15,23,42,.05)' : 'transparent',
            border: '1px solid',
            borderColor: calendarActive ? 'var(--border-strong)' : 'transparent',
            cursor: 'pointer', textAlign: 'left',
            color: 'inherit', font: 'inherit',
          }}
          onMouseEnter={(e) => { if (!calendarActive) e.currentTarget.style.background = 'var(--panel-2)'; }}
          onMouseLeave={(e) => { if (!calendarActive) e.currentTarget.style.background = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: 6,
            background: calendarActive ? 'var(--ink)' : 'var(--panel-hi)',
            display: 'grid', placeItems: 'center',
            border: '1px solid var(--border)',
            color: calendarActive ? '#fff' : 'var(--muted)',
            flexShrink: 0,
          }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <rect x="3" y="4" width="18" height="18" rx="2"/>
              <line x1="3" y1="9" x2="21" y2="9"/>
              <line x1="8" y1="2" x2="8" y2="6"/>
              <line x1="16" y1="2" x2="16" y2="6"/>
              <line x1="7" y1="14" x2="9" y2="14"/>
              <line x1="12" y1="14" x2="14" y2="14"/>
            </svg>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 500, color: calendarActive ? 'var(--text)' : 'var(--text-2)' }}>Testing Calendar</div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>Schedules · 周期 · 参与人</div>
          </div>
        </button>

        {/* FORCE-WS-1a (2026-07-10): Force Compare sub-button removed — 对比 is a mode
            inside 测力台工作区, reached from the single 测力台 rail entry. */}

        {/* DSI Panel */}
        <button
          onClick={() => onViewChange && onViewChange('dsi')}
          style={{
            display: 'flex', alignItems: 'center', gap: 10,
            padding: '9px 10px', borderRadius: 6,
            background: dsiActive ? 'rgba(15,23,42,.05)' : 'transparent',
            border: '1px solid',
            borderColor: dsiActive ? 'var(--border-strong)' : 'transparent',
            cursor: 'pointer', textAlign: 'left',
            color: 'inherit', font: 'inherit',
          }}
          onMouseEnter={(e) => { if (!dsiActive) e.currentTarget.style.background = 'var(--panel-2)'; }}
          onMouseLeave={(e) => { if (!dsiActive) e.currentTarget.style.background = 'transparent'; }}
        >
          <div style={{
            width: 28, height: 28, borderRadius: 6,
            background: dsiActive ? 'linear-gradient(135deg,#f59e0b,#d97706)' : 'var(--panel-hi)',
            display: 'grid', placeItems: 'center',
            border: '1px solid var(--border)',
            color: dsiActive ? '#fff' : 'var(--muted)',
            flexShrink: 0,
          }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>
            </svg>
          </div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, fontWeight: 500, color: dsiActive ? 'var(--text)' : 'var(--text-2)' }}>力量剖析仪表板</div>
            <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>DSI · SSC · FVimb · 早期发力</div>
          </div>
        </button>
      </div>

      <div style={{
        padding: '6px 12px 4px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        fontSize: 10, textTransform: 'uppercase', letterSpacing: '.08em',
        color: 'var(--muted)',
      }}>
        <span>Athletes</span>
        <button type="button" onClick={onToggleCollapse} title="折叠名单" aria-label="折叠名单"
          style={{ width: 22, height: 22, borderRadius: 6, border: '1px solid var(--border)', background: 'var(--panel)', color: 'var(--muted)', display: 'grid', placeItems: 'center', cursor: 'pointer', flexShrink: 0 }}
          onMouseEnter={(e) => { e.currentTarget.style.background = 'var(--panel-hi)'; e.currentTarget.style.color = 'var(--text)'; }}
          onMouseLeave={(e) => { e.currentTarget.style.background = 'var(--panel)'; e.currentTarget.style.color = 'var(--muted)'; }}>
          <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 18l-6-6 6-6"/></svg>
        </button>
      </div>

      <div style={{ padding: '4px 14px 8px', display: 'flex', flexDirection: 'column', gap: 8 }}>
        <div style={{ display: 'flex', gap: 8 }}>
        <div style={{ position: 'relative', flex: 1 }}>
          <div style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: 'var(--muted)' }}>
            <Icon name="search" size={13}/>
          </div>
          <input
            value={q}
            onChange={(e) => setQ(e.target.value)}
            placeholder="Search athletes…"
            style={{
              width: '100%', padding: '8px 10px 8px 30px',
              background: 'var(--panel-2)', border: '1px solid var(--border)',
              borderRadius: 6, color: 'var(--text)', font: '13px var(--font-sans)',
              outline: 'none',
            }}
            onFocus={(e) => e.target.style.borderColor = 'var(--accent)'}
            onBlur={(e) => e.target.style.borderColor = 'var(--border)'}
          />
        </div>
          <button type="button" onClick={() => setFilterOpen(o => !o)} title="筛选"
            style={{
              flexShrink: 0, display: 'flex', alignItems: 'center', gap: 5, padding: '0 10px',
              background: filterActiveCount ? 'var(--ink)' : 'var(--panel)', color: filterActiveCount ? '#fff' : 'var(--text-2)',
              border: '1px solid var(--border)', borderRadius: 6, cursor: 'pointer', font: '12px var(--font-sans)', whiteSpace: 'nowrap',
            }}>
            筛选{filterActiveCount ? ' · ' + filterActiveCount : ''} ▾
          </button>
        </div>
        {filterOpen && (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, padding: 10, border: '1px solid var(--border)', borderRadius: 8, background: 'var(--panel-2)' }}>
            <FilterRow label="Sport"    options={sports}    value={sport}  onChange={setSport}/>
            {groups.length > 2    && <FilterRow label="Group"    options={groups}    value={group}  onChange={setGroup}/>}
            {positions.length > 2 && <FilterRow label="Position" options={positions} value={filter} onChange={setFilter}/>}
            {filterActiveCount > 0 && (
              <button type="button" onClick={() => { setSport('All'); setGroup('All'); setFilter('All'); }}
                style={{ alignSelf: 'flex-start', background: 'transparent', border: '1px solid var(--border)', color: 'var(--accent)', padding: '4px 10px', borderRadius: 5, fontSize: 11, cursor: 'pointer' }}>清空筛选</button>
            )}
          </div>
        )}
      </div>

      <div style={{
        padding: '6px 8px 8px 14px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8,
        color: 'var(--muted)', fontSize: 10, textTransform: 'uppercase', letterSpacing: '.08em',
      }}>
        <span>Squad · {filtered.length}{filtered.length !== athletes.length && <span style={{ color: 'var(--muted-2)', textTransform: 'none', letterSpacing: 0 }}> / {athletes.length}</span>}</span>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
          {onAddAthlete && (
            <button onClick={onAddAthlete} title="Add athlete (sport, group, position)"
              style={{
                display: 'inline-flex', alignItems: 'center', gap: 3,
                padding: '3px 7px', borderRadius: 4,
                background: 'transparent', border: '1px solid var(--border)',
                color: 'var(--text-2)', cursor: 'pointer',
                font: '10px var(--font-sans)', letterSpacing: '.08em', textTransform: 'uppercase',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--accent)'; e.currentTarget.style.color = 'var(--accent)'; }}
              onMouseLeave={(e) => { e.currentTarget.style.borderColor = 'var(--border)'; e.currentTarget.style.color = 'var(--text-2)'; }}>
              <Icon name="plus" size={9}/> New
            </button>
          )}
          <label style={{ display:'flex', alignItems:'center', gap: 6, cursor:'pointer', color: compareMode ? 'var(--accent)' : 'var(--muted)' }}>
            <input type="checkbox" checked={compareMode} onChange={(e) => onCompareModeChange(e.target.checked)} style={{ accentColor: 'var(--accent)' }}/>
            Compare
          </label>
        </div>
      </div>

      <div style={{ overflowY: 'auto', flex: 1, padding: '0 6px 14px' }}>
        {filtered.length === 0 && (
          <div style={{
            padding: '24px 16px', textAlign: 'center',
            color: 'var(--muted)', fontSize: 12,
          }}>
            <div style={{ marginBottom: 6 }}>No athletes match these filters.</div>
            <button onClick={() => { setQ(''); setSport('All'); setGroup('All'); setFilter('All'); }} style={{
              background: 'transparent', border: '1px solid var(--border)', color: 'var(--accent)',
              padding: '4px 10px', borderRadius: 4, fontSize: 11, cursor: 'pointer',
              letterSpacing: '.04em',
            }}>{window.t('Clear filters')}</button>
          </div>
        )}
        {filtered.map(a => {
          const isSelected = a.id === selectedId;
          const isCompared = compareIds.includes(a.id);
          const active = compareMode ? isCompared : (!teamActive && isSelected);
          return (
            <button key={a.id}
              onClick={() => compareMode ? onToggleCompare(a.id) : onSelect(a.id)}
              style={{
                width: '100%', display: 'flex', alignItems: 'center', gap: 10,
                padding: '8px 10px', marginBottom: 2,
                background: active ? 'rgba(15,23,42,.05)' : 'transparent',
                border: '1px solid',
                borderColor: active ? 'var(--border-strong)' : 'transparent',
                borderRadius: 6, cursor: 'pointer', textAlign: 'left',
                color: 'inherit', font: 'inherit',
                transition: 'background .12s',
              }}
              onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel-2)'; }}
              onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
            >
              <AvatarMini name={a.name} accent={active ? 'var(--ink-2)' : '#eaeef3'} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 6 }}>
                  <div style={{
                    fontWeight: 500, fontSize: 13,
                    color: active ? 'var(--text)' : 'var(--text-2)',
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                  }}>{a.name}</div>
                  <div className="mono" style={{ fontSize: 11, color: 'var(--muted)' }}>
                    #{String(a.jersey).padStart(2,'0')}
                  </div>
                </div>
                <div style={{
                  display: 'flex', alignItems: 'center', gap: 6,
                  fontSize: 11, color: 'var(--muted)', minHeight: 14,
                }}>
                  <span>{a.position}</span>
                  {a.group && (
                    <>
                      <span style={{ opacity: .5 }}>·</span>
                      <span style={{
                        fontSize: 9.5, letterSpacing: '.06em', textTransform: 'uppercase',
                        color: 'var(--text-2)',
                        padding: '1px 5px', borderRadius: 3,
                        background: 'var(--panel-hi)', fontWeight: 600,
                      }}>{a.group}</span>
                    </>
                  )}
                </div>
              </div>
              <SidebarRiskDot level={(riskMap[a.id] || {}).level} />
              <ScoreDot value={a.overall}/>
            </button>
          );
        })}
      </div>

      <div style={{ padding: '10px 14px', borderTop: '1px solid var(--border)', display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: 'var(--muted)', fontSize: 11 }}>
        <LivePulse label="Synced · 2 min ago"/>
        <span className="mono">v3.4</span>
      </div>
    </aside>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// Mini avatar — initials in a tinted circle
// ──────────────────────────────────────────────────────────────────────────
const AvatarMini = ({ name, accent = '#eaeef3', size = 28 }) => {
  const initials = name.split(' ').map(n => n[0]).slice(0,2).join('');
  return (
    <div style={{
      width: size, height: size, borderRadius: 999,
      background: `linear-gradient(135deg, ${accent} 0%, rgba(0,0,0,.4) 100%)`,
      display: 'grid', placeItems: 'center',
      fontSize: 11, fontWeight: 600, color: 'var(--text)',
      border: '1px solid var(--border)',
      flexShrink: 0,
    }}>{initials}</div>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// Score Dot — tiny circular progress
// ──────────────────────────────────────────────────────────────────────────
const ScoreDot = ({ value, size = 26, stroke = 3 }) => {
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const off = c * (1 - value / 100);
  const color = value >= 70 ? 'var(--pos)' : value >= 50 ? 'var(--accent-2)' : value >= 35 ? 'var(--warn)' : 'var(--neg)';
  return (
    <div style={{ position: 'relative', width: size, height: size, flexShrink: 0 }}>
      <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="rgba(15,23,42,.08)" strokeWidth={stroke}/>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={stroke}
          strokeDasharray={c} strokeDashoffset={off} strokeLinecap="round"
          transform={`rotate(-90 ${size/2} ${size/2})`}
          style={{ transition: 'stroke-dashoffset .5s cubic-bezier(.65,0,.35,1)' }}/>
      </svg>
      <div style={{
        position: 'absolute', inset: 0, display: 'grid', placeItems: 'center',
        fontSize: 10, fontWeight: 600, fontFamily: 'var(--font-mono)', color: 'var(--text)',
      }}>{value}</div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// Diff arrow — value plus colored up/down chevron
// ──────────────────────────────────────────────────────────────────────────
const DiffArrow = ({ value, direction = 'higher', formatter }) => {
  if (value == null || isNaN(value)) return <span style={{ color: 'var(--muted-2)' }}>—</span>;
  const isPositive = direction === 'higher' ? value > 0 : value < 0;
  const isNegative = direction === 'higher' ? value < 0 : value > 0;
  const isZero = Math.abs(value) < 0.005;
  const color = isZero ? 'var(--muted)' : isPositive ? 'var(--pos)' : 'var(--neg)';
  const arrow = isZero ? '–' : value > 0 ? '▲' : '▼';
  const display = formatter ? formatter(Math.abs(value)) : Math.abs(value).toFixed(2);
  return (
    <span className="mono" style={{ color, fontSize: 12, display: 'inline-flex', alignItems: 'center', gap: 4 }}>
      <span style={{ fontSize: 9 }}>{arrow}</span>
      {display}
    </span>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// AthleteHeader — top hero card
// ──────────────────────────────────────────────────────────────────────────
const AthleteHeader = ({ athlete, season, overallScore, posAvgScore, acwr, onPrint, riskLevel }) => {
  const heightM = ((athlete.height || 0) / 100);
  const bmi = heightM > 0 ? (athlete.weight / (heightM * heightM)).toFixed(1) : null;
  const resolvedAge = window.AthleteProfile?.resolveAge(athlete) ?? athlete.age;
  const ageDisplay = resolvedAge != null && !(typeof resolvedAge === 'string' && resolvedAge.trim() === '') && isFinite(Number(resolvedAge))
    ? Number(resolvedAge).toFixed(1)
    : '—';
  const birthDateDisplay = athlete.birthDate
    ? (window.formatDate ? window.formatDate(athlete.birthDate, 'short') : athlete.birthDate)
    : '—';
  return (
    <section className="athlete-story-profile-card">
      {/* Avatar column */}
      <div className="athlete-story-profile-avatar-col">
        <div className="athlete-story-profile-avatar" title={athlete.name}>
          <image-slot
            id={`photo-${athlete.id}`}
            shape="rect"
            placeholder="Drop photo"
            style={{ width: '100%', height: '100%', display: 'block' }}
          ></image-slot>
        </div>
        <div className="athlete-story-profile-jersey" title={`Jersey #${athlete.jersey}`}>#{String(athlete.jersey).padStart(2,'0')}</div>
      </div>

      {/* Info column */}
      <div className="athlete-story-profile-info">
        <div className="athlete-story-profile-eyebrow">
          Athlete Profile · {window.formatDate ? window.formatDate(season, 'short') : season}
        </div>
        <div className="athlete-story-profile-name-row">
          <div>
            <h1 className="athlete-story-profile-name" title={athlete.name}>{athlete.name}</h1>
            <div className="athlete-story-profile-subline" title={[athlete.position, athlete.dominant, athlete.country].filter(Boolean).join(' · ')}>
              {[athlete.position, athlete.dominant, athlete.country].filter(Boolean).join(' · ')}
            </div>
          </div>
          {riskLevel && <RiskBadge level={riskLevel.level} flags={riskLevel.flags}/>}
        </div>

        <div className="athlete-story-profile-grid">
          <div className="athlete-story-profile-mini-stat" title={`Gender ${athlete.gender || '—'}`}>
            <span>Gender / 性别</span><strong>{athlete.gender || '—'}</strong>
          </div>
          <div className="athlete-story-profile-mini-stat" title={`Birth date ${athlete.birthDate || '—'}`}>
            <span>Birth date / 出生日期</span><strong>{birthDateDisplay}</strong>
          </div>
          <div className="athlete-story-profile-mini-stat" title={`Age ${ageDisplay}`}>
            <span>{window.t('Age')}</span><strong>{ageDisplay}</strong>
          </div>
          <div className="athlete-story-profile-mini-stat" title={`Height ${athlete.height ?? '—'} cm`}>
            <span>{window.t('Height')}</span><strong>{athlete.height ?? '—'}<em> cm</em></strong>
          </div>
          <div className="athlete-story-profile-mini-stat" title={`Weight ${athlete.weight ?? '—'} kg`}>
            <span>{window.t('Weight')}</span><strong>{athlete.weight ?? '—'}<em> kg</em></strong>
          </div>
          <div className="athlete-story-profile-mini-stat" title={`Sport ${athlete.sport || '—'}`}>
            <span>Sport</span><strong>{athlete.sport || '—'}</strong>
          </div>
          <div className="athlete-story-profile-mini-stat" title={bmi ? `BMI ${bmi}` : 'BMI unavailable'}>
            <span>BMI</span><strong>{bmi ?? '—'}</strong>
          </div>
        </div>

        {/* 状态石 — 综合评分 / 位置均值 / 负荷 ACWR (定稿) */}
        <div className="athlete-story-profile-stones">
          {(() => {
            const acwrOk = acwr != null && isFinite(acwr);
            const acwrColor = !acwrOk ? 'var(--muted)' : (acwr > 1.5 ? 'var(--neg)' : acwr > 1.3 ? 'var(--warn)' : 'var(--pos)');
            const stones = [
              { k: '综合评分', v: overallScore != null ? Math.round(overallScore) : '—', sub: '队内', color: 'var(--text)' },
              { k: '位置均值', v: posAvgScore != null ? Math.round(posAvgScore) : '—', sub: athlete.position, color: 'var(--text)' },
              { k: '负荷 ACWR', v: acwrOk ? (+acwr).toFixed(2) : '—', sub: !acwrOk ? '待接负荷' : (acwr > 1.5 ? '偏高·风险' : acwr > 1.3 ? '偏高·留意' : '最优 1.0–1.3'), color: acwrColor },
            ];
            return stones.map((s, i) => (
              <div key={i} className="athlete-story-profile-stone" title={`${s.k}: ${s.v} (${s.sub})`}>
                <div className="athlete-story-profile-stone-label">{s.k}</div>
                <div className="mono athlete-story-profile-stone-value" style={{ color: s.color }}>{s.v}</div>
                <div className="athlete-story-profile-stone-sub">{s.sub}</div>
              </div>
            ));
          })()}
        </div>
      </div>
    </section>
  );
};

const Stat = ({ label, value }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
    <div style={{ fontSize: 10, color: 'var(--muted-2)', textTransform: 'uppercase', letterSpacing: '.08em', fontWeight: 600 }}>{label}</div>
    <div className="mono" style={{ fontSize: 14, color: 'var(--text)', fontWeight: 500 }}>{value}</div>
  </div>
);

// BodyMetric — large numeric with unit, for anthropometric column
const BodyMetric = ({ label, value, unit, icon }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
    <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, color: 'var(--muted-2)', textTransform: 'uppercase', letterSpacing: '.08em', fontWeight: 600 }}>
      {icon === 'height' && (
        <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
          <path d="M8 2v12M4 4l4-2 4 2M4 12l4 2 4-2"/>
        </svg>
      )}
      {icon === 'weight' && (
        <svg width="11" height="11" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <path d="M3 5h10l-1.2 8.4a1 1 0 0 1-1 .9H5.2a1 1 0 0 1-1-.9L3 5z"/>
          <circle cx="8" cy="3" r="1.4"/>
        </svg>
      )}
      <span>{label}</span>
    </div>
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 3 }}>
      <span className="mono" style={{ fontSize: 22, fontWeight: 600, color: 'var(--text)', lineHeight: 1 }}>{value ?? '—'}</span>
      <span style={{ fontSize: 11, color: 'var(--muted)' }}>{unit}</span>
    </div>
  </div>
);

const BigScoreRing = ({ label, value, accent, muted, size = 78 }) => {
  const stroke = 6;
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const off = c * (1 - value / 100);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
      <div style={{ position: 'relative', width: size, height: size }}>
        <svg width={size} height={size}>
          <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="rgba(15,23,42,.07)" strokeWidth={stroke}/>
          <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={accent} strokeWidth={stroke}
            strokeDasharray={c} strokeDashoffset={off} strokeLinecap="round"
            transform={`rotate(-90 ${size/2} ${size/2})`}
            style={{
              transition: 'stroke-dashoffset .8s cubic-bezier(.65,0,.35,1)',
              filter: muted ? 'none' : `drop-shadow(var(--chart-line-shadow, 0 0 0 transparent) ${accent})`,
            }}/>
        </svg>
        <div style={{
          position: 'absolute', inset: 0, display: 'grid', placeItems: 'center',
          fontSize: 26, fontWeight: 600, fontFamily: 'var(--font-mono)',
          color: muted ? 'var(--text-2)' : 'var(--text)',
          fontVariantNumeric: 'tabular-nums',
        }}>
          <CountUp value={value} duration={800}/>
        </div>
      </div>
      <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.1em' }}>{label}</div>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// FixedRangeLight — red/green status pip based on metric.fixedRange reference
// ──────────────────────────────────────────────────────────────────────────
const FixedRangeLight = ({ metric, value }) => {
  const fr = metric && metric.fixedRange;
  if (!fr || value == null || isNaN(value)) return null;
  const t = +fr.threshold;
  if (isNaN(t)) return null;
  let status; // 'good' | 'bad' | 'on'
  if (value === t) status = 'on';
  else if (fr.goodWhen === 'below') status = value < t ? 'good' : 'bad';
  else                              status = value > t ? 'good' : 'bad';
  const color = status === 'good' ? 'var(--pos)' : status === 'bad' ? 'var(--neg)' : 'var(--warn)';
  const tip = `Reference threshold ${t}${metric.unit ? ' ' + metric.unit : ''} · ${fr.goodWhen === 'above' ? 'above is good' : 'below is good'}`;
  return (
    <span
      title={tip}
      style={{
        display: 'inline-block', width: 8, height: 8, borderRadius: 99,
        background: color,
        boxShadow: `0 0 6px ${color}`,
        flexShrink: 0,
      }}
    />
  );
};

// ──────────────────────────────────────────────────────────────────────────
// MetricGroupCard — one card per group (Fitness, Speed, etc.)
// shows: group label, score ring, current metrics with diff vs avg + range bar
// ──────────────────────────────────────────────────────────────────────────
const MetricGroupCard = ({ group, values, squadStats, score, active, onClick, onMetricClick, onLabelChange, density, cardIndex, recentHistory }) => {
  const compact = density === 'compact';
  const [editing, setEditing] = useState(false);
  const [draft, setDraft] = useState(group.label);
  const [hovered, setHovered] = useState(false);
  const inputRef = useRef(null);

  useEffect(() => { setDraft(group.label); }, [group.label]);
  useEffect(() => {
    if (editing && inputRef.current) {
      inputRef.current.focus();
      inputRef.current.select();
    }
  }, [editing]);

  const commit = () => {
    const v = draft.trim();
    if (v && v !== group.label && onLabelChange) onLabelChange(v);
    else setDraft(group.label);
    setEditing(false);
  };
  const cancel = () => { setDraft(group.label); setEditing(false); };

  return (
    <div
      onClick={editing ? (e) => e.stopPropagation() : onClick}
      onMouseEnter={() => !editing && setHovered(true)}
      onMouseLeave={() => setHovered(false)}
      className="fade-up"
      style={{
        background: active
          ? `linear-gradient(135deg, var(--panel-2) 0%, ${group.accent}12 100%)`
          : hovered
            ? `linear-gradient(135deg, var(--panel-hi) 0%, ${group.accent}08 100%)`
            : `linear-gradient(135deg, var(--panel) 0%, ${group.accent}05 100%)`,
        border: '1px solid',
        borderColor: active ? group.accent : hovered ? 'var(--border-strong)' : 'var(--border)',
        borderRadius: 10,
        padding: compact ? '12px 14px' : '14px 16px',
        cursor: editing ? 'default' : 'pointer',
        transition: 'border-color .18s, background .18s, transform .18s, box-shadow .18s',
        transform: hovered && !active ? 'translateY(-2px)' : 'none',
        boxShadow: active
          ? `0 0 0 3px ${group.accent}22, 0 6px 24px rgba(0,0,0,.35)`
          : hovered
            ? '0 6px 20px rgba(0,0,0,.3)'
            : 'none',
        animationDelay: `${(cardIndex || 0) * 55}ms`,
        display: 'flex', flexDirection: 'column', gap: compact ? 8 : 10,
        minWidth: 0,
      }}
    >
      {/* header */}
      <div className="metric-group-header" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{
          width: 4, height: 18, borderRadius: 2,
          background: group.accent,
          boxShadow: `0 0 8px ${group.accent}66`,
        }}/>
        <div style={{ flex: 1, minWidth: 0 }}>
          {editing ? (
            <input
              ref={inputRef}
              value={draft}
              onChange={(e) => setDraft(e.target.value)}
              onClick={(e) => e.stopPropagation()}
              onBlur={commit}
              onKeyDown={(e) => {
                if (e.key === 'Enter') { e.preventDefault(); commit(); }
                else if (e.key === 'Escape') { e.preventDefault(); cancel(); }
              }}
              style={{
                width: '100%',
                background: 'var(--panel-hi)',
                border: `1px solid ${group.accent}`,
                borderRadius: 4,
                padding: '2px 6px',
                color: 'var(--text)',
                fontSize: 11,
                textTransform: 'uppercase',
                letterSpacing: '.1em',
                fontFamily: 'inherit',
                outline: 'none',
              }}
            />
          ) : (
            <div
              onDoubleClick={(e) => { e.stopPropagation(); setEditing(true); }}
              title="Double-click to rename"
              style={{
                fontSize: 11, textTransform: 'uppercase', letterSpacing: '.1em',
                color: 'var(--muted)',
                display: 'inline-flex', alignItems: 'center', gap: 6,
                whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                maxWidth: '100%',
              }}
            >
              <span>{group.label}</span>
              <button
                onClick={(e) => { e.stopPropagation(); setEditing(true); }}
                title={window.t('Rename')}
                className="group-rename"
                style={{
                  background: 'transparent', border: 0, padding: 2,
                  color: 'var(--muted-2)', cursor: 'pointer', borderRadius: 3,
                  display: 'inline-flex', alignItems: 'center',
                  opacity: 0, transition: 'opacity .15s, color .15s',
                }}
              >
                <svg width="10" height="10" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M11 2.5l2.5 2.5L6 12.5 3 13l.5-3 7.5-7.5z"/>
                </svg>
              </button>
            </div>
          )}
        </div>
        <ScoreDot value={score} size={42} stroke={4} />
      </div>

      {/* metrics list */}
      <div style={{ display: 'flex', flexDirection: 'column', gap: compact ? 5 : 7 }}>
        {group.metrics.map(m => {
          const v = values[m.id];
          const stats = squadStats[m.id];
          if (!stats) return null;
          const diff = m.dir === 'lower' ? (stats.avg - v) : (v - stats.avg);

          // Trend arrow: based on last 3 measurements (hist[0]=current, hist[1]=prev, hist[2]=prev-prev)
          const hist = recentHistory?.[m.id] || [];
          let trendIcon = null, trendColor = 'var(--muted)';
          if (hist.length >= 2 && m.dir !== 'neutral') {
            const imp0 = m.dir === 'lower' ? hist[0] < hist[1] : hist[0] > hist[1];
            const dec0 = m.dir === 'lower' ? hist[0] > hist[1] : hist[0] < hist[1];
            if (hist.length >= 3) {
              const imp1 = m.dir === 'lower' ? hist[1] < hist[2] : hist[1] > hist[2];
              const dec1 = m.dir === 'lower' ? hist[1] > hist[2] : hist[1] < hist[2];
              if (imp0 && imp1)       { trendIcon = '↑↑'; trendColor = 'var(--pos)'; }
              else if (dec0 && dec1)  { trendIcon = '↓↓'; trendColor = 'var(--neg)'; }
              else if (imp0)          { trendIcon = '↑';  trendColor = 'var(--pos)'; }
              else if (dec0)          { trendIcon = '↓';  trendColor = 'var(--neg)'; }
              else                    { trendIcon = '→';  trendColor = 'var(--muted-2)'; }
            } else {
              if (imp0)      { trendIcon = '↑'; trendColor = 'var(--pos)'; }
              else if (dec0) { trendIcon = '↓'; trendColor = 'var(--neg)'; }
            }
          }

          // Season-best marker: athlete's value matches squad best this session
          const isBest = v != null && stats.best != null && v === stats.best && m.dir !== 'neutral';

          return (
            <div key={m.id}
              onClick={(e) => { e.stopPropagation(); onMetricClick && onMetricClick(m); }}
              style={{
                display: 'grid',
                gridTemplateColumns: '1fr auto',
                alignItems: 'center', gap: 8,
                padding: compact ? '3px 6px' : '4px 6px',
                borderRadius: 5,
                transition: 'background .1s',
              }}
              onMouseEnter={(e) => { e.currentTarget.style.background = `${group.accent}12`; }}
              onMouseLeave={(e) => { e.currentTarget.style.background = 'transparent'; }}
            >
              {/* Left: label + range bar */}
              <div style={{ minWidth: 0 }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: compact ? 3 : 4 }}>
                  <span style={{ fontSize: 11, color: 'var(--text-2)' }}>{m.label}</span>
                  <FixedRangeLight metric={m} value={v}/>
                </div>
                <RangeBar value={v} worst={stats.worst} best={stats.best} avg={stats.avg} direction={m.dir} color={group.accent}/>
              </div>
              {/* Right: value + trend + diff stacked */}
              <div style={{ textAlign: 'right', flexShrink: 0, paddingLeft: 8 }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: 3, lineHeight: 1.1 }}>
                  {isBest && (
                    <span title={window.t('Season best')} style={{ fontSize: 9, color: '#fbbf24', lineHeight: 1 }}>★</span>
                  )}
                  <span className="mono" style={{ fontSize: 14, fontWeight: 600, color: 'var(--text)' }}>
                    {formatValue(v)}<span style={{ color: 'var(--muted)', marginLeft: 2, fontSize: 10, fontWeight: 400 }}>{m.unit}</span>
                  </span>
                  {trendIcon && (
                    <span title={`Trend (last 3): ${trendIcon}`} style={{ fontSize: 9, color: trendColor, lineHeight: 1, fontWeight: 700 }}>
                      {trendIcon}
                    </span>
                  )}
                </div>
                <div style={{ marginTop: 2 }}>
                  <DiffArrow value={diff} direction={m.dir} formatter={(x) => formatDiff(x)}/>
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
};

const formatValue = (v) => {
  if (v == null || isNaN(v)) return '—';
  if (Math.abs(v) >= 100) return Math.round(v).toString();
  if (Math.abs(v) >= 10)  return v.toFixed(1);
  return v.toFixed(2);
};
const formatDiff = (v) => {
  if (v == null || isNaN(v)) return '—';
  if (Math.abs(v) >= 100) return Math.round(v).toString();
  if (Math.abs(v) >= 10)  return v.toFixed(1);
  return v.toFixed(2);
};

// Range bar: shows worst..best horizontal scale with athlete marker + avg marker
const RangeBar = ({ value, worst, best, avg, direction, color }) => {
  // Always render with low->high left-to-right based on direction "performance" axis
  // - For "lower" direction: worst is high, best is low → flip
  const lo = direction === 'lower' ? best : worst;
  const hi = direction === 'lower' ? worst : best;
  const t = (value - lo) / (hi - lo);
  const ta = (avg - lo) / (hi - lo);
  const pct = Math.max(0, Math.min(1, t)) * 100;
  const aPct = Math.max(0, Math.min(1, ta)) * 100;
  // Direction of axis: performance left → right means "better" is on right
  // We flip for "lower" so left=better still reads naturally? Actually keep: right = better always.
  const perfPctV = direction === 'lower' ? (1 - t) * 100 : t * 100;
  const perfPctA = direction === 'lower' ? (1 - ta) * 100 : ta * 100;
  const v = Math.max(0, Math.min(100, perfPctV));
  const a = Math.max(0, Math.min(100, perfPctA));
  return (
    <div style={{ position: 'relative', height: 6, background: 'rgba(15,23,42,.06)', borderRadius: 3 }}>
      {/* filled bar */}
      <div style={{
        position: 'absolute', left: 0, top: 0, bottom: 0,
        width: `${v}%`,
        background: `linear-gradient(90deg, ${color}55, ${color})`,
        borderRadius: 3,
        transition: 'width .4s cubic-bezier(.65,0,.35,1)',
      }}/>
      {/* avg marker */}
      <div style={{
        position: 'absolute', left: `${a}%`, top: -2, bottom: -2,
        width: 1.5, background: 'var(--text-2)', opacity: .55,
        transform: 'translateX(-.75px)',
      }}/>
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// SeasonSelector dropdown (values are ISO dates; displayed pretty)
// ──────────────────────────────────────────────────────────────────────────
const SeasonSelector = ({ value, onChange, seasons }) => {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);
  const fmt = (v) => (window.formatDate ? window.formatDate(v, 'short') : v);
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button className="btn" onClick={() => setOpen(o => !o)} style={{ minWidth: 160, justifyContent: 'space-between' }}>
        <span style={{ display:'flex', alignItems:'center', gap: 8 }}>
          <Icon name="history" size={13}/>
          <span>{fmt(value)}</span>
        </span>
        <Icon name="chevDown" size={12}/>
      </button>
      {open && (
        <div className="fade" style={{
          position: 'absolute', top: 'calc(100% + 4px)', left: 0, right: 0,
          background: 'var(--panel-2)', border: '1px solid var(--border-strong)',
          borderRadius: 6, padding: 4, zIndex: 50,
          boxShadow: '0 8px 24px rgba(0,0,0,.4)',
          maxHeight: 320, overflowY: 'auto',
        }}>
          {seasons.map(s => (
            <button key={s} onClick={() => { onChange(s); setOpen(false); }}
              style={{
                display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                width: '100%', textAlign: 'left',
                padding: '6px 10px', borderRadius: 4,
                background: s === value ? 'var(--accent-soft)' : 'transparent',
                color: s === value ? 'var(--accent-2)' : 'var(--text)',
                border: 0, font: '12px var(--font-sans)', cursor: 'pointer',
              }}
              onMouseEnter={(e) => { if (s !== value) e.currentTarget.style.background = 'var(--panel-hi)'; }}
              onMouseLeave={(e) => { if (s !== value) e.currentTarget.style.background = 'transparent'; }}
            >
              <span>{fmt(s)}</span>
              <span style={{ fontSize: 10, color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>{s}</span>
            </button>
          ))}
        </div>
      )}
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// KpiTile — small numeric card (counts in topbar)
// ──────────────────────────────────────────────────────────────────────────
const KpiTile = ({ label, value, sub, accent = 'var(--accent)' }) => (
  <div style={{
    padding: '8px 14px',
    background: 'var(--panel)',
    border: '1px solid var(--border)',
    borderRadius: 8,
    display: 'flex', flexDirection: 'column', gap: 2,
    minWidth: 120,
  }}>
    <div style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>{label}</div>
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 6 }}>
      <span className="mono" style={{ fontSize: 18, fontWeight: 600, color: accent }}>{value}</span>
      {sub && <span style={{ fontSize: 11, color: 'var(--muted)' }}>{sub}</span>}
    </div>
  </div>
);

// ──────────────────────────────────────────────────────────────────────────
// GroupingToggle — Day / Week / Month segmented control
// ──────────────────────────────────────────────────────────────────────────
const GroupingToggle = ({ value, onChange }) => {
  const opts = [
    { id: 'day',   label: 'Day' },
    { id: 'week',  label: 'Week' },
    { id: 'month', label: 'Month' },
  ];
  return (
    <div style={{
      display: 'inline-flex',
      background: 'var(--panel-hi)',
      border: '1px solid var(--border)',
      borderRadius: 6, padding: 2,
    }}>
      {opts.map(o => {
        const active = value === o.id;
        return (
          <button key={o.id} onClick={() => onChange(o.id)}
            style={{
              padding: '4px 10px', borderRadius: 4,
              background: active ? 'var(--text)' : 'transparent',
              color: active ? '#fff' : 'var(--text-2)',
              border: 0, font: '500 11px var(--font-sans)',
              cursor: 'pointer',
              letterSpacing: '.02em',
            }}>
            {o.label}
          </button>
        );
      })}
    </div>
  );
};

// ──────────────────────────────────────────────────────────────────────────
// CountUp — animates a number from current display value to a target.
// Uses requestAnimationFrame; respects the global motion toggle.
// ──────────────────────────────────────────────────────────────────────────
function CountUp({ value, decimals = 0, duration = 700, prefix = '', suffix = '', style }) {
  const [shown, setShown] = useState(value);
  const fromRef = useRef(value);
  const startRef = useRef(0);
  const rafRef = useRef(0);
  useEffect(() => {
    const motionOn = document.documentElement.getAttribute('data-motion') !== 'off';
    if (!motionOn || !isFinite(value)) { setShown(value); return; }
    cancelAnimationFrame(rafRef.current);
    fromRef.current = isFinite(shown) ? shown : value;
    startRef.current = performance.now();
    const tick = (now) => {
      const t = Math.min(1, (now - startRef.current) / duration);
      // ease out cubic
      const eased = 1 - Math.pow(1 - t, 3);
      setShown(fromRef.current + (value - fromRef.current) * eased);
      if (t < 1) rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [value, duration]);
  const display = isFinite(shown) ? shown.toFixed(decimals) : value;
  return <span className="mono" style={style}>{prefix}{display}{suffix}</span>;
}

// ──────────────────────────────────────────────────────────────────────────
// LivePulse — small pulsing dot + label, used for "Live"/"Synced" indicators.
// ──────────────────────────────────────────────────────────────────────────
function LivePulse({ label = 'LIVE', color = 'var(--pos)', style }) {
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      fontSize: 10, color: 'var(--muted)', fontFamily: 'var(--font-mono)',
      letterSpacing: '.08em', textTransform: 'uppercase',
      ...style,
    }}>
      <span className="live-dot" style={{ background: color }}></span>
      {label}
    </span>
  );
}

// ──────────────────────────────────────────────────────────────────────────
// RiskBadge — four-level traffic light pill with tooltip (P3-B)
// Props: level ('gray'|'green'|'amber'|'red'), flags (string[])
// ──────────────────────────────────────────────────────────────────────────
const RISK_COLORS = {
  gray:  { bg: 'rgba(139,148,163,.12)', border: 'rgba(139,148,163,.30)', text: '#8b94a3', dot: '#8b94a3', label: '–' },
  green: { bg: 'rgba(16,185,129,.10)',  border: 'rgba(16,185,129,.30)',  text: '#059669', dot: '#10b981', label: '正常' },
  amber: { bg: 'rgba(245,158,11,.10)',  border: 'rgba(245,158,11,.30)',  text: '#d97706', dot: '#f59e0b', label: '注意' },
  red:   { bg: 'rgba(239,68,68,.10)',   border: 'rgba(239,68,68,.30)',   text: '#dc2626', dot: '#ef4444', label: '风险' },
};

const RiskBadge = ({ level = 'gray', flags = [], size = 'md' }) => {
  const [show, setShow] = useState(false);
  const c = RISK_COLORS[level] || RISK_COLORS.gray;
  const dotSize = size === 'sm' ? 7 : 9;
  const fontSize = size === 'sm' ? 10 : 11;
  return (
    <div style={{ position: 'relative', display: 'inline-flex' }}
         onMouseEnter={() => setShow(true)} onMouseLeave={() => setShow(false)}>
      <div style={{
        display: 'inline-flex', alignItems: 'center', gap: 5,
        padding: size === 'sm' ? '2px 6px' : '3px 8px',
        borderRadius: 999, cursor: flags.length > 0 ? 'help' : 'default',
        background: c.bg, border: `1px solid ${c.border}`,
      }}>
        <div style={{
          width: dotSize, height: dotSize, borderRadius: '50%',
          background: c.dot, flexShrink: 0,
        }}/>
        <span style={{ fontSize, fontFamily: 'var(--font-mono)', color: c.text, fontWeight: 500, letterSpacing: '.02em' }}>
          {c.label}
        </span>
      </div>
      {show && flags.length > 0 && (
        <div style={{
          position: 'absolute', bottom: 'calc(100% + 6px)', left: 0,
          background: 'var(--panel)', border: '1px solid var(--border-strong)',
          borderRadius: 7, padding: '8px 10px',
          boxShadow: '0 4px 16px rgba(15,23,42,.12)',
          minWidth: 200, maxWidth: 280, zIndex: 200,
          display: 'flex', flexDirection: 'column', gap: 5,
          pointerEvents: 'none',
        }}>
          <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--muted)', letterSpacing: '.08em', textTransform: 'uppercase', marginBottom: 2 }}>风险指标</div>
          {flags.map((f, i) => (
            <div key={i} style={{ fontSize: 11, color: 'var(--text)', lineHeight: 1.4 }}>· {f}</div>
          ))}
        </div>
      )}
    </div>
  );
};

// ── P5-1: 力板面板公共壳组件 (CMJ / SJ / IMTP 共用) ─────────────────────────────
// FL-1: ForceNote — shared hover-collapse primitive for explanatory copy on the
// force-lab upload/save/session surfaces (cmj/sj/imtp), mirroring the SE-1
// pattern in settings.jsx (Note). Per §3.9 hover-first: descriptive text
// collapses to a hairline caption row and surfaces on hover (or pinned via
// click); persistent actions/controls never route through this component.
const ForceNote = ({ children }) => {
  const [pinned, setPinned] = useState(false);
  return (
    <div data-se1-note style={{ position: 'relative' }}>
      <button
        type="button"
        data-se1-note-trigger
        onClick={() => setPinned(p => !p)}
        style={{
          display: 'flex', alignItems: 'center', gap: 6, width: '100%',
          padding: '4px 2px', background: 'transparent', border: 0,
          borderTop: '1px solid var(--border)', cursor: 'help',
          font: 'inherit', textAlign: 'left', color: 'var(--muted)',
        }}
      >
        <Icon name="info" size={11}/>
        <span style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '.06em' }}>
          {window.t ? window.t('Details') : 'Details'}
        </span>
        <span style={{ marginLeft: 'auto', fontSize: 9.5, color: 'var(--muted-2)' }}>
          {pinned ? (window.t ? window.t('hide') : 'hide') : (window.t ? window.t('hover / click') : 'hover / click')}
        </span>
      </button>
      {pinned && (
        <div data-se1-note-body style={{
          marginTop: 6, padding: '9px 11px',
          background: 'var(--panel-2)', border: '1px solid var(--border)',
          borderRadius: 6,
          fontSize: 11, color: 'var(--text-2)', lineHeight: 1.55,
        }}>{children}</div>
      )}
      <div data-se1-note-hover className="se1-note-hover" style={{
        display: 'none',
        position: 'absolute', top: '100%', left: 0, zIndex: 6,
        marginTop: 4, padding: '9px 11px', width: 'min(380px, 90vw)',
        background: 'var(--panel-2)', border: '1px solid var(--border)',
        borderRadius: 6, boxShadow: '0 8px 24px rgba(15,23,42,.12)',
        fontSize: 11, color: 'var(--text-2)', lineHeight: 1.55,
      }}>{children}</div>
    </div>
  );
};

// ForceTestUploadZone — 拖拽上传区 + 隐藏 input + 重新上传按钮
const ForceTestUploadZone = ({ fileRef, hasResult, dragOver, onDrop, setDragOver, onFileChange, onReupload, hint, accept }) => (
  <>
    {!hasResult && (
      <div
        onDrop={onDrop}
        onDragOver={e => { e.preventDefault(); setDragOver(true); }}
        onDragLeave={() => setDragOver(false)}
        onClick={() => fileRef.current?.click()}
        style={{
          border: `2px dashed ${dragOver ? 'var(--accent)' : 'var(--border)'}`,
          borderRadius: 12, padding: '40px 24px', cursor: 'pointer', textAlign: 'center',
          background: dragOver ? 'var(--accent-soft)' : 'var(--panel)',
          transition: 'border-color .15s, background .15s',
        }}>
        <div style={{ fontSize: 24, marginBottom: 8 }}>↑</div>
        <div style={{ fontSize: 13, fontWeight: 500, color: 'var(--text-2)' }}>拖拽或点击上传力板数据文件</div>
        <div data-se1-upload-hint onClick={e => e.stopPropagation()} style={{ marginTop: 6, display: 'inline-block', textAlign: 'left' }}>
          <ForceNote>{hint || 'VALD ForceDecks · CSV / XLSX / TSV'}</ForceNote>
        </div>
      </div>
    )}
    <input ref={fileRef} type="file" accept={accept || '.csv,.tsv,.xlsx,.xls'} style={{ display: 'none' }} onChange={onFileChange} />
    {hasResult && (
      <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
        <button onClick={onReupload} style={{ fontSize: 11, padding: '5px 14px', borderRadius: 6, background: 'var(--panel-2)', border: '1px solid var(--border)', color: 'var(--text-2)', cursor: 'pointer', fontFamily: 'var(--font-sans)' }}>重新上传</button>
      </div>
    )}
  </>
);

// ForceTestLoadingError — 解析中提示 + 错误展示
const ForceTestLoadingError = ({ loading, error, onRetry }) => (
  <>
    {loading && <div style={{ color: 'var(--muted)', fontSize: 12 }}>正在分析…</div>}
    {error && (
      <div style={{ background: 'rgba(239,68,68,.07)', border: '1px solid rgba(239,68,68,.22)', borderRadius: 10, padding: '12px 14px', fontSize: 11.5, color: 'var(--text)', display: 'flex', alignItems: 'flex-start', gap: 10 }}>
        <div style={{ width: 24, height: 24, borderRadius: 7, background: 'rgba(239,68,68,.12)', color: 'var(--neg)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontWeight: 750, flex: '0 0 auto' }}>!</div>
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ fontSize: 12.5, fontWeight: 700, color: 'var(--neg)' }}>File format not recognized / 文件格式未识别</div>
          <div style={{ marginTop: 3, color: 'var(--text-2)', lineHeight: 1.5 }}>
            Upload a raw force-plate export with Time, Left, and Right force columns. / 请上传包含 Time、Left、Right 力值列的力板原始导出文件。
          </div>
          <div style={{ marginTop: 5, color: 'var(--muted)', lineHeight: 1.45 }}>
            Detail: {error}
          </div>
        </div>
        {onRetry && <button onClick={onRetry} style={{ flex: '0 0 auto', fontSize: 11, color: 'var(--accent-2)', background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 6, padding: '5px 9px', cursor: 'pointer' }}>重新选择文件</button>}
      </div>
    )}
  </>
);

// ForceTestSaveBanner — 运动员选择 + 代表 trial 选择 + 保存按钮 + 已存计数
const ForceTestSaveBanner = ({ result, athletes, athleteId, setAthleteId, sessionCount, onSave, trials = [], selectedTrialIndex = 0, onSelectedTrialChange, testType = 'cmj', saveDate, onSaveDateChange }) => {
  if (!result || !athletes?.length) return null;
  const cfg = window.forceTestConfig?.(testType) || {};
  const primaryKey = cfg.primaryMetricKey || (testType === 'imtp' ? 'peakForce' : 'jumpHeight');
  const primaryLabel = cfg.primaryMetricLabel || (testType === 'imtp' ? 'Fpeak' : 'JH');
  const primaryUnit = cfg.primaryMetricUnit || '';
  const fmtPrimary = (v) => v == null || !isFinite(v) ? '—' : `${Number(v).toFixed(primaryKey === 'peakForce' ? 0 : 1)}${primaryUnit}`;
  return (
    <div data-se1-save-banner style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '10px 14px', background: 'var(--panel)', border: '1px solid var(--border)', borderTop: '2px solid var(--accent)', borderRadius: 8, flexWrap: 'wrap' }}>
      <span style={{ fontSize: 12, color: 'var(--text-2)', flex: '0 0 auto' }}>保存至运动员：</span>
      <select value={athleteId || ''} onChange={e => setAthleteId(e.target.value)}
        style={{ flex: 1, minWidth: 120, fontSize: 12, padding: '3px 8px', borderRadius: 5, border: '1px solid var(--border)', background: 'var(--panel)', color: 'var(--text)', fontFamily: 'var(--font-sans)' }}>
        {athletes.map(a => <option key={a.id} value={a.id}>{a.name}</option>)}
      </select>
      <span style={{ fontSize: 12, color: 'var(--text-2)', flex: '0 0 auto' }}>日期：</span>
      <input type="date" value={saveDate || ''} onChange={e => onSaveDateChange && onSaveDateChange(e.target.value)}
        style={{ fontSize: 12, padding: '3px 6px', borderRadius: 5, border: '1px solid var(--border)', background: 'var(--panel)', color: 'var(--text)' }} />
      {trials.length > 1 && (
        <>
          <span style={{ fontSize: 12, color: 'var(--text-2)', flex: '0 0 auto' }}>代表 trial：</span>
          <select value={selectedTrialIndex} onChange={e => onSelectedTrialChange && onSelectedTrialChange(+e.target.value)}
            style={{ flex: '0 1 180px', minWidth: 150, fontSize: 12, padding: '3px 8px', borderRadius: 5, border: '1px solid var(--border)', background: 'var(--panel)', color: 'var(--text)', fontFamily: 'var(--font-sans)' }}>
            {trials.map((t, idx) => (
              <option key={t.index ?? idx} value={idx}>
                T{t.index ?? idx + 1} · {primaryLabel} {fmtPrimary(t.metrics?.[primaryKey])}
              </option>
            ))}
          </select>
        </>
      )}
      <button onClick={onSave} className="btn" style={{ fontSize: 12, padding: '4px 14px', flex: '0 0 auto' }}>保存 Session</button>
      {sessionCount > 0 && <span style={{ fontSize: 10, color: 'var(--muted)', whiteSpace: 'nowrap' }}>已有 {sessionCount} 个 session</span>}
      <div style={{ flexBasis: '100%' }}>
        <ForceNote>保存后，运动员档案指标将使用所选代表 trial；完整 trial 列表仍保存在 session 中。</ForceNote>
      </div>
    </div>
  );
};

// expose
Object.assign(window, {
  Icon, BrandMark, Sidebar, FilterRow, AvatarMini, ScoreDot, DiffArrow,
  AthleteHeader, BigScoreRing, Stat, MetricGroupCard, RangeBar,
  SeasonSelector, KpiTile, formatValue, formatDiff, MetricPicker,
  BodyMetric, GroupingToggle, CountUp, LivePulse, RiskBadge,
  ForceTestUploadZone, ForceTestLoadingError, ForceTestSaveBanner, ForceNote,
});

// ──────────────────────────────────────────────────────────────────────────
// MetricPicker — grouped popover, replaces the cramped chip row
// ──────────────────────────────────────────────────────────────────────────
function MetricPicker({ groups, value, onChange }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);
  useEffect(() => {
    const h = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    document.addEventListener('mousedown', h);
    return () => document.removeEventListener('mousedown', h);
  }, []);
  // resolve group for current value
  let activeGroup = null;
  groups.forEach(g => { if (g.metrics.some(m => m.id === value.id)) activeGroup = g; });
  return (
    <div ref={ref} style={{ position: 'relative' }}>
      <button className="btn" onClick={() => setOpen(o => !o)} style={{ minWidth: 200, justifyContent: 'space-between' }}>
        <span style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
          {activeGroup && <span style={{ width: 8, height: 8, borderRadius: 2, background: activeGroup.accent, flexShrink: 0 }}/>}
          <span style={{ fontSize: 10, color: 'var(--muted)', textTransform: 'uppercase', letterSpacing: '.08em' }}>
            {activeGroup?.label}
          </span>
          <span style={{ color: 'var(--text)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
            {value.label}
          </span>
        </span>
        <Icon name="chevDown" size={12}/>
      </button>
      {open && (
        <div className="fade" style={{
          position: 'absolute', top: 'calc(100% + 4px)', right: 0,
          background: 'var(--panel-2)', border: '1px solid var(--border-strong)',
          borderRadius: 8, padding: 6, zIndex: 50, minWidth: 260, maxHeight: 360,
          overflowY: 'auto',
          boxShadow: '0 12px 36px rgba(0,0,0,.5)',
          display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0,1fr))', gap: 2,
        }}>
          {groups.map(g => (
            <div key={g.id} style={{ display: 'flex', flexDirection: 'column', padding: '4px 2px' }}>
              <div style={{
                fontSize: 9, textTransform: 'uppercase', letterSpacing: '.1em',
                color: g.accent, padding: '4px 6px', display: 'flex', alignItems: 'center', gap: 6,
              }}>
                <span style={{ width: 4, height: 4, borderRadius: 99, background: g.accent }}/>
                {g.label}
              </div>
              {g.metrics.filter(m => !m.computed).map(m => {
                const active = m.id === value.id;
                return (
                  <button key={m.id}
                    onClick={() => { onChange(m); setOpen(false); }}
                    style={{
                      display: 'flex', justifyContent: 'space-between', alignItems: 'center',
                      padding: '5px 8px', borderRadius: 4,
                      background: active ? 'var(--accent-soft)' : 'transparent',
                      color: active ? 'var(--accent-2)' : 'var(--text-2)',
                      border: 0, font: '12px var(--font-sans)', cursor: 'pointer',
                      textAlign: 'left',
                    }}
                    onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'var(--panel-hi)'; }}
                    onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
                  >
                    <span>{m.label}</span>
                    <span style={{ fontSize: 10, color: 'var(--muted)', fontFamily: 'var(--font-mono)' }}>{m.unit}</span>
                  </button>
                );
              })}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// ── AthleteForceTestsCard ────────────────────────────────────────────────────
// Shows CMJ / SJ / IMTP history for one athlete in their individual panel.
function effectiveForceSessions(sessions) {
  const source = window.ForceSessionSource;
  return (sessions || []).map(session =>
    source && typeof source.resolveEffectiveSession === 'function'
      ? source.resolveEffectiveSession(session) : session
  );
}

const AthleteForceTestsCard = ({
  athlete,
  sessions = [],
  sjSessions = [],
  imtpSessions = [],
  forceSessions = null,
  reviewedEvidence = null,
  onUpload,
  onOpenReview,
  onOpenReport,
  hasCmjResult = false,
  onViewResult,
  onViewLongitudinal,
}) => {
  const [forceDetailPinned, setForceDetailPinned] = useState(false);
  const testTypes = window.FORCE_TEST_TYPES || ['cmj', 'sj', 'imtp'];
  const rawSessionMap = forceSessions || { cmj: sessions, sj: sjSessions, imtp: imtpSessions };
  const sessionMap = {};
  testTypes.forEach(type => { sessionMap[type] = effectiveForceSessions(rawSessionMap[type]); });
  const configs = window.FORCE_TEST_CONFIGS || {};
  const totalSessions = testTypes.reduce((sum, t) => sum + (sessionMap[t] || []).length, 0);

  const bestJHRaw = (sessionMap.cmj || []).length > 0
    ? Math.max(...(sessionMap.cmj || []).map(s => s.best?.jumpHeight).filter(v => v != null && isFinite(v)))
    : null;
  const bestJH = bestJHRaw != null && isFinite(bestJHRaw) ? bestJHRaw.toFixed(1) : null;

  // CMJ jump-height trend (last 8) — the scard mini-bars
  const jhSeries = (sessionMap.cmj || [])
    .map(s => ({ date: s.date, v: s.best?.jumpHeight }))
    .filter(d => d.v != null && isFinite(d.v))
    .sort((a, b) => String(a.date).localeCompare(String(b.date)))
    .slice(-8);
  const jhMax = jhSeries.length ? Math.max(...jhSeries.map(d => d.v)) : 0;
  const jhMin = jhSeries.length ? Math.min(...jhSeries.map(d => d.v)) : 0;

  const Panel = window.Panel, Pill = window.Pill, ModuleArrow = window.ModuleArrow;
  const enter = () => (onViewLongitudinal ? onViewLongitudinal('cmj') : onUpload && onUpload());
  const formatStoryEvidenceDate = (value) => {
    if (!value) return '—';
    const date = new Date(value);
    if (Number.isNaN(date.getTime())) return String(value).slice(0, 10) || '—';
    return date.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
  };
  const reviewed = reviewedEvidence && reviewedEvidence.reviewedStatus === 'reviewed';

  const inner = (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 7, minWidth: 0 }}>
        {bestJH != null
          ? <><span className="mono" style={{ fontSize: 18, fontWeight: 600, lineHeight: 1, color: 'var(--text)' }}>{bestJH}</span><span style={{ fontSize: 11, color: 'var(--muted)' }}>cm · 最佳弹跳</span></>
          : <span style={{ fontSize: 15, fontWeight: 600, color: 'var(--text)' }}>{totalSessions ? `${totalSessions} 次测试` : '尚未测试'}</span>}
      </div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center' }}>
        {testTypes.map(type => {
          const n = (sessionMap[type] || []).length;
          if (!n) return null;
          const cfg = configs[type] || { label: type.toUpperCase() };
          return <Pill key={type}>{cfg.label} {n}</Pill>;
        })}
        {hasCmjResult && onViewResult && (
          <button onClick={onViewResult} className="btn ghost" style={{ fontSize: 11, padding: '2px 8px', color: 'var(--accent)' }}>继续查看 ↗</button>
        )}
      </div>
      {jhSeries.length >= 2 && (
        <div>
          <div style={{ display: 'flex', alignItems: 'flex-end', gap: 4, height: 34 }}>
            {jhSeries.map((d, i) => {
              const h = jhMax > jhMin ? 28 + (d.v - jhMin) / (jhMax - jhMin) * 72 : 60;
              return <div key={i} title={`${d.date}: JH ${d.v}cm`} style={{ flex: 1, height: `${h}%`, minHeight: 4, borderRadius: '3px 3px 0 0', background: 'var(--accent)', opacity: i === jhSeries.length - 1 ? 1 : 0.4 }}/>;
            })}
          </div>
          <div style={{ fontSize: 9, color: 'var(--muted-2)', textAlign: 'right', marginTop: 2 }}>CMJ 弹跳高度 · 最近 {jhSeries.length} 次</div>
        </div>
      )}
      <div style={{
        border: '1px solid var(--border)',
        borderRadius: 8,
        background: reviewed ? 'rgba(22,163,74,.06)' : 'var(--panel-2)',
        padding: '9px 10px',
        display: 'grid',
        gap: 7,
      }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'center' }}>
          <div style={{ fontSize: 10, color: 'var(--muted-2)', textTransform: 'uppercase', letterSpacing: '.08em', fontWeight: 700 }}>Confirmed CMJ Result / 已确认 CMJ 结果</div>
          <div style={{
            fontSize: 9.5,
            color: reviewed ? 'var(--pos)' : 'var(--muted)',
            border: '1px solid var(--border)',
            borderRadius: 999,
            padding: '2px 7px',
            textTransform: 'uppercase',
            whiteSpace: 'nowrap',
          }}>{reviewed ? 'Confirmed' : 'Not Confirmed'}</div>
        </div>
        {reviewedEvidence ? (
          <>
            <div data-se1-note className="athlete-story-hover-detail" style={{ position: 'relative' }}>
              <button type="button" data-se1-note-trigger onClick={() => setForceDetailPinned(p => !p)} className="athlete-story-hover-detail-trigger">
                <span>Details / 详情</span>
                <span className="athlete-story-hover-detail-hint">{forceDetailPinned ? 'hide' : 'hover / click'}</span>
              </button>
              {forceDetailPinned && (
                <dl data-se1-note-body className="athlete-story-context-list athlete-story-hover-detail-body">
                  <div><dt>Sports Scientist Conclusion</dt><dd>{reviewedEvidence.classification}</dd></div>
                  <div><dt>Reviewer Training Focus</dt><dd>{reviewedEvidence.reviewerTrainingFocus}</dd></div>
                  <div><dt>Session: / Last Confirmed:</dt><dd>{String(reviewedEvidence.sessionDate || '—').slice(0, 10)} · {formatStoryEvidenceDate(reviewedEvidence.lastReviewedAt)}</dd></div>
                </dl>
              )}
              <dl data-se1-note-hover className="se1-note-hover athlete-story-context-list athlete-story-hover-detail-body">
                <div><dt>Sports Scientist Conclusion</dt><dd>{reviewedEvidence.classification}</dd></div>
                <div><dt>Reviewer Training Focus</dt><dd>{reviewedEvidence.reviewerTrainingFocus}</dd></div>
                <div><dt>Session / Last Confirmed</dt><dd>{String(reviewedEvidence.sessionDate || '—').slice(0, 10)} · {formatStoryEvidenceDate(reviewedEvidence.lastReviewedAt)}</dd></div>
              </dl>
            </div>
            <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
              {onOpenReview && onOpenReview === onOpenReport ? (
                <button type="button" className="btn ghost" disabled={!reviewedEvidence.sessionExists} onClick={() => onOpenReport?.(reviewedEvidence)} style={{ fontSize: 11, padding: '3px 8px', color: 'var(--accent)' }}>Open Review / Report</button>
              ) : (
                <>
                  <button type="button" className="btn ghost" disabled={!reviewedEvidence.sessionExists} onClick={() => onOpenReview?.(reviewedEvidence)} style={{ fontSize: 11, padding: '3px 8px', color: 'var(--accent)' }}>Open Review</button>
                  <button type="button" className="btn ghost" disabled={!reviewedEvidence.sessionExists} onClick={() => onOpenReport?.(reviewedEvidence)} style={{ fontSize: 11, padding: '3px 8px', color: 'var(--accent)' }}>Open Report</button>
                </>
              )}
            </div>
          </>
        ) : (
          <div style={{ fontSize: 10.5, color: 'var(--muted)' }}>No confirmed CMJ result yet. / 暂无已确认 CMJ 结果</div>
        )}
      </div>
      <button onClick={() => (onUpload ? onUpload() : enter())} className="btn ghost" style={{ fontSize: 11, padding: '4px 8px', color: 'var(--muted)', alignSelf: 'flex-start' }}>+ 上传力板</button>
    </div>
  );
  return Panel
    ? <Panel title="Force Plate · 力板数据" subtitle={`${athlete?.name} · ${totalSessions} 次测试`} rightAction={ModuleArrow && <ModuleArrow onClick={enter} title="进入力量剖析" />}>{inner}</Panel>
    : <div style={{ background: 'var(--panel)', border: '1px solid var(--border)', borderRadius: 12, padding: 14 }}>{inner}</div>;
};

const AthleteCMJCard = AthleteForceTestsCard;


Object.assign(window, { AthleteForceTestsCard, AthleteCMJCard });
