/* global React, Card, Button, Segmented, Pill, Eyebrow */
// ============================================================================
// progress.jsx — Tab 3 · Progress (_spec-app-v2.md): Ladders · Benchmarks · Tests.
//
// Data only through window.PlanAPI (getLadders, getBenchmarks, getSeason, getWeek).
// Routing via the shell's sub: 'ladders' (default) | 'benchmarks' | 'tests'
//   | 'ladder:<progression id>' (one ladder) | 'benchmark:<benchmark id>' (one benchmark).
//
// Design rule kept here: the elite mark is a HORIZON, never a target. No progress
// bars toward elite values and no "% of" anything. The only filled segments on this
// screen are his own ladder steps.
// ============================================================================
(() => {
  const { useState, useEffect } = React;
  const A = () => window.PlanAPI;

  // ---------------------------------------------------------------- constants
  const DOMAINS = [
    { id: 'physical', label: 'Physical' },
    { id: 'skill', label: 'Skills' },
    { id: 'academic', label: 'School' },
    { id: 'profile', label: 'Application' },
  ];
  const KIND_LABEL = { ruler: 'ruler', pass_line: 'pass line', gate: 'gate', form: 'form', report: 'report' };
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
  const ROLE_LABEL = { support: 'Support', next_prep: 'Getting ready for the next step', warmup: 'Warm-up' };
  const WD = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
  // Reference keys → label, and which bucket they belong to. The horizon bucket is the
  // elite end: read as a line on the horizon, never as a target.
  const REF_LABEL = {
    combine: 'Tested at', combine_avg_cb: 'Combine avg (CB)', combine_avg_s: 'Combine avg (S)',
    combine_avg_wr: 'Combine avg (WR)', combine_avg_rb: 'Combine avg (RB)', record: 'Record',
    elite: 'Elite', other: 'Note', youth: 'Youth', peer_end: 'Judged against',
    working_target: 'Working target', horizon_unhooked_admit: 'Unhooked admits',
  };
  const HORIZON_KEYS = ['combine_avg_cb', 'combine_avg_wr', 'combine_avg_s', 'combine_avg_rb', 'record', 'elite', 'horizon_unhooked_admit', 'combine'];
  const OWN_KEYS = ['peer_end', 'working_target', 'youth'];
  const LINE_KEYS = ['combine_avg_cb', 'combine_avg_wr', 'combine_avg_s', 'combine_avg_rb'];

  // ---------------------------------------------------------------- helpers
  function fmtDay(s) {
    if (!s) return '';
    const d = new Date(String(s).slice(0, 10) + 'T12:00:00');
    return `${WD[(d.getDay() + 6) % 7]} ${d.getDate()} ${MONTHS[d.getMonth()]}`;
  }
  function fmtShort(s) {
    if (!s) return '';
    const d = new Date(String(s).slice(0, 10) + 'T12:00:00');
    return `${d.getDate()} ${MONTHS[d.getMonth()]}`;
  }
  function daysUntil(s) {
    const t = new Date(A().today() + 'T12:00:00'), d = new Date(String(s).slice(0, 10) + 'T12:00:00');
    return Math.round((d - t) / 864e5);
  }
  function whenLabel(s) {
    const n = daysUntil(s);
    if (n === 0) return 'today';
    if (n === 1) return 'tomorrow';
    if (n > 1 && n < 14) return `in ${n} days`;
    if (n >= 14) return `in ${Math.round(n / 7)} weeks`;
    return '';
  }
  function num(v) {
    if (v == null || v === '') return null;
    const n = Number(v);
    return Number.isFinite(n) ? n : null;
  }
  function fmtNum(v) {
    const n = num(v);
    if (n == null) return String(v ?? '');
    return Number.isInteger(n) ? String(n) : String(Math.round(n * 100) / 100);
  }
  // "4.48" or "91 (36")" → the leading number; anything with words → null
  function refNumber(s) {
    const m = String(s == null ? '' : s).trim().match(/^(-?\d+(?:\.\d+)?)(?:\s*\([^)]*\))?$/);
    return m ? Number(m[1]) : null;
  }
  // "Football · QB · Throwing velocity" → { sport: 'Football', rest: 'QB · Throwing velocity' }
  function splitSport(name) {
    const parts = String(name || '').split(' · ');
    return parts.length > 1 ? { sport: parts[0], rest: parts.slice(1).join(' · ') } : { sport: 'Other', rest: name };
  }
  function kidOf(memberDb) {
    const kids = (A().me().kids || []);
    return kids.find(k => k.db_id === memberDb) || {};
  }
  function parseSub(sub) {
    const s = String(sub || '');
    if (s.startsWith('ladder:')) return { view: 'ladder', id: decodeURIComponent(s.slice(7)), tab: 'ladders' };
    if (s.startsWith('benchmark:')) return { view: 'benchmark', id: decodeURIComponent(s.slice(10)), tab: 'benchmarks' };
    if (s === 'benchmarks' || s === 'tests') return { view: s, tab: s };
    return { view: 'ladders', tab: 'ladders' };
  }

  // Loads once per key; returns { loading, error, data, reload }
  function useLoad(key, fn) {
    const [st, setSt] = useState({ loading: false, error: null, data: null, n: 0, loadedKey: null });
    useEffect(() => {
      if (!key) return undefined;
      let dead = false;
      setSt(s => ({ ...s, loading: true, error: null }));
      Promise.resolve().then(fn)
        .then(data => { if (!dead) setSt(s => ({ ...s, loading: false, data, loadedKey: key })); })
        .catch(e => { console.error(e); if (!dead) setSt(s => ({ ...s, loading: false, error: e.message || String(e), loadedKey: key })); });
      return () => { dead = true; };
    }, [key, st.n]);
    // Not loaded for this key yet counts as loading, so a tab switch never flashes an empty list
    return { ...st, loading: !!key && (st.loading || st.loadedKey !== key), reload: () => setSt(s => ({ ...s, n: s.n + 1 })) };
  }

  // ---------------------------------------------------------------- styles
  const page = { maxWidth: 720, margin: '0 auto', padding: '16px 16px 120px', fontFamily: 'var(--font-sans)', boxSizing: 'border-box' };
  const muted = { fontSize: 13, color: 'var(--fg-muted)', lineHeight: 1.45 };
  const linkBtn = { background: 'none', border: 'none', color: 'var(--signal)', cursor: 'pointer', fontSize: 13, padding: 0, fontFamily: 'var(--font-sans)' };
  const title15 = { fontSize: 15, fontWeight: 500, color: 'var(--fg)', lineHeight: 1.3 };
  const wrap = { overflowWrap: 'anywhere', minWidth: 0 };

  // ---------------------------------------------------------------- shared bits
  function Loading({ what }) {
    return <div style={{ color: 'var(--fg-muted)', padding: 28, textAlign: 'center', fontSize: 14 }}>Loading {what}…</div>;
  }
  function ErrorCard({ error, onRetry }) {
    return (
      <Card pad={16} style={{ borderColor: 'var(--danger)', marginTop: 12 }}>
        <div style={{ fontSize: 14, marginBottom: 8 }}>Something went wrong: {error}</div>
        {onRetry && <Button kind="secondary" size="sm" onClick={onRetry}>Try again</Button>}
      </Card>
    );
  }
  function Back({ onClick, children }) {
    return <button onClick={onClick} style={{ ...linkBtn, fontSize: 14, marginBottom: 12 }}>‹ {children}</button>;
  }
  function GroupHeader({ label, count, open, onToggle, sub }) {
    return (
      <button onClick={onToggle} style={{
        all: 'unset', cursor: 'pointer', display: 'flex', alignItems: 'baseline', gap: 10, width: '100%',
        boxSizing: 'border-box', padding: sub ? '10px 2px 8px' : '18px 2px 10px',
        borderBottom: sub ? 'none' : '1px solid var(--border)', marginBottom: sub ? 4 : 10,
      }}>
        <span style={{ fontSize: sub ? 14 : 17, fontWeight: 500, color: 'var(--fg)' }}>{label}</span>
        <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-subtle)' }}>{count}</span>
        <span style={{ flex: 1 }} />
        <span style={{ fontSize: 13, color: 'var(--fg-subtle)' }}>{open ? '−' : '+'}</span>
      </button>
    );
  }
  function Line({ label, children, style }) {
    if (children == null || children === '' || children === false) return null;
    return (
      <div style={{ ...muted, marginTop: 4, ...wrap, ...style }}>
        {label && <span style={{ color: 'var(--fg-subtle)' }}>{label} </span>}{children}
      </div>
    );
  }

  // Step segments: passed = filled, current = outlined, future = empty
  function Steps({ step, of, dim }) {
    if (!of) return null;
    return (
      <div style={{ display: 'flex', gap: 4, margin: '8px 0 6px' }} aria-label={`Step ${step} of ${of}`}>
        {[...Array(of)].map((_, i) => {
          const n = i + 1, passed = n < step, cur = n === step;
          return <span key={n} style={{
            flex: 1, maxWidth: 44, height: 8, borderRadius: 2, boxSizing: 'border-box',
            background: passed ? (dim ? 'var(--ink-300)' : 'var(--signal)') : cur ? 'var(--signal-tint)' : 'var(--ink-50)',
            border: cur ? `1.5px solid ${dim ? 'var(--ink-300)' : 'var(--signal)'}` : '1px solid var(--border)',
          }} />;
        })}
      </div>
    );
  }

  // Own results over time (+ optional dotted horizon line). Responsive via viewBox.
  function ResultChart({ points, horizon, unit, better }) {
    const pts = (points || []).map(p => ({ day: p.day, v: num(p.value) })).filter(p => p.v != null);
    if (!pts.length) return null;
    const W = 320, H = 132, L = 40, R = 12, T = horizon ? 22 : 12, B = 22;
    const vals = pts.map(p => p.v).concat(horizon ? [horizon.value] : []);
    let lo = Math.min(...vals), hi = Math.max(...vals);
    if (lo === hi) { lo -= Math.abs(lo) * 0.1 || 1; hi += Math.abs(hi) * 0.1 || 1; }
    const pad = (hi - lo) * 0.12; lo -= pad; hi += pad;
    const t0 = new Date(pts[0].day + 'T12:00:00').getTime(), t1 = new Date(pts[pts.length - 1].day + 'T12:00:00').getTime();
    const x = (p, i) => pts.length === 1 ? L + (W - L - R) / 2
      : (t1 > t0 ? L + ((new Date(p.day + 'T12:00:00').getTime() - t0) / (t1 - t0)) * (W - L - R) : L + (i / (pts.length - 1)) * (W - L - R));
    const y = (v) => T + (1 - (v - lo) / (hi - lo)) * (H - T - B);
    const d = pts.map((p, i) => `${i ? 'L' : 'M'} ${x(p, i).toFixed(1)} ${y(p.v).toFixed(1)}`).join(' ');
    const tick = { fontFamily: 'var(--font-mono)', fontSize: 9, fill: 'var(--fg-subtle)' };
    const last = pts[pts.length - 1];
    return (
      <div style={{ marginTop: 10 }}>
        <svg viewBox={`0 0 ${W} ${H}`} width="100%" style={{ display: 'block', maxWidth: 520 }} role="img"
             aria-label={`Results${unit ? ' in ' + unit : ''}${horizon ? '; ' + horizon.label : ''}`}>
          <line x1={L} x2={W - R} y1={H - B} y2={H - B} style={{ stroke: 'var(--border)' }} />
          <text x={L - 6} y={y(hi - pad) + 3} textAnchor="end" style={tick}>{fmtNum(hi - pad)}</text>
          <text x={L - 6} y={y(lo + pad) + 3} textAnchor="end" style={tick}>{fmtNum(lo + pad)}</text>
          {horizon && <>
            <line x1={L} x2={W - R} y1={y(horizon.value)} y2={y(horizon.value)}
                  style={{ stroke: 'var(--fg-subtle)', strokeWidth: 1, strokeDasharray: '2 4' }} />
            <text x={W - R} y={y(horizon.value) - 5} textAnchor="end" style={{ ...tick, fontSize: 9.5 }}>{horizon.label}</text>
          </>}
          {pts.length > 1 && <path d={d} fill="none" style={{ stroke: 'var(--signal)', strokeWidth: 1.75, strokeLinejoin: 'round', strokeLinecap: 'round' }} />}
          {pts.map((p, i) => <circle key={i} cx={x(p, i)} cy={y(p.v)} r={i === pts.length - 1 ? 3.5 : 2.5} style={{ fill: 'var(--signal)' }} />)}
          <text x={L} y={H - 7} style={tick}>{fmtShort(pts[0].day)}</text>
          {pts.length > 1 && <text x={W - R} y={H - 7} textAnchor="end" style={tick}>{fmtShort(last.day)}</text>}
        </svg>
        <div style={{ ...muted, fontSize: 12, marginTop: 4, fontFamily: 'var(--font-mono)' }}>
          Latest {fmtNum(last.v)}{unit ? ' ' + unit : ''} · {fmtShort(last.day)}{better === 'lower' ? ' · lower is better' : better === 'higher' ? ' · higher is better' : ''}
        </div>
      </div>
    );
  }

  // ============================================================================
  // Screen
  // ============================================================================
  function ProgressScreen({ memberDb, isAdmin, go, sub }) {
    const r = parseSub(sub);
    const [openDomains, setOpenDomains] = useState({ physical: true });
    const [openSports, setOpenSports] = useState({});
    const [openBench, setOpenBench] = useState({ physical: true });

    const needLadders = r.view === 'ladders' || r.view === 'ladder';
    const needBench = r.view === 'benchmarks' || r.view === 'benchmark';
    const ladders = useLoad(needLadders && memberDb ? 'l' + memberDb : null, () => A().getLadders(memberDb));
    const benches = useLoad(needBench && memberDb ? 'b' + memberDb : null, () => A().getBenchmarks(memberDb));
    const tests = useLoad(r.view === 'tests' && memberDb ? 't' + memberDb : null, () => loadTests(memberDb));

    const kid = kidOf(memberDb);
    if (!memberDb) return <div style={page}><Card pad={20} style={{ color: 'var(--fg-muted)' }}>No one selected.</Card></div>;

    return (
      <div style={page}>
        <div style={{ marginBottom: 16, overflowX: 'auto' }}>
          <Segmented value={r.tab} onChange={(v) => go('progress', v)}
                     options={[{ value: 'ladders', label: 'Ladders' }, { value: 'benchmarks', label: 'Benchmarks' }, { value: 'tests', label: 'Tests' }]} />
        </div>

        {r.view === 'ladders' && (ladders.loading ? <Loading what="ladders" />
          : ladders.error ? <ErrorCard error={ladders.error} onRetry={ladders.reload} />
          : <LadderList ladders={ladders.data || []} go={go} isAdmin={isAdmin}
                        openDomains={openDomains} setOpenDomains={setOpenDomains}
                        openSports={openSports} setOpenSports={setOpenSports} />)}

        {r.view === 'ladder' && (ladders.loading ? <Loading what="the ladder" />
          : ladders.error ? <ErrorCard error={ladders.error} onRetry={ladders.reload} />
          : <LadderDetail ladder={(ladders.data || []).find(l => l.progression.id === r.id)} kid={kid} go={go} isAdmin={isAdmin} />)}

        {r.view === 'benchmarks' && (benches.loading ? <Loading what="benchmarks" />
          : benches.error ? <ErrorCard error={benches.error} onRetry={benches.reload} />
          : <BenchmarkList rows={benches.data || []} kid={kid} go={go} open={openBench} setOpen={setOpenBench} />)}

        {r.view === 'benchmark' && (benches.loading ? <Loading what="the benchmark" />
          : benches.error ? <ErrorCard error={benches.error} onRetry={benches.reload} />
          : <BenchmarkDetail b={(benches.data || []).find(b => b.id === r.id)} kid={kid} go={go} memberDb={memberDb} onSaved={benches.reload} />)}

        {r.view === 'tests' && (tests.loading ? <Loading what="test days" />
          : tests.error ? <ErrorCard error={tests.error} onRetry={tests.reload} />
          : <TestsView data={tests.data} go={go} />)}
      </div>
    );
  }

  // ============================================================================
  // LADDERS
  // ============================================================================
  function LadderList({ ladders, go, isAdmin, openDomains, setOpenDomains, openSports, setOpenSports }) {
    if (!ladders.length) return <Card pad={20} style={{ color: 'var(--fg-muted)' }}>No ladders yet. A parent sets your first steps.</Card>;
    const byDomain = {};
    ladders.forEach(l => { const d = l.progression.domain || 'physical'; (byDomain[d] = byDomain[d] || []).push(l); });
    const known = DOMAINS.map(d => d.id);
    const extra = Object.keys(byDomain).filter(d => !known.includes(d)).map(d => ({ id: d, label: d }));
    return (
      <div>
        <div style={{ ...muted, marginBottom: 4 }}>Every ladder is your own next step. Pass the step, then move up one.</div>
        {[...DOMAINS, ...extra].filter(d => byDomain[d.id]).map(d => {
          const list = byDomain[d.id], open = !!openDomains[d.id];
          const toggle = () => setOpenDomains({ ...openDomains, [d.id]: !open });
          return (
            <section key={d.id}>
              <GroupHeader label={d.label} count={list.length} open={open} onToggle={toggle} />
              {open && (d.id === 'skill'
                ? <SkillGroups list={list} go={go} isAdmin={isAdmin} openSports={openSports} setOpenSports={setOpenSports} />
                : <div style={{ display: 'grid', gap: 10 }}>{list.map(l => <LadderCard key={l.progression.id} l={l} go={go} isAdmin={isAdmin} />)}</div>)}
            </section>
          );
        })}
      </div>
    );
  }

  function SkillGroups({ list, go, isAdmin, openSports, setOpenSports }) {
    const bySport = {}, order = [];
    list.forEach(l => {
      const { sport } = splitSport(l.progression.name);
      if (!bySport[sport]) { bySport[sport] = []; order.push(sport); }
      bySport[sport].push(l);
    });
    return (
      <div>
        {order.map(s => {
          const open = !!openSports[s];
          return (
            <div key={s} style={{ marginBottom: 6 }}>
              <GroupHeader sub label={s} count={bySport[s].length} open={open} onToggle={() => setOpenSports({ ...openSports, [s]: !open })} />
              {open && <div style={{ display: 'grid', gap: 10, marginBottom: 10 }}>
                {bySport[s].map(l => <LadderCard key={l.progression.id} l={l} go={go} isAdmin={isAdmin} stripSport />)}
              </div>}
            </div>
          );
        })}
      </div>
    );
  }

  function LadderCard({ l, go, isAdmin, stripSport }) {
    const paused = l.status === 'paused';
    const name = stripSport ? splitSport(l.progression.name).rest : l.progression.name;
    const nextAge = l.next && num(l.next.min_age) > 0 ? num(l.next.min_age) : null;
    return (
      <Card pad={14} hover onClick={() => go('progress', 'ladder:' + l.progression.id)} style={{ opacity: paused ? 0.55 : 1 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <Eyebrow>Step {l.step} of {l.of || '?'}</Eyebrow>
          <span style={{ flex: 1 }} />
          {paused && <Pill>Paused</Pill>}
          {l.pending && !paused && <Pill dot="var(--warning)">Step-up proposed</Pill>}
        </div>
        <div style={{ ...title15, marginTop: 6, ...wrap }}>{name}</div>
        <Steps step={l.step} of={l.of} dim={paused} />
        {paused
          ? <Line>Team sport covers it this season.</Line>
          : <>
              {l.current && <Line label="Now:">{l.current.name}</Line>}
              {l.current && l.current.pass_result && <Line label="To move up:">{l.current.pass_result}</Line>}
              {l.lastResult && <Line label="Last check:">{fmtNum(l.lastResult.value)}{l.current && l.current.unit ? ' ' + l.current.unit : ''} · {fmtShort(l.lastResult.day)}</Line>}
              {l.next
                ? <Line label="Next:">{l.next.name}{nextAge ? ` · opens at ${nextAge}` : ''}</Line>
                : l.of ? <Line>Top step of this ladder.</Line> : null}
              {l.pending && <div style={{ fontSize: 13, color: 'var(--warning)', marginTop: 6 }}>
                Step-up proposed — waiting for a parent{isAdmin ? ' (decide on the Family tab)' : ''}.</div>}
            </>}
      </Card>
    );
  }

  function LadderDetail({ ladder: l, kid, go, isAdmin }) {
    const back = <Back onClick={() => go('progress', 'ladders')}>All ladders</Back>;
    if (!l) return <div>{back}<Card pad={20} style={{ color: 'var(--fg-muted)' }}>This ladder is not on your list.</Card></div>;
    const paused = l.status === 'paused';
    const age = num(kid.age);
    const unit = l.current && l.current.unit;
    return (
      <div>
        {back}
        <Eyebrow>{(DOMAINS.find(d => d.id === l.progression.domain) || {}).label || l.progression.domain} ladder</Eyebrow>
        <h1 style={{ fontSize: 22, fontWeight: 500, letterSpacing: '-0.015em', margin: '6px 0 4px', ...wrap }}>{l.progression.name}</h1>
        <div style={{ ...muted }}>Step {l.step} of {l.of || '?'}{l.member && l.member.since ? ` · on this step since ${fmtShort(l.member.since)}` : ''}</div>
        <Steps step={l.step} of={l.of} dim={paused} />
        {paused && <Card pad={12} style={{ marginTop: 10, background: 'var(--ink-25)' }}><div style={muted}>Paused — team sport covers it this season.</div></Card>}
        {l.pending && <Card pad={12} style={{ marginTop: 10, borderColor: 'var(--warning)', background: 'var(--warning-tint)' }}>
          <div style={{ fontSize: 14 }}>Step-up proposed — waiting for a parent{isAdmin ? ' (decide on the Family tab)' : ''}.</div>
          {l.pending.evidence_value != null && <div style={{ ...muted, marginTop: 4 }}>Evidence: {fmtNum(l.pending.evidence_value)}{unit ? ' ' + unit : ''}</div>}
        </Card>}
        {l.progression.logic && <div style={{ ...muted, marginTop: 12 }}>{l.progression.logic}</div>}

        {/* Current step's exercises */}
        {l.exercises && l.exercises.length > 0 && <>
          <Eyebrow style={{ margin: '22px 0 8px' }}>This step — what you train</Eyebrow>
          <div style={{ display: 'grid', gap: 8 }}>
            {l.exercises.map(e => (
              <Card key={e.id} pad={14}>
                {e.role && e.role !== 'main' && <Eyebrow style={{ marginBottom: 4 }}>{ROLE_LABEL[e.role] || e.role}</Eyebrow>}
                <div style={{ ...title15, ...wrap }}>{e.name}</div>
                <Line>{[e.dose, e.per_week ? `${e.per_week}× a week` : null].filter(Boolean).join(' · ')}</Line>
                <Line label="Effort:">{e.effort}</Line>
                <Line label="Remember:">{e.cue}</Line>
                <Line label="Easier:">{e.easier}</Line>
                <Line label="Where:">{e.venue}</Line>
              </Card>
            ))}
          </div>
        </>}

        {/* Results over time */}
        <Eyebrow style={{ margin: '22px 0 8px' }}>Checkpoint results</Eyebrow>
        <Card pad={14}>
          {l.results && l.results.length
            ? <>
                <ResultChart points={l.results} unit={unit} />
                <div style={{ marginTop: 8 }}>
                  {[...l.results].reverse().slice(0, 6).map((x, i) => (
                    <div key={i} style={{ ...muted, display: 'flex', gap: 10, fontFamily: 'var(--font-mono)', fontSize: 12 }}>
                      <span style={{ width: 90 }}>{fmtDay(x.day)}</span><span>{fmtNum(x.value)}{unit ? ' ' + unit : ''}</span>
                      {x.step != null && <span style={{ color: 'var(--fg-subtle)' }}>step {x.step}</span>}
                    </div>
                  ))}
                </div>
              </>
            : <div style={muted}>No checkpoint result yet. Results logged on a checkpoint day appear here.</div>}
        </Card>

        {/* All steps */}
        <Eyebrow style={{ margin: '22px 0 8px' }}>All steps</Eyebrow>
        <div style={{ display: 'grid', gap: 8 }}>
          {(l.steps || []).map(s => {
            const passed = s.step < l.step, cur = s.step === l.step;
            const gate = num(s.min_age) > 0 ? num(s.min_age) : null;
            const gateOpen = gate == null || (age != null && age >= gate);
            return (
              <div key={s.id || s.step} style={{
                display: 'flex', gap: 12, padding: 14, borderRadius: 8, background: cur ? 'var(--bg-raised)' : 'var(--ink-25)',
                border: `1px solid ${cur ? 'var(--signal)' : 'var(--border)'}`, opacity: !passed && !cur ? 0.8 : 1,
              }}>
                <div style={{ width: 22, flexShrink: 0, textAlign: 'center', fontSize: 15, paddingTop: 1,
                              color: passed ? 'var(--success)' : cur ? 'var(--signal)' : 'var(--fg-faint)' }}>
                  {passed ? '✓' : cur ? '●' : '○'}
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: 'flex', gap: 8, alignItems: 'baseline', flexWrap: 'wrap' }}>
                    <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-subtle)' }}>STEP {s.step}</span>
                    {!passed && !cur && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--fg-subtle)', letterSpacing: '0.06em' }}>LOCKED</span>}
                    {cur && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 10, color: 'var(--signal)', letterSpacing: '0.06em' }}>NOW</span>}
                  </div>
                  <div style={{ ...title15, fontSize: 14, marginTop: 2, color: passed ? 'var(--fg-muted)' : 'var(--fg)', ...wrap }}>{s.name}</div>
                  <Line label="Form:">{s.form_standard}</Line>
                  <Line label="Pass:">{s.pass_result}</Line>
                  {cur && <Line label="Checkpoint:">{s.checkpoint}</Line>}
                  {cur && <Line label="Prepare for next:">{s.next_prep}</Line>}
                  {gate != null && <Line label="Age gate:" style={{ color: gateOpen ? 'var(--fg-muted)' : 'var(--fg)' }}>
                    {gateOpen ? `${gate}+ (open)` : `opens at ${gate}`}</Line>}
                  <Line label="Also needs:">{s.extra_gate}</Line>
                  <Line label="Usually takes:">{s.typical_weeks ? (/^[\d\s–-]+$/.test(String(s.typical_weeks)) ? `${s.typical_weeks} weeks` : String(s.typical_weeks)) : null}</Line>
                </div>
              </div>
            );
          })}
        </div>

        {/* Step history */}
        <Eyebrow style={{ margin: '22px 0 8px' }}>Step history</Eyebrow>
        <Card pad={14}>
          {l.events && l.events.length
            ? l.events.map(e => (
                <div key={e.id} style={{ padding: '6px 0', borderBottom: '1px solid var(--border-faint)' }}>
                  <div style={{ fontSize: 14 }}>
                    Step {e.from_step} → {e.to_step}{' '}
                    <span style={{ fontSize: 12, color: e.status === 'approved' ? 'var(--success)' : e.status === 'rejected' ? 'var(--fg-subtle)' : 'var(--warning)' }}>
                      {e.status === 'approved' ? 'moved up' : e.status === 'rejected' ? 'not yet' : 'waiting for a parent'}
                    </span>
                  </div>
                  <div style={{ ...muted, fontSize: 12 }}>
                    {fmtDay(e.decided_at || e.proposed_at)}
                    {e.evidence_value != null ? ` · result ${fmtNum(e.evidence_value)}${unit ? ' ' + unit : ''}` : ''}
                    {e.note ? ` · ${e.note}` : ''}
                  </div>
                </div>
              ))
            : <div style={muted}>Placed on step {l.step}{l.member && l.member.since ? ` on ${fmtShort(l.member.since)}` : ''}. No step changes yet.</div>}
        </Card>

        {/* Linked benchmarks */}
        {l.benchmarks && l.benchmarks.length > 0 && <>
          <Eyebrow style={{ margin: '22px 0 8px' }}>Benchmarks this ladder builds toward</Eyebrow>
          <div style={{ display: 'grid', gap: 8 }}>
            {l.benchmarks.map(b => (
              <Card key={b.id} pad={12} hover onClick={() => go('progress', 'benchmark:' + b.id)}>
                <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                  <div style={{ flex: 1, fontSize: 14, ...wrap }}>{b.name}</div>
                  <span style={{ color: 'var(--signal)', fontSize: 13, flexShrink: 0 }}>View ›</span>
                </div>
              </Card>
            ))}
          </div>
        </>}
      </div>
    );
  }

  // ============================================================================
  // BENCHMARKS
  // ============================================================================
  function horizonLine(b) {
    if (b.kind !== 'ruler' || !b.refs) return null;
    for (const k of LINE_KEYS) {
      const v = refNumber(b.refs[k]);
      if (v != null) return { value: v, label: `Horizon: ${REF_LABEL[k].replace('Combine avg', 'combine avg')} ${fmtNum(v)}` };
    }
    return null;
  }
  function refRows(b, keys, kid) {
    const refs = b.refs || {};
    return keys.filter(k => refs[k] != null && String(refs[k]).trim() !== '').map(k => ({ k, label: REF_LABEL[k] || k, v: String(refs[k]) }));
  }
  function whenFor(b, kid) {
    const refs = b.refs || {}, n = String(kid.name || '').toLowerCase();
    if (/^axel/.test(n)) return refs.axel_when || null;
    if (/^otto/.test(n)) return refs.otto_when || null;
    return null;
  }
  // Pass-line reading. Threshold comes from the text ("Pass line ~10 cm", "Pass line: 0 fumbles")
  // because benchmark has no numeric pass-line field (see report).
  function passLine(b) {
    const res = b.results || [];
    if (!res.length) return { state: 'none', text: 'Not tested yet' };
    const last = res[res.length - 1], v = num(last.value);
    if (/pass\s*\/\s*fail/i.test(b.unit || '') && v == null && last.text) return /^(pass|yes|ok|over)/i.test(last.text) ? { state: 'over', text: 'Over the line', last } : { state: 'under', text: 'Under the line', last };
    if (/pass\s*\/\s*fail/i.test(b.unit || '')) return v >= 1 ? { state: 'over', text: 'Over the line', last } : { state: 'under', text: 'Under the line', last };
    let t = null;
    Object.values(b.refs || {}).some(s => { const m = String(s).match(/pass line[^\d-]*(-?\d+(?:\.\d+)?)/i); if (m) { t = Number(m[1]); return true; } return false; });
    if (t == null || v == null) return { state: 'unknown', text: 'Tested — a parent reads it against the line', last };
    const lower = b.better === 'lower' || /fumble|drop|miss|error/i.test(b.unit || '');
    const over = lower ? v <= t : v >= t;
    return { state: over ? 'over' : 'under', text: over ? 'Over the line' : 'Under the line', last };
  }

  function BenchmarkList({ rows, kid, go, open, setOpen }) {
    if (!rows.length) return <Card pad={20} style={{ color: 'var(--fg-muted)' }}>No benchmarks yet.</Card>;
    const byDomain = {};
    rows.forEach(b => { const d = b.domain || 'physical'; (byDomain[d] = byDomain[d] || []).push(b); });
    const known = DOMAINS.map(d => d.id);
    const extra = Object.keys(byDomain).filter(d => !known.includes(d)).map(d => ({ id: d, label: d }));
    return (
      <div>
        <div style={{ ...muted, marginBottom: 4 }}>
          Your own results over time. The elite mark is a dotted horizon line: where the road leads, not a number to chase.
        </div>
        {[...DOMAINS, ...extra].filter(d => byDomain[d.id]).map(d => {
          const list = byDomain[d.id], isOpen = !!open[d.id];
          const cats = [], byCat = {};
          list.forEach(b => { const c = b.category || 'Other'; if (!byCat[c]) { byCat[c] = []; cats.push(c); } byCat[c].push(b); });
          return (
            <section key={d.id}>
              <GroupHeader label={d.label} count={list.length} open={isOpen} onToggle={() => setOpen({ ...open, [d.id]: !isOpen })} />
              {isOpen && cats.map(c => (
                <div key={c} style={{ marginBottom: 14 }}>
                  <Eyebrow style={{ margin: '4px 0 8px' }}>{c}</Eyebrow>
                  <div style={{ display: 'grid', gap: 10 }}>
                    {byCat[c].map(b => <BenchmarkCard key={b.id} b={b} kid={kid} onOpen={() => go('progress', 'benchmark:' + b.id)} />)}
                  </div>
                </div>
              ))}
            </section>
          );
        })}
      </div>
    );
  }

  function BenchmarkCard({ b, kid, onOpen, full }) {
    const physical = b.domain === 'physical' || b.domain === 'skill';
    const horizon = horizonLine(b);
    const own = refRows(b, OWN_KEYS, kid);
    const hz = refRows(b, HORIZON_KEYS, kid);
    const when = whenFor(b, kid);
    const pl = b.kind === 'pass_line' ? passLine(b) : null;
    const hasResults = b.results && b.results.length > 0;
    const numeric = (b.results || []).filter(x => x.value != null);
    const latest = hasResults ? b.results[b.results.length - 1] : null;
    const plColor = pl && (pl.state === 'over' ? 'var(--success)' : pl.state === 'under' ? 'var(--warning)' : 'var(--fg-muted)');
    return (
      <Card pad={14} hover={!!onOpen} onClick={onOpen}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
          <Pill>{KIND_LABEL[b.kind] || b.kind || 'benchmark'}</Pill>
          {b.unit && <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-subtle)', ...wrap }}>{b.unit}</span>}
        </div>
        <div style={{ ...title15, marginTop: 8, ...wrap }}>{b.name}</div>
        <Line>{b.measures}</Line>
        {full && <Line label="How:">{b.how_measured || (b.test && b.test.name) || b.method}</Line>}

        {/* His own results */}
        {pl
          ? <div style={{ marginTop: 10, display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
              <span style={{ width: 8, height: 8, borderRadius: 999, background: plColor, display: 'inline-block' }} />
              <span style={{ fontSize: 14, color: 'var(--fg)' }}>{pl.text}</span>
              {pl.last && <span style={{ ...muted, fontSize: 12, fontFamily: 'var(--font-mono)' }}>{fmtNum(pl.last.value)}{b.unit ? ' ' + b.unit : ''} · {fmtShort(pl.last.day)}</span>}
            </div>
          : numeric.length > 1 && (b.kind === 'ruler' || b.kind == null)
            ? <ResultChart points={numeric} horizon={horizon} unit={b.unit} better={b.better} />
            : hasResults
              ? <Line label="Latest:">{latest.value != null ? fmtNum(latest.value) + (b.unit && b.unit.length < 12 ? ' ' + b.unit : '') : latest.text} · {fmtShort(latest.day)}</Line>
              : <div style={{ ...muted, marginTop: 10, fontStyle: 'italic' }}>
                  {physical ? 'No result yet — first test on the baseline day.' : 'No result yet. Log one on this page when it comes.'}
                </div>}

        {/* His measuring stick */}
        {own.length > 0 && <div style={{ marginTop: 10 }}>
          {own.map(x => <Line key={x.k} label={x.label + ':'}>{x.v}</Line>)}
        </div>}
        {when && <Line label="When:">{when}</Line>}
        {b.refs && b.refs.other && <Line label="Note:">{String(b.refs.other)}</Line>}

        {/* The horizon, in words */}
        {hz.length > 0 && <div style={{ marginTop: 10, paddingTop: 8, borderTop: '1px dashed var(--border)' }}>
          <Eyebrow style={{ marginBottom: 4 }}>Horizon</Eyebrow>
          {(full ? hz : hz.slice(0, 3)).map(x => <Line key={x.k} label={x.label + ':'} style={{ marginTop: 2 }}>{x.v}</Line>)}
          {!full && hz.length > 3 && <div style={{ ...muted, fontSize: 12, marginTop: 2, color: 'var(--signal)' }}>+ {hz.length - 3} more</div>}
        </div>}
      </Card>
    );
  }

  // Any benchmark can take a result: a number (a SAT score, a throw) or words (a grade, "form signed off").
  function LogResult({ b, memberDb, onSaved }) {
    const [open, setOpen] = useState(false);
    const [f, setF] = useState({ day: A().today(), value: '', value_text: '', note: '' });
    const [err, setErr] = useState(null);
    const [busy, setBusy] = useState(false);
    const wordy = /A–F|sign-off|pass \/ fail|rubric|profile|courses|index|grade/i.test(b.unit || '') || !b.unit;
    const inp = { padding: '8px 10px', border: '1px solid var(--border-strong)', borderRadius: 6, fontSize: 15,
      background: 'var(--bg-raised)', color: 'var(--fg)', fontFamily: 'var(--font-sans)', boxSizing: 'border-box', minWidth: 0, width: '100%' };
    async function save() {
      setBusy(true); setErr(null);
      try {
        const v = f.value.trim() === '' ? null : Number(f.value.replace(',', '.'));
        if (v != null && Number.isNaN(v)) throw new Error('The number isn’t a number. Put words in the second box.');
        await A().results.add(memberDb, b.id, { day: f.day, value: v, value_text: f.value_text.trim(), note: f.note.trim() });
        setF({ ...f, value: '', value_text: '', note: '' }); setOpen(false); onSaved && onSaved();
      } catch (e) { setErr(e.message); } finally { setBusy(false); }
    }
    if (!open) return <div style={{ marginTop: 12 }}><Button size="sm" onClick={() => setOpen(true)}>Log a result</Button></div>;
    return (
      <Card pad={14} style={{ marginTop: 12 }}>
        <Eyebrow>Log a result</Eyebrow>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 8, marginTop: 8 }}>
          <input type="date" value={f.day} onChange={e => setF({ ...f, day: e.target.value })} style={inp} />
          <input inputMode="decimal" placeholder={`Number${b.unit && b.unit.length < 14 ? ' (' + b.unit + ')' : ''}`} value={f.value}
            onChange={e => setF({ ...f, value: e.target.value })} style={inp} />
          <input placeholder={wordy ? 'In words (e.g. B, signed off, pass)' : 'In words (optional)'} value={f.value_text}
            onChange={e => setF({ ...f, value_text: e.target.value })} style={inp} />
          <input placeholder="Note (optional)" value={f.note} onChange={e => setF({ ...f, note: e.target.value })} style={inp} />
        </div>
        {err && <div style={{ color: 'var(--danger)', fontSize: 13, marginTop: 6 }}>{err}</div>}
        <div style={{ display: 'flex', gap: 8, marginTop: 10 }}>
          <Button size="sm" disabled={busy || (!f.value.trim() && !f.value_text.trim())} onClick={save}>Save</Button>
          <Button size="sm" kind="ghost" onClick={() => setOpen(false)}>Cancel</Button>
        </div>
      </Card>
    );
  }

  function BenchmarkDetail({ b, kid, go, memberDb, onSaved }) {
    const back = <Back onClick={() => go('progress', 'benchmarks')}>All benchmarks</Back>;
    if (!b) return <div>{back}<Card pad={20} style={{ color: 'var(--fg-muted)' }}>This benchmark isn’t on your list.</Card></div>;
    const results = b.results || [];
    return (
      <div>
        {back}
        <BenchmarkCard b={b} kid={kid} full />
        {A().canWrite(memberDb) && <LogResult b={b} memberDb={memberDb} onSaved={onSaved} />}
        {results.length > 0 && <>
          <Eyebrow style={{ margin: '20px 0 8px' }}>All results</Eyebrow>
          <Card pad={14}>
            {[...results].reverse().map((x, i) => (
              <div key={i} style={{ ...muted, display: 'flex', gap: 10, fontFamily: 'var(--font-mono)', fontSize: 12, padding: '2px 0' }}>
                <span style={{ width: 90 }}>{fmtDay(x.day)}</span>
                <span style={{ flex: 1, ...wrap }}>{x.value != null ? fmtNum(x.value) + (b.unit && b.unit.length < 12 ? ' ' + b.unit : '') : ''}{x.text ? (x.value != null ? ' · ' : '') + x.text : ''}{x.note ? ' — ' + x.note : ''}</span>
                <span style={{ color: 'var(--fg-subtle)' }}>{x.source === 'checkpoint' ? 'checkpoint' : x.source === 'logged' ? 'logged' : 'test day'}</span>
                {x.source === 'logged' && A().canWrite(memberDb) && (
                  <button style={{ ...linkBtn, fontSize: 12 }} onClick={async () => {
                    if (!confirm('Remove this result?')) return;
                    try { await A().results.remove(x.id); onSaved && onSaved(); } catch (e) { alert(e.message); }
                  }}>Remove</button>
                )}
              </div>
            ))}
          </Card>
        </>}
        {b.progression_ids && b.progression_ids.length > 0 && <>
          <Eyebrow style={{ margin: '20px 0 8px' }}>Ladders that build it</Eyebrow>
          <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
            {b.progression_ids.map(id => {
              const p = (window.PlanAPI.ladderTag(id, 1) || {});
              return <Button key={id} kind="secondary" size="sm" onClick={() => go('progress', 'ladder:' + id)}>{p.name || id} ›</Button>;
            })}
          </div>
        </>}
      </div>
    );
  }

  // ============================================================================
  // TESTS
  // ============================================================================
  async function loadTests(memberDb) {
    const api = A(), t = api.today();
    const [season, ...weeks] = await Promise.all([
      api.getSeason(memberDb),
      ...[0, 7, 14, 21].map(n => api.getWeek(memberDb, api.addDays(t, n))),
    ]);
    const ladderBy = {};
    (season.ladders || []).forEach(l => { ladderBy[l.progression.id] = l; });
    const events = (season.events || []).filter(e => e.kind === 'test_day' || e.kind === 'checkpoint_week');
    const upcoming = events.filter(e => e.ends_on >= t).sort((a, b) => a.starts_on.localeCompare(b.starts_on));
    const past = events.filter(e => e.ends_on < t).sort((a, b) => b.starts_on.localeCompare(a.starts_on));
    const seen = {};
    const checks = weeks.flatMap(w => w.days.flatMap(d => d.items))
      .filter(i => i.is_checkpoint && i.status !== 'draft' && !seen[i.id] && (seen[i.id] = true))
      .sort((a, b) => a.day.localeCompare(b.day) || String(a.start_time || '').localeCompare(String(b.start_time || '')))
      .map(i => ({ ...i, ladderRow: i.progression_id ? ladderBy[i.progression_id] : null }));
    return { today: t, upcoming, past, checks, from: weeks[0].from, to: weeks[weeks.length - 1].to };
  }

  function TestsView({ data, go }) {
    const [showPast, setShowPast] = useState(false);
    if (!data) return null;
    const { upcoming, past, checks, today } = data;
    return (
      <div>
        <div style={{ ...muted, marginBottom: 16 }}>Results are logged on the Today tab: tap the test item on the day and type the number.</div>

        <Eyebrow style={{ marginBottom: 8 }}>Test days and checkpoint weeks</Eyebrow>
        {upcoming.length === 0
          ? <Card pad={16} style={{ color: 'var(--fg-muted)' }}>No test days left this season.</Card>
          : <div style={{ display: 'grid', gap: 8 }}>
              {upcoming.map(e => <EventCard key={e.id} e={e} today={today} />)}
            </div>}
        {past.length > 0 && <div style={{ marginTop: 10 }}>
          <button onClick={() => setShowPast(!showPast)} style={linkBtn}>{showPast ? 'Hide earlier' : `Show earlier (${past.length})`}</button>
          {showPast && <div style={{ display: 'grid', gap: 8, marginTop: 8, opacity: 0.6 }}>{past.map(e => <EventCard key={e.id} e={e} today={today} />)}</div>}
        </div>}

        <Eyebrow style={{ margin: '26px 0 8px' }}>Checkpoints — the next 4 weeks</Eyebrow>
        {checks.length === 0
          ? <Card pad={16} style={{ color: 'var(--fg-muted)' }}>No checkpoints planned in the next four weeks.</Card>
          : <div style={{ display: 'grid', gap: 8 }}>
              {checks.map(i => <CheckRow key={i.id} i={i} today={today} go={go} />)}
            </div>}
      </div>
    );
  }

  function EventCard({ e, today }) {
    const isWeek = e.kind === 'checkpoint_week';
    const now = e.starts_on <= today && e.ends_on >= today;
    return (
      <Card pad={14} style={{ borderLeft: `3px solid ${isWeek ? 'var(--border-strong)' : 'var(--warning)'}` }}>
        <div style={{ display: 'flex', gap: 8, alignItems: 'baseline', flexWrap: 'wrap' }}>
          <Eyebrow>{isWeek ? 'Checkpoint week' : 'Test day'}</Eyebrow>
          <span style={{ flex: 1 }} />
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: now ? 'var(--signal)' : 'var(--fg-subtle)' }}>
            {now ? (isWeek ? 'this week' : 'today') : whenLabel(e.starts_on)}
          </span>
        </div>
        <div style={{ ...title15, marginTop: 4, ...wrap }}>{e.label}</div>
        <div style={{ ...muted, marginTop: 2 }}>
          {isWeek && e.ends_on !== e.starts_on ? `${fmtDay(e.starts_on)} – ${fmtDay(e.ends_on)}` : fmtDay(e.starts_on)}
        </div>
        {e.note && <Line>{e.note}</Line>}
      </Card>
    );
  }

  function CheckRow({ i, today, go }) {
    const l = i.ladderRow, tag = i.ladder;
    const done = i.status === 'done', skipped = i.status === 'skipped';
    const unit = l && l.current && l.current.unit;
    const record = i.dose || (l && l.current && l.current.checkpoint) || null;
    return (
      <Card pad={14} style={{ background: done || skipped ? 'var(--ink-25)' : 'var(--bg-raised)' }}>
        <div style={{ display: 'flex', gap: 8, alignItems: 'baseline', flexWrap: 'wrap' }}>
          <span style={{ fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--fg-subtle)' }}>
            {fmtDay(i.day)}{i.start_time ? ' · ' + String(i.start_time).slice(0, 5) : ''}
          </span>
          <span style={{ flex: 1 }} />
          {done && <Pill dot="var(--success)">Logged</Pill>}
          {skipped && <Pill>Skipped</Pill>}
          {!done && !skipped && i.day === today && <Pill dot="var(--warning)">Today</Pill>}
        </div>
        <div style={{ ...title15, marginTop: 4, ...wrap }}>{tag ? tag.name : i.title}</div>
        {tag && <div style={{ ...muted, fontSize: 12 }}>Step {tag.step} of {tag.of}{tag.stepName ? ` · ${tag.stepName}` : ''}</div>}
        {tag && i.title && i.title !== tag.name && <Line>{i.title}</Line>}
        <Line label="Record:">{record || (tag ? tag.passResult : null) || i.detail}</Line>
        {tag && tag.passResult && record && <Line label="To move up:">{tag.passResult}</Line>}
        {!tag && record && i.detail && <Line>{i.detail}</Line>}
        {done && i.result_value != null && <Line label="Result:">{fmtNum(i.result_value)}{unit ? ' ' + unit : ''}</Line>}
        {tag && <button onClick={() => go('progress', 'ladder:' + tag.id)} style={{ ...linkBtn, marginTop: 8 }}>Open the ladder ›</button>}
      </Card>
    );
  }

  window.ProgressScreen = ProgressScreen;
})();
