// ─── RACE ANALYSIS (3/3) — lap time heatmap · stint analysis · driver cards ──────────────────
const fra3Css = `
.fra-hm{display:grid;gap:2px;min-width:0}
.fra-hm-r{display:grid;gap:2px;align-items:stretch;transition:opacity .15s}
.fra-hm-n{display:flex;align-items:center;gap:8px;padding-left:8px;font:800 13px/1 'Archivo',sans-serif;text-transform:uppercase;color:${COLORS.text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
.fra-hm-n em{font:500 12px/1 'JetBrains Mono',monospace;font-style:normal;color:${COLORS.textMute};min-width:20px;text-align:right}
.fra-hm-c{height:24px;display:grid;place-items:center;font:600 11px/1 'JetBrains Mono',monospace;color:#0b0b0d;font-variant-numeric:tabular-nums;overflow:hidden;min-width:0}
.fra-hm-h{font:500 12px/1 'JetBrains Mono',monospace;color:${COLORS.textDim};text-align:center;padding-bottom:4px}
.fra-lgd{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;margin-top:14px;font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.04em;color:${COLORS.textMute}}
.fra-lgd span{display:inline-flex;align-items:center;gap:7px}
.fra-lgd i{display:inline-block;width:14px;height:14px}
.fra-tbl{width:100%;border-collapse:collapse;margin-top:16px;font:500 13px/1.3 'JetBrains Mono',monospace;color:${COLORS.textMute};font-variant-numeric:tabular-nums}
.fra-tbl th{font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.12em;text-transform:uppercase;color:${COLORS.textDim};text-align:right;padding:8px 10px;border-bottom:1px solid ${COLORS.hairline}}
.fra-tbl td{text-align:right;padding:9px 10px;border-bottom:1px solid rgba(255,255,255,.05)}
.fra-tbl th:first-child,.fra-tbl td:first-child{text-align:left}
.fra-tbl b{color:${COLORS.text};font-weight:700}
.fra-tyb{display:inline-grid;place-items:center;width:22px;height:22px;border-radius:999px;font:800 11px/1 'Archivo',sans-serif;border:3px solid;box-sizing:border-box;background:#000}
.fra-cards{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:10px}
.fra-card{all:unset;box-sizing:border-box;cursor:pointer;position:relative;display:grid;gap:14px;padding:16px 16px 14px 20px;border:1px solid ${COLORS.hairline};background:rgba(255,255,255,.02);min-width:0}
.fra-card:hover{border-color:rgba(255,255,255,.4)}
.fra-card .bar{position:absolute;left:0;top:0;bottom:0;width:4px}
.fra-card .top{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:12px}
.fra-card .pos{font:900 34px/.9 'Archivo',sans-serif;letter-spacing:-.03em;color:${COLORS.text}}
.fra-card .nm{font:900 18px/1.05 'Archivo',sans-serif;text-transform:uppercase;color:${COLORS.text};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.fra-card .tm{margin-top:4px;font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.1em;text-transform:uppercase;color:${COLORS.textMute}}
.fra-card .car{width:64px;height:auto;display:block}
.fra-card dl{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px 10px;margin:0}
.fra-card dt{font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.1em;text-transform:uppercase;color:${COLORS.textDim}}
.fra-card dd{margin:5px 0 0;font:800 17px/1 'Archivo',sans-serif;color:${COLORS.text};font-variant-numeric:tabular-nums;white-space:nowrap}
.fra-card dd small{font:500 12px/1 'JetBrains Mono',monospace;color:${COLORS.textMute};margin-left:5px}
.fra-card .strat{display:flex;height:8px;gap:2px}
.fra-card.big{cursor:default;padding:24px 26px 22px 30px;gap:20px}
.fra-card.big .pos{font-size:clamp(48px,5vw,72px)}
.fra-card.big .nm{font-size:clamp(26px,3vw,40px)}
.fra-card.big .car{width:120px}
.fra-card.big dl{grid-template-columns:repeat(auto-fit,minmax(130px,1fr))}
.fra-card.big dd{font-size:24px}
.fra-card.big .strat{height:14px}
`;
const fraHeat = (d) => { if (d <= 0.0005) return '#B44DFF'; const f = Math.min(1, d / 2.5); return `oklch(0.74 0.15 ${(150 - 125 * f).toFixed(0)})`; };
const fraDeg = (v) => (v == null ? '—' : (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(3) + 's');
const fraOn = (id, foc) => !foc || !foc.length || foc.includes(id);

function FRAHeatmap({ S, drv, foc, onPick }) {
  const [box, W] = useFraWidth();
  const L = S.cars[0].done, best = S.fastest ? S.fastest.t : 0, nameW = W < 640 ? 110 : 168;
  const cw = (W - nameW - L * 2) / L, cols = `${nameW}px repeat(${L}, minmax(0,1fr))`;
  return (
    <div ref={box}>
      <div className="fra-hm">
        <div className="fra-hm-r" style={{ gridTemplateColumns: cols }}><span></span>{Array.from({ length: L }, (_, i) => <span key={i} className="fra-hm-h">{cw >= 18 || (i + 1) % 2 === 0 ? i + 1 : ''}</span>)}</div>
        {S.cars.map((c) => { const d = drv[c.id];
          return (
            <div key={c.id} className="fra-hm-r" style={{ gridTemplateColumns: cols, opacity: fraOn(c.id, foc) ? 1 : 0.28 }}>
              <span className="fra-hm-n" style={{ boxShadow: `inset 3px 0 0 ${d.color}` }} onClick={() => onPick(c.id)}><em>{c.dnf ? '—' : c.pos}</em>{d.label}</span>
              {Array.from({ length: L }, (_, i) => { const l = c.laps[i];
                if (!l) return <span key={i} className="fra-hm-c" style={{ background: 'rgba(255,255,255,0.025)' }}></span>;
                const pb = l.t === c.pb, fast = S.fastest && S.fastest.id === c.id && S.fastest.lap === l.n, ty = FRA_TYRE[l.c];
                const bg = l.pit ? 'repeating-linear-gradient(45deg,#2c2c31 0 3px,#18181b 3px 6px)' : fraHeat(l.t - best);
                return <span key={i} className="fra-hm-c" title={`${d.label} · lap ${l.n} · ${fraFmt(l.t)}${ty ? ' · ' + ty[2] + ' (lap ' + l.age + ' on the set)' : ''}${l.pit ? ' · pit lap' : ''}${fast ? ' · fastest lap' : pb ? ' · personal best' : ''}`}
                  style={{ background: bg, opacity: l.n === 1 ? 0.55 : 1, color: l.pit ? COLORS.textMute : '#0b0b0d', boxShadow: pb && !fast ? 'inset 0 0 0 2px #fff' : null }}>{l.pit ? 'P' : cw >= 34 ? l.t.toFixed(1) : ''}</span>; })}
            </div>); })}
      </div>
      <div className="fra-lgd">
        <span><i style={{ background: '#B44DFF' }}></i>Fastest lap</span>
        <span><i style={{ background: 'linear-gradient(90deg, oklch(0.74 0.15 150), oklch(0.74 0.15 88), oklch(0.74 0.15 25))', width: 64 }}></i>0 → +2.5s off the fastest</span>
        <span><i style={{ boxShadow: 'inset 0 0 0 2px #fff' }}></i>Personal best</span>
        <span><i style={{ background: 'repeating-linear-gradient(45deg,#2c2c31 0 3px,#18181b 3px 6px)' }}></i>Pit lap</span>
      </div>
    </div>);
}

function FRAStints({ S, drv, foc = [] }) {
  const setHover = () => {};
  const [xm, setXm] = React.useState('age'); // x = laps on the set | race lap
  const [cond, setCond] = React.useState('all'); // track: all | dry | wet (wet races only)
  const wetOn = !!S.wet, cd = wetOn ? cond : 'all', inC = (l) => cd === 'all' || (cd === 'wet' ? l.wet >= S.wet.th : l.wet < S.wet.th);
  const ax = (l) => (xm === 'age' ? l.age : l.n);
  const [box, W] = useFraWidth();
  const [tip, setTip] = React.useState(null);
  const H = W < 640 ? 300 : 360, M = { l: 52, r: 16, t: 12, b: 34 }, pw = W - M.l - M.r, ph = H - M.t - M.b;
  const lines = []; S.cars.forEach((c) => c.stints.forEach((s, i) => { const pts = c.laps.filter((l) => l.si === i && l.clean && inC(l)); if (pts.length > (cd === 'all' ? 1 : 0)) lines.push({ c, i, comp: s.c, pts }); }));
  const ts = lines.flatMap((x) => x.pts.map((l) => l.t)).sort((a, b) => a - b), q = (f) => ts[Math.floor(f * (ts.length - 1))] || 0;
  const maxA = xm === 'lap' ? S.cars[0].done : Math.max(2, ...lines.flatMap((x) => x.pts.map((l) => l.age)));
  const yT = Math.floor((q(0) - 0.2) * 2) / 2, yB = Math.ceil((q(0.99) + 0.3) * 2) / 2;
  const X = (a) => M.l + ((a - 1) / Math.max(1, maxA - 1)) * pw, Y = (t) => M.t + Math.max(0, Math.min(1, (t - yT) / (yB - yT))) * ph;
  const med = {}; lines.forEach((x) => x.pts.forEach((l) => { ((med[x.comp] = med[x.comp] || {})[ax(l)] = med[x.comp][ax(l)] || []).push(l.t); }));
  const medLine = Object.entries(med).map(([k, byAge]) => ({ k, pts: Object.entries(byAge).filter(([, v]) => v.length >= 2).map(([a, v]) => { v.sort((p, r) => p - r); return [+a, v[Math.floor(v.length / 2)]]; }).sort((p, r) => p[0] - r[0]) })).filter((m) => m.pts.length > 1);
  const yt = []; for (let v = Math.ceil(yT); v <= yB; v += yB - yT > 6 ? 2 : 1) yt.push(v);
  const xt = []; for (let a = 1; a <= maxA; a += maxA > 16 ? 2 : 1) xt.push(a);
  const move = (e) => { const r = e.currentTarget.getBoundingClientRect(), mx = e.clientX - r.left, my = e.clientY - r.top; let b = null, bd = 1e9;
    lines.forEach((x) => { if (foc.length && !foc.includes(x.c.id)) return; x.pts.forEach((l) => { const d = Math.hypot(X(ax(l)) - mx, Y(l.t) - my); if (d < bd) { bd = d; b = { x, l }; } }); });
    if (b && bd < 24) { setTip(b); setHover(b.x.c.id); } else { setTip(null); setHover(null); } };
  const selCars = foc.map((id) => S.cars.find((c) => c.id === id)).filter(Boolean);
  const fmean = (a) => a.reduce((s, v) => s + v, 0) / a.length;
  const rowsOf = (c) => cd === 'all' ? c.stintStats.map((s, i) => ({ ...s, k: i + 1 })) : c.stints.map((s, i) => { // same stats, only the laps run in the chosen conditions
    const L = c.laps.filter((l) => l.si === i && inC(l)), cl = L.filter((l) => l.clean);
    let deg = null; if (cl.length >= 4) { const mx = fmean(cl.map((l) => l.age)), my = fmean(cl.map((l) => l.t)), sxx = cl.reduce((q, l) => q + (l.age - mx) ** 2, 0); deg = sxx ? cl.reduce((q, l) => q + (l.age - mx) * (l.t - my), 0) / sxx : null; }
    return { k: i + 1, c: s.c, laps: L.length, first: L.length ? L[0].n : null, last: L.length ? L[L.length - 1].n : null, avg: cl.length ? fmean(cl.map((l) => l.t)) : null, best: cl.length ? Math.min(...cl.map((l) => l.t)) : null, deg };
  }).filter((s) => s.laps);
  const comps = ['S', 'M', 'H', 'I', 'W'].map((k) => { const st = S.cars.flatMap((c) => rowsOf(c).filter((s) => s.c === k)); const av = st.filter((s) => s.avg != null), dg = st.filter((s) => s.deg != null);
    return st.length ? { k, n: st.length, laps: st.reduce((a, s) => a + s.laps, 0), avg: av.length ? av.reduce((a, s) => a + s.avg, 0) / av.length : null, best: av.length ? Math.min(...av.map((s) => s.best)) : null, deg: dg.length ? dg.reduce((a, s) => a + s.deg, 0) / dg.length : null } : null; }).filter(Boolean);
  const badge = (k) => { const t = FRA_TYRE[k] || ['#555', '#fff', '?']; return <span className="fra-tyb" style={{ borderColor: t[0], color: t[0] === '#F2F2F2' ? '#fff' : t[0] }}>{k || '?'}</span>; };
  return (
    <div ref={box} style={{ position: 'relative' }}>
      <div style={{ display: 'flex', flexWrap: 'wrap', justifyContent: 'flex-end', gap: 8, marginTop: -6, marginBottom: 6 }}>{wetOn && <span className="fra-tabs" role="group" aria-label="Track">{[['all', 'All laps'], ['dry', 'Dry track'], ['wet', 'Wet track']].map(([k, l]) => <button key={k} className="fra-tab" aria-pressed={cd === k ? 'true' : 'false'} onClick={() => setCond(k)} style={k === 'wet' && cd === k ? { background: FRA_WET } : null}>{l}</button>)}</span>}<span className="fra-tabs" role="group" aria-label="X axis">{[['age', 'Laps on the set'], ['lap', 'Race lap']].map(([k, l]) => <button key={k} className="fra-tab" aria-pressed={xm === k ? 'true' : 'false'} onClick={() => setXm(k)}>{l}</button>)}</span></div>
      <svg width={W} height={H} onMouseMove={move} onMouseLeave={() => { setTip(null); setHover(null); }} style={{ display: 'block', cursor: 'crosshair' }}>
        {yt.map((v) => <g key={v}><line x1={M.l} x2={M.l + pw} y1={Y(v)} y2={Y(v)} stroke="rgba(255,255,255,0.07)"></line><text x={M.l - 10} y={Y(v) + 4} textAnchor="end" className="fra-tick">{v}s</text></g>)}
        {xt.map((a) => <text key={a} x={X(a)} y={H - 10} textAnchor="middle" className="fra-tick">{a}</text>)}
        {lines.map((x) => { const me = foc.includes(x.c.id); return <polyline key={x.c.id + x.i} points={x.pts.map((l) => X(ax(l)).toFixed(1) + ',' + Y(l.t).toFixed(1)).join(' ')} fill="none" stroke={(FRA_TYRE[x.comp] || ['#777'])[0]} strokeWidth={me ? 2.8 : 1.2} opacity={foc.length ? (me ? 1 : 0.07) : 0.3}></polyline>; })}
        {!foc.length && xm === 'age' && medLine.map((m) => <g key={m.k}><polyline points={m.pts.map(([a, t]) => X(a) + ',' + Y(t)).join(' ')} fill="none" stroke="#000" strokeWidth="6" opacity="0.6"></polyline><polyline points={m.pts.map(([a, t]) => X(a) + ',' + Y(t)).join(' ')} fill="none" stroke={FRA_TYRE[m.k][0]} strokeWidth="3.2"></polyline></g>)}
        {lines.filter((x) => foc.includes(x.c.id)).flatMap((x) => x.pts.map((l) => <circle key={x.c.id + '-' + x.i + '-' + l.n} cx={X(ax(l))} cy={Y(l.t)} r="3.2" fill={FRA_TYRE[x.comp][0]} stroke={drv[x.c.id].color} strokeWidth="1.5"></circle>))}
      </svg>
      {tip && <div className="fra-tip" style={{ left: Math.min(W - 220, X(ax(tip.l)) + 12), top: Math.max(0, Y(tip.l.t) - 64) }}><b style={{ color: drv[tip.x.c.id].color }}>{drv[tip.x.c.id].label}</b><span>Stint {tip.x.i + 1} · {FRA_TYRE[tip.x.comp] ? FRA_TYRE[tip.x.comp][2] : '?'} · lap {tip.l.age} on the set</span><span>Race lap {tip.l.n} · {fraFmt(tip.l.t)}</span></div>}
      <div className="fra-lgd"><span>{xm === 'age' ? 'X = laps on the set (1 = out-lap)' : 'X = race lap'} · Y = lap time, faster at the top · clean laps only (no lap 1, pit laps or laps over 107%){cd === 'wet' ? ' · wet-track laps only' : cd === 'dry' ? ' · dry-track laps only' : ''}</span>{!foc.length && xm === 'age' && <span>Thick line = median per compound</span>}</div>
      <table className="fra-tbl">
        {selCars.length ? <React.Fragment>
          <thead><tr>{selCars.length > 1 && <th>Driver</th>}<th>Stint</th><th>Tyre</th><th>Laps</th><th>Length</th><th>Average</th><th>Best</th><th>Deg / lap</th></tr></thead>
          <tbody>{selCars.flatMap((c) => rowsOf(c).map((s, i) => <tr key={c.id + '-' + s.k}>{selCars.length > 1 && <td style={{ boxShadow: `inset 3px 0 0 ${drv[c.id].color}`, paddingLeft: 12 }}><b>{i === 0 ? drv[c.id].label : ''}</b></td>}<td><b>{s.k}</b></td><td>{badge(s.c)}</td><td>{s.first != null ? `L${s.first}–${s.last}` : '—'}</td><td>{s.laps}</td><td><b>{fraFmt(s.avg)}</b></td><td>{fraFmt(s.best)}</td><td>{fraDeg(s.deg)}</td></tr>))}</tbody>
        </React.Fragment> : <React.Fragment>
          <thead><tr><th>Compound</th><th>Stints</th><th>Laps</th><th>Avg pace</th><th>Best</th><th>Avg deg / lap</th></tr></thead>
          <tbody>{comps.map((m) => <tr key={m.k}><td style={{ display: 'flex', alignItems: 'center', gap: 10 }}>{badge(m.k)}<b>{FRA_TYRE[m.k][2]}</b></td><td>{m.n}</td><td>{m.laps}</td><td><b>{fraFmt(m.avg)}</b></td><td>{fraFmt(m.best)}</td><td>{fraDeg(m.deg)}</td></tr>)}</tbody>
        </React.Fragment>}
      </table>
    </div>);
}

function FRADriverCard({ S, c, d, big, onClick }) {
  const n = S.cars.length, L = S.cars[0].done;
  // Small cards keep three numbers (user 2026-10-13: "too wordy") — the finish is the big P on top, stops are in the strategy
  // bar; the focused (big) card lists the rest.
  const stat = [
    ['Grid', <span>P{c.grid}{c.gained ? <small style={{ color: c.gained > 0 ? '#46C178' : COLORS.red }}>{c.gained > 0 ? '▲' + c.gained : '▼' + -c.gained}</small> : null}</span>],
    ['Best lap', <span>{fraFmt(c.pb)}{c.pbRank ? <small style={{ color: c.pbRank === 1 ? '#C98BFF' : null }}>#{c.pbRank}</small> : null}</span>],
    ['Pace', <span>{fraFmt(c.pace)}{c.paceRank ? <small>#{c.paceRank}</small> : null}</span>],
    ...(big ? [['Consistency', c.sd != null ? '±' + c.sd.toFixed(2) + 's' : '—'], ['Pit stops', String(c.pits.length)], ['Laps led', String(c.led)], ['Gap', c.pos === 1 && !c.dnf ? 'Winner' : c.dnf ? 'Lap ' + c.done : typeof c.gap === 'string' ? c.gap : '+' + c.gap.toFixed(3) + 's'], ['Race time', c.dnf ? '—' : fraFmt(c.time)], ['Laps', c.done + ' / ' + L]] : [])];
  const Tag = big ? 'div' : 'button';
  return (
    <Tag className={'fra-card' + (big ? ' big' : '')} onClick={onClick}>
      <span className="bar" style={{ background: d.color }}></span>
      <div className="top">
        <span className="pos">{c.dnf ? 'DNF' : 'P' + c.pos}</span>
        <div style={{ minWidth: 0 }}><div className="nm">{d.label}</div><div className="tm">{d.team || 'Independent'}{big ? ` · ${n} starters` : ''}</div></div>
        {d.car ? <img className="car" src={d.car} alt=""></img> : <span></span>}
      </div>
      <dl>{stat.map(([k, v]) => <div key={k}><dt>{k}</dt><dd>{v}</dd></div>)}</dl>
      <div className="strat" title="Tyre strategy">{c.stints.map((s, i) => <span key={i} style={{ flex: Math.max(0.01, s.to - s.from), background: (FRA_TYRE[s.c] || ['#555'])[0] }}></span>)}</div>
    </Tag>);
}

function FRADriverCards({ S, drv, onPick }) {
  return <div className="fra-cards">{S.cars.map((c) => <FRADriverCard key={c.id} S={S} c={c} d={drv[c.id]} onClick={() => onPick(c.id)} />)}</div>;
}

Object.assign(window, { fra3Css, FRAHeatmap, FRAStints, FRADriverCard, FRADriverCards });
