// ─── RACE ANALYSIS — Lap comparison: track map (sectors · speed · dominance) + gap along the lap ─────────
// Data: frsLapTrace / frsSectors / useLapTraces (ur2d-replay-stats.jsx). Mounted under the Lap times heatmap.
// Laps follow the sticky driver chips: none pinned = fastest lap vs the next-fastest driver's best; one pinned =
// their best vs the fastest lap; 2–4 pinned = their best laps. Each row's lap can be changed in its dropdown.
// Palette: purple/green keep their F1 timing meaning. Compared laps wear their drivers' team colours, as on the race trace —
// on every map mode, the table and the gap graph (user 2026-10-13 — no fixed/grey default colours).
const FLC_PURPLE = '#B98CFF', FLC_GREEN = '#4CD08A', FLC_MINI = 20, FLC_CAR_M = 5.6;
const flcCss = `
.flc-rows{width:100%;border-collapse:collapse;font:500 13px/1.3 'JetBrains Mono',monospace;color:${COLORS.textMute};font-variant-numeric:tabular-nums}
.flc-rows th{font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.12em;text-transform:uppercase;color:${COLORS.textDim};text-align:right;padding:0 10px 9px;border-bottom:1px solid ${COLORS.hairline};white-space:nowrap}
.flc-rows td{text-align:right;padding:7px 10px;border-bottom:1px solid rgba(255,255,255,.05);white-space:nowrap}
.flc-rows th:first-child,.flc-rows td:first-child{text-align:left;padding-left:12px}
.flc-rows td:last-child{width:1%}
.flc-rows small{color:${COLORS.textDim};margin-left:4px}
.flc-nm{font:800 14px/1 'Archivo',sans-serif;text-transform:uppercase;color:${COLORS.text}}
.flc-tag{margin-left:9px;font:600 10px/1 'JetBrains Mono',monospace;letter-spacing:.14em;text-transform:uppercase;padding:3px 6px;border:1px solid rgba(255,255,255,.35);color:${COLORS.text};vertical-align:2px}
.flc-tag.dim{border-color:${COLORS.hairline};color:${COLORS.textDim}}
.flc-btn{all:unset;cursor:pointer;font:500 11px/1 'JetBrains Mono',monospace;letter-spacing:.1em;text-transform:uppercase;color:${COLORS.textMute};padding:6px 9px;border:1px solid ${COLORS.hairline}}
.flc-btn:hover,.flc-btn:focus-visible{color:#fff;border-color:#fff}
.flc-pw{position:relative;display:inline-block}
.flc-pw::after{content:"";position:absolute;right:10px;top:50%;margin-top:-2px;border-left:4px solid transparent;border-right:4px solid transparent;border-top:5px solid ${COLORS.textMute};pointer-events:none}
.flc-pick{appearance:none;-webkit-appearance:none;font:500 12px/1 'JetBrains Mono',monospace;color:${COLORS.text};background:#0b0b0d;border:1px solid rgba(255,255,255,.22);border-radius:0;padding:7px 28px 7px 10px;cursor:pointer;color-scheme:dark}
.flc-pick:hover,.flc-pick:focus-visible{border-color:#fff;outline:none}
.flc-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.15fr);gap:28px clamp(24px,3vw,44px);align-items:start;margin-top:26px}
@media (max-width:1100px){.flc-grid{grid-template-columns:minmax(0,1fr)}}
.flc-card{border:0;border-top:1px solid ${COLORS.hairline};padding:16px 0 0;min-width:0}
.flc-ch{display:flex;flex-wrap:wrap;align-items:center;gap:10px 14px;margin-bottom:14px;min-height:34px}
.flc-ct{font:800 15px/1.2 'Archivo',sans-serif;letter-spacing:.06em;text-transform:uppercase;color:${COLORS.text}}
.flc-ct b{font-weight:800}
.flc-ch .fra-tabs{margin-left:auto}
.fra-panel .fra-ph .flc-unit{margin-left:auto}
.fra-panel .fra-tabs .fra-tab{white-space:nowrap}
.flc-scroll{overflow-x:auto;overflow-y:hidden}
.flc-wait{display:grid;place-items:center;min-height:220px}
.flc-sw{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:10px;vertical-align:0}
`;
// Speed ramp (user 2026-10-13): red (slow) → green (fast), mixed in oklch so the middle passes through amber, not mud.
const flcSpeedCol = (f) => { f = Math.max(0, Math.min(1, isFinite(f) ? f : 0)); return `color-mix(in oklch, #E10600 ${Math.round((1 - f) * 100)}%, ${FLC_GREEN})`; };
const flcSigned = (v) => (v >= 0 ? '+' : '−') + Math.abs(v).toFixed(3);
let _flcCtx = null;
const flcRgb = (c) => { _flcCtx = _flcCtx || document.createElement('canvas').getContext('2d'); _flcCtx.fillStyle = '#000'; _flcCtx.fillStyle = c; const s = _flcCtx.fillStyle; const m = s.match(/^#([0-9a-f]{6})$/i); if (m) return [0, 2, 4].map((i) => parseInt(m[1].slice(i, i + 2), 16)); const r = s.match(/[\d.]+/g); return r ? r.slice(0, 3).map(Number) : [128, 128, 128]; };
const flcHex = (a) => '#' + a.map((v) => Math.round(Math.max(0, Math.min(255, v))).toString(16).padStart(2, '0')).join('');
// sRGB ⇄ OKLab/OKLCH (Ottosson's matrices): dark team colours are lifted in lightness without losing hue or chroma.
const flcLin = (v) => { v /= 255; return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4; };
const flcGam = (v) => 255 * (v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055);
function flcOklab(rgb) {
  const [R, G, B] = rgb.map(flcLin);
  const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B), m = Math.cbrt(0.2119034982 * R + 0.6806995451 * G + 0.1073969566 * B), s = Math.cbrt(0.0883024619 * R + 0.2817188376 * G + 0.6299787005 * B);
  return [0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s, 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s, 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s];
}
function flcFromLch(L, C, h) { // → sRGB 0–255; chroma eased off until the colour fits the gamut
  for (let c = C; ; c *= 0.94) {
    const a = c * Math.cos(h), b = c * Math.sin(h);
    const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3, m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3, s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;
    const v = [4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s];
    if (c < 0.004 || v.every((x) => x >= -1e-4 && x <= 1.0001)) return v.map((x) => flcGam(Math.max(0, Math.min(1, x))));
  }
}
// Compared laps' colours, whole panel (user 2026-10-13): each driver's race colour (fraIdentify: team primary, or its secondary
// when a higher team has a look-alike), lifted in OKLCH lightness only so reds stay red. A teammate — or a look-alike that's
// left — is striped two-tone with the team's other colour (chosen over black dashes after a side-by-side); no other colour →
// dashed. `mate` = the earlier row it's told apart from.
function flcRowStyle(rows, drv) {
  const used = [], near = (u, p) => (typeof fraNear === 'function' ? fraNear([u.L, u.A, u.B], [p.L, p.A, p.B]) : Math.hypot(u.L - p.L, u.A - p.A, u.B - p.B) < 0.06);
  const lift = (col) => { const [L0, A, B] = flcOklab(flcRgb(col)), L = Math.max(L0, 0.62); return { L, A, B, hex: flcHex(flcFromLch(L, Math.hypot(A, B), Math.atan2(B, A))) }; };
  return rows.map((r) => {
    const d = drv[r.id] || {}, p = lift(d.color || '#888'), q = d.color2 ? lift(d.color2) : null;
    let mate = used.findIndex((u) => (d.team && u.team === d.team) || near(u, p));
    if (mate < 0) mate = null;
    const mateTeam = mate != null && !!d.team && used[mate].team === d.team;
    used.push({ team: d.team || null, L: p.L, A: p.A, B: p.B });
    const qOk = !!q && Math.hypot(q.L - p.L, q.A - p.A, q.B - p.B) > 0.1;
    // team striped race-wide (fraIdentify: its colour and secondary both taken) → its 2nd car here is dashed instead
    if (d.stripe && qOk) return mateTeam ? { c: p.hex, c2: null, dash: true, mate } : { c: p.hex, c2: q.hex, dash: false, mate };
    const two = mate != null && qOk;
    return { c: p.hex, c2: two ? q.hex : null, dash: mate != null && !two, mate };
  });
}
const flcDomSw = (d) => (d.c2 ? `repeating-linear-gradient(90deg, ${d.c} 0 4px, ${d.c2} 4px 8px)` : d.dash ? `repeating-linear-gradient(90deg, ${d.c} 0 4px, transparent 4px 7px)` : d.c);
function flcBestLap(c) { let b = null; c.laps.forEach((l) => { if (l.n > 1 && !l.pit && (!b || l.t < b.t)) b = l; }); return b ? b.n : c.laps.length ? c.laps[0].n : null; }
function flcCalc(V) { // gaps to the reference along the lap, mini-sector winners, the reference lap's speed range, top speeds
  const { tr, ri } = V, R = tr[ri]; if (!R) return null;
  const K = R.K, gaps = tr.map((t) => (t ? Float64Array.from(R.t, (v, k) => t.t[k] - v) : null));
  const win = [], cnt = tr.map(() => 0);
  for (let m = 0; m < FLC_MINI; m++) {
    const i0 = Math.round((m * K) / FLC_MINI), i1 = Math.round(((m + 1) * K) / FLC_MINI); let w = -1, bt = Infinity;
    tr.forEach((t, i) => { if (!t) return; const d = t.t[i1] - t.t[i0]; if (d < bt) { bt = d; w = i; } });
    win.push({ i0, i1, w }); if (w >= 0) cnt[w]++;
  }
  const sec = [0, 1, 2].map((j) => { // sector (equal thirds) winners — sectors map mode colours each whole sector by its winner
    const i0 = Math.round((K * j) / 3), i1 = Math.round((K * (j + 1)) / 3); let w = -1, bt = Infinity;
    tr.forEach((t, i) => { if (!t) return; const d = t.t[i1] - t.t[i0]; if (d < bt) { bt = d; w = i; } });
    return { i0, i1, w };
  });
  const vs = Array.from(R.v).filter((v) => isFinite(v)).sort((a, b) => a - b), q = (f) => vs[Math.floor(f * (vs.length - 1))];
  const top = tr.map((t) => { if (!t) return null; let m = -Infinity; for (const v of t.v) if (isFinite(v) && v > m) m = v; return isFinite(m) ? m : null; });
  return { R, K, gaps, win, cnt, sec, lo: q(0.02), hi: q(0.995), top };
}

function FRALapMap({ T, calc, rows, cols, dom, mode, hover, setHover }) {
  const [box, W] = useFraWidth();
  const R = calc.R, K = calc.K;
  const okp = (k) => isFinite(R.x[k]) && isFinite(R.y[k]);
  const geo = React.useMemo(() => {
    let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
    for (let k = 0; k <= K; k++) { if (!okp(k)) continue; x0 = Math.min(x0, R.x[k]); x1 = Math.max(x1, R.x[k]); y0 = Math.min(y0, R.y[k]); y1 = Math.max(y1, R.y[k]); }
    const pad = Math.max(x1 - x0, y1 - y0) * 0.05; return { vx: x0 - pad, vy: y0 - pad, vw: x1 - x0 + 2 * pad, vh: y1 - y0 + 2 * pad };
  }, [R]);
  const Hpx = Math.round(Math.min(W < 640 ? 300 : 430, (W * geo.vh) / geo.vw)), u = Math.max(geo.vw / W, geo.vh / Hpx); // u = world units per screen px
  const poly = (a, b) => { const o = []; for (let k = a; k <= b; k++) if (okp(k)) o.push(R.x[k].toFixed(1) + ',' + R.y[k].toFixed(1)); return o.join(' '); };
  const rowKey = rows.map((r) => r.id).join() + cols.join() + dom.map((d) => d.c + (d.c2 || '') + (d.dash ? '-' : '')).join();
  const segs = React.useMemo(() => {
    const out = [];
    if (mode === 'speed') {
      const B = 28, bin = (k) => (isFinite(R.v[k]) ? Math.max(0, Math.min(B - 1, Math.floor(((R.v[k] - calc.lo) / (calc.hi - calc.lo)) * B))) : 0);
      let s0 = 0, cb = bin(0);
      for (let k = 1; k < K; k++) { const b = bin(k); if (b !== cb) { out.push({ a: s0, b: k, color: flcSpeedCol((cb + 0.5) / B) }); cb = b; s0 = k; } }
      out.push({ a: s0, b: K, color: flcSpeedCol((cb + 0.5) / B) });
    } else (mode === 'dom' ? calc.win : calc.sec).forEach((w) => { const d = w.w >= 0 ? dom[w.w] : null; out.push({ a: w.i0, b: w.i1, color: d ? d.c : '#555', c2: d ? d.c2 : null, dash: !!(d && d.dash) }); });
    return out;
  }, [mode, calc, rowKey]);
  const norm = (k) => { const a = Math.max(0, k - 2), b = Math.min(K, k + 2), dx = R.x[b] - R.x[a], dy = R.y[b] - R.y[a], L = Math.hypot(dx, dy) || 1; return [-dy / L, dx / L]; };
  const tick = (k, len, wd) => { if (!okp(k)) return null; const [nx, ny] = norm(k); return <line x1={R.x[k] - nx * len * u} y1={R.y[k] - ny * len * u} x2={R.x[k] + nx * len * u} y2={R.y[k] + ny * len * u} stroke="#fff" strokeWidth={wd * u}></line>; };
  const move = (e) => {
    const svg = e.currentTarget, m = svg.getScreenCTM(); if (!m) return;
    const p = svg.createSVGPoint(); p.x = e.clientX; p.y = e.clientY; const w = p.matrixTransform(m.inverse());
    let bk = null, bd = Infinity; for (let k = 0; k <= K; k++) { if (!okp(k)) continue; const d = (R.x[k] - w.x) ** 2 + (R.y[k] - w.y) ** 2; if (d < bd) { bd = d; bk = k; } }
    setHover(Math.sqrt(bd) <= 36 * u ? bk : null);
  };
  const mini = T.mini;
  return (
    <div ref={box}>
      <svg viewBox={`${geo.vx} ${geo.vy} ${geo.vw} ${geo.vh}`} width={W} height={Hpx} onPointerMove={move} onPointerDown={move} onPointerLeave={() => setHover(null)} style={{ display: 'block', cursor: 'crosshair', touchAction: 'pan-y' }} role="img" aria-label="Track map">
        {mini && <image href={mini.src} x={-mini.ox / mini.s} y={-mini.oy / mini.s} width={mini.w / mini.s} height={mini.h / mini.s} opacity="0.13" preserveAspectRatio="none"></image>}
        <polyline points={poly(0, K)} fill="none" stroke="#000" strokeWidth={10 * u} strokeLinejoin="round" strokeLinecap="round"></polyline>
        {segs.map((s, i) => <React.Fragment key={mode + i}><polyline points={poly(s.a, s.b)} fill="none" style={{ stroke: s.color }} strokeWidth={4.6 * u} strokeLinejoin="round" strokeLinecap={s.dash || s.c2 ? 'butt' : 'round'} strokeDasharray={s.dash ? `${7 * u} ${4 * u}` : undefined}></polyline>{s.c2 && <polyline points={poly(s.a, s.b)} fill="none" style={{ stroke: s.c2 }} strokeWidth={4.6 * u} strokeLinejoin="round" strokeLinecap="butt" strokeDasharray={`${6 * u} ${6 * u}`}></polyline>}</React.Fragment>)}
        {mode === 'sectors' && [1, 2].map((j) => <g key={'t' + j}>{tick(Math.round((K * j) / 3), 12, 2.5)}</g>)}
        {mode === 'sectors' && [0, 1, 2].map((j) => { const k = Math.round((K * (j + 0.5)) / 3); if (!okp(k)) return null; const [nx, ny] = norm(k);
          return <text key={'s' + j} x={R.x[k] + nx * 24 * u} y={R.y[k] + ny * 24 * u + 4.5 * u} textAnchor="middle" style={{ font: `800 ${13 * u}px 'Archivo',sans-serif`, fill: COLORS.text, paintOrder: 'stroke', stroke: '#000', strokeWidth: 4 * u }}>S{j + 1}</text>; })}
        {tick(0, 12, 3.5)}
        {hover != null && okp(hover) && <circle cx={R.x[hover]} cy={R.y[hover]} r={6.5 * u} fill="#000" stroke="#fff" strokeWidth={2.6 * u}></circle>}
      </svg>
    </div>);
}

function FRALapGap({ calc, rows, cols, sty = [], ri, hover, setHover }) {
  const [box, W] = useFraWidth();
  const narrow = W < 640, H = narrow ? 230 : 270, M = { l: narrow ? 50 : 60, r: 14, t: 22, b: 10 }, pw = W - M.l - M.r, ph = H - M.t - M.b, K = calc.K;
  let mx = 0; calc.gaps.forEach((g, i) => { if (!g || i === ri) return; for (let k = 0; k <= K; k++) if (Math.abs(g[k]) > mx) mx = Math.abs(g[k]); });
  const lim0 = Math.max(0.1, mx * 1.12), st = [0.05, 0.1, 0.2, 0.25, 0.5, 1, 2, 5].find((s) => lim0 / s <= 3) || 10, lim = Math.ceil(lim0 / st) * st;
  const X = (k) => M.l + (k / K) * pw, Y = (g) => M.t + ph / 2 + (Math.max(-lim, Math.min(lim, g)) / lim) * (ph / 2); // behind (+) plots DOWN
  const yt = []; for (let v = -lim; v <= lim + 1e-9; v += st) yt.push(+v.toFixed(3));
  const move = (e) => { const r = e.currentTarget.getBoundingClientRect(), k = Math.round(((e.clientX - r.left - M.l) / pw) * K); setHover(k < 0 || k > K ? null : k); };
  const dp = st < 0.1 ? 2 : st < 1 ? 1 : 0;
  return (
    <div ref={box}>
      <svg width={W} height={H} onPointerMove={move} onPointerDown={move} onPointerLeave={() => setHover(null)} style={{ display: 'block', cursor: 'crosshair', touchAction: 'pan-y' }}>
        <rect x={X(K / 3)} y={M.t} width={X((2 * K) / 3) - X(K / 3)} height={ph} fill="rgba(255,255,255,0.025)"></rect>
        {[0, 1, 2].map((j) => <text key={'s' + j} x={(X((K * j) / 3) + X((K * (j + 1)) / 3)) / 2} y={M.t - 9} textAnchor="middle" className="fra-tick">S{j + 1}</text>)}
        {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 === 0 ? '0' : Math.abs(v).toFixed(dp) + 's'}</text></g>)}
        <text x={M.l + 8} y={M.t + 15} className="fra-tick" style={{ letterSpacing: '.14em' }}>AHEAD</text>
        <text x={M.l + 8} y={M.t + ph - 8} className="fra-tick" style={{ letterSpacing: '.14em' }}>BEHIND</text>
        <line x1={M.l} x2={M.l + pw} y1={Y(0)} y2={Y(0)} stroke="rgba(255,255,255,0.4)" strokeWidth="1.2" strokeDasharray="3 4"></line>
        {calc.gaps.map((g, i) => { if (!g || i === ri) return null; const pts = []; for (let k = 0; k <= K; k++) pts.push(X(k).toFixed(1) + ',' + Y(g[k]).toFixed(1));
          const s = sty[i] || {}, P = pts.join(' ');
          return <React.Fragment key={rows[i].id}><polyline points={P} fill="none" stroke={cols[i]} strokeWidth="2.2" strokeLinejoin="round" strokeDasharray={s.dash ? '7 4' : undefined}></polyline>{s.c2 && <polyline points={P} fill="none" stroke={s.c2} strokeWidth="2.2" strokeLinejoin="round" strokeDasharray="6 6"></polyline>}</React.Fragment>; })}
        {hover != null && <g><line x1={X(hover)} x2={X(hover)} y1={M.t} y2={M.t + ph} stroke="rgba(255,255,255,0.4)"></line>
          {calc.gaps.map((g, i) => (g ? <circle key={i} cx={X(hover)} cy={Y(i === ri ? 0 : g[hover])} r="4" fill={cols[i]} stroke="#000" strokeWidth="1.5"></circle> : null))}</g>}
      </svg>
    </div>);
}

function FRALapSectors({ S, rows, drv, cols, mode, secSty }) {
  const sec = frsSectors(S); if (!sec) return null;
  const cell = (v, j, id) => {
    if (v == null) return <td key={j}>—</td>;
    const purple = sec.best[j] && Math.abs(v - sec.best[j].t) < 1e-9, green = !purple && sec.pb[id] && sec.pb[id][j] != null && Math.abs(v - sec.pb[id][j]) < 1e-9;
    return <td key={j} style={{ color: purple ? FLC_PURPLE : green ? FLC_GREEN : COLORS.text, fontWeight: purple || green ? 700 : 500 }}>{fraFmt(v)}</td>;
  };
  return (
    <div className="flc-scroll">
      <table className="fra-tbl">
        <thead><tr><th>Driver</th>{[0, 1, 2].map((j) => <th key={j}>{mode === 'sectors' && secSty && secSty[j] && <i style={{ display: 'inline-block', width: 10, height: 10, background: flcDomSw(secSty[j]), marginRight: 7, verticalAlign: -1 }}></i>}S{j + 1}</th>)}<th>Lap time</th></tr></thead>
        <tbody>
          {rows.map((r, i) => { const c = S.cars.find((x) => x.id === r.id), s = sec.by[r.id] && sec.by[r.id][r.n - 1], l = c.laps[r.n - 1];
            return <tr key={r.id}><td style={{ boxShadow: `inset 3px 0 0 ${cols[i]}`, paddingLeft: 12 }}><b>{drv[r.id].label}</b> L{r.n}</td>{[0, 1, 2].map((j) => cell(s && s[j], j, r.id))}<td><b>{fraFmt(l && l.t)}</b></td></tr>; })}
          <tr><td style={{ paddingLeft: 12 }}>Best in race</td>{sec.best.map((b, j) => <td key={j}>{b ? <span style={{ color: FLC_PURPLE, fontWeight: 700 }}>{fraFmt(b.t)}</span> : '—'}{b && <div style={{ fontSize: 11, color: COLORS.textDim, marginTop: 4 }}>{drv[b.id].label} · L{b.n}</div>}</td>)}<td></td></tr>
        </tbody>
      </table>
      <div className="fra-lgd" style={{ marginTop: 10 }}><span><i style={{ background: FLC_PURPLE }}></i>Fastest in the race</span><span><i style={{ background: FLC_GREEN }}></i>Driver’s best</span><span>Lap 1 and pit laps don’t count</span></div>
    </div>);
}

function FRALapCompare({ S, drv, pin = [] }) {
  const T = S.tele;
  const [lapPick, setLapPick] = React.useState({});
  const [refId, setRefId] = React.useState(null);
  const [mode, setMode] = React.useState('speed');
  const [unit, setUnit] = React.useState('kmh');
  const [hover, setHover] = React.useState(null);
  const carOf = (id) => S.cars.find((c) => c.id === id);
  const byPb = S.cars.filter((c) => c.pb != null).sort((a, b) => a.pb - b.pb);
  const fastId = (S.fastest && S.fastest.id) || (byPb[0] && byPb[0].id) || null, nextId = (byPb.find((c) => c.id !== fastId) || {}).id || null;
  const want = !pin.length ? [fastId] : pin.length === 1 ? [pin[0], pin[0] === fastId ? nextId : fastId] : pin.slice(0, 4); // All drivers = the fastest lap alone (user 2026-10-13)
  const ids = want.filter((id, i) => id && want.indexOf(id) === i && carOf(id));
  const rows = ids.map((id) => ({ id, n: lapPick[id] || flcBestLap(carOf(id)) })).filter((r) => r.n);
  // All drivers (nothing pinned): the table shows the fastest lap alone, but the map reads EVERY driver's best lap so Dominance /
  // Sectors show who owned each part of the track across the whole field (user 2026-10-13). Ref = the fastest lap (index 0).
  const allMode = !pin.length;
  const trRows = allMode ? [...rows, ...byPb.filter((c) => !ids.includes(c.id)).map((c) => ({ id: c.id, n: flcBestLap(c) })).filter((r) => r.n)] : rows;
  const ri = allMode ? 0 : Math.max(0, rows.findIndex((r) => r.id === refId));
  const lt = useLapTraces(T ? S : null, trRows);
  const key = trRows.map((r) => r.id + ':' + r.n).join(',') + '|' + ri;
  const keep = React.useRef(null); // last drawn laps stay up while the next ones are read (no layout jump)
  if (lt.traces) keep.current = { rows: trRows, tr: lt.traces, ri, key };
  const view = keep.current;
  const calc = React.useMemo(() => (view ? flcCalc(view) : null), [view && view.tr, view && view.ri]);
  if (!T || !rows.length) return null;
  const fresh = !!(view && view.key === key && calc);
  const conv = unit === 'kmh' ? (FLC_CAR_M / (T.carLen || 44)) * 3.6 : 1, uLbl = unit === 'kmh' ? 'km/h' : 'px/s';
  const fmtV = (v) => (v == null || !isFinite(v) ? '—' : <span>{Math.round(v * conv)}<small>{uLbl}</small></span>);
  const wetTh = S.wet ? S.wet.th : 0.05;
  const lapOpt = (c, l) => 'L' + l.n + ' · ' + fraFmt(l.t) + (l.t === c.pb ? ' · best' : '') + (l.n === 1 ? ' · start' : '') + (l.pit ? ' · pit' : '') + (l.wet >= wetTh ? ' · wet' : '');
  const refRow = rows[ri], refLap = carOf(refRow.id).laps[refRow.n - 1];
  const vr = view ? view.rows : rows, vri = view ? view.ri : ri, vref = vr[vri];
  const sty = flcRowStyle(rows, drv), dcols = flcRowStyle(vr, drv), cols = sty.map((x) => x.c), vcols = dcols.map((x) => x.c);
  const solo = vr.length < 2, mm = solo && mode === 'dom' ? 'speed' : mode; // one lap only → no dominance
  const one = pin.length === 1 ? drv[pin[0]] : null;
  const cap = !pin.length ? 'Fastest lap of the race · pin drivers above to compare theirs'
    : one ? `${one.label}’s best lap vs ${pin[0] === fastId ? 'the next-fastest driver' : 'the fastest lap'}`
    : `Best laps of the pinned drivers${pin.length > 4 ? ' · first 4 shown' : ''}`;
  const wait = <div className="flc-wait fra-note">{lt.error ? 'Couldn’t read the laps from the replay.' : 'Reading the laps…'}</div>;
  return (
    <div className="fra-panel">
      <style>{flcCss}</style>
      <div className="fra-ph">
        <span className="fra-pt">Lap comparison</span>
        <span className="fra-cap">{cap}</span>
        <span className="fra-tabs flc-unit" role="group" aria-label="Speed unit">{[['u', 'px/s'], ['kmh', 'km/h']].map(([k, l]) => <button key={k} className="fra-tab" aria-pressed={unit === k ? 'true' : 'false'} onClick={() => setUnit(k)} title={k === 'u' ? 'Pixels per second — the game’s own unit' : 'Estimated km/h'}>{l}</button>)}</span>
      </div>
      <div className="flc-scroll">
        <table className="flc-rows">
          <thead><tr><th>Driver</th><th>Lap</th><th>Lap time</th><th>{hover != null ? 'Gap here' : 'Gap'}</th><th>{hover != null ? 'Speed here' : 'Top speed'}</th><th></th></tr></thead>
          <tbody>{rows.map((r, i) => {
            const c = carOf(r.id), d = drv[r.id], l = c.laps[r.n - 1], isRef = i === ri, g = fresh && calc.gaps[i];
            const gap = isRef ? null : hover != null ? (g ? g[hover] : null) : l && refLap ? l.t - refLap.t : null;
            const sp = !fresh ? null : hover != null ? (lt.traces[i] ? lt.traces[i].v[hover] : null) : calc.top[i];
            return (
              <tr key={r.id}>
                <td style={{ boxShadow: `inset 3px 0 0 ${cols[i]}` }}><i className="flc-sw" style={{ background: flcDomSw(sty[i]) }} aria-hidden="true"></i><span className="flc-nm">{d.label}</span>{isRef && <span className="flc-tag">{pin.length ? 'Ref' : 'Fastest lap'}</span>}{one && i === 1 && !isRef && <span className="flc-tag dim">Benchmark</span>}</td>
                <td><span className="flc-pw"><select className="flc-pick" value={r.n} aria-label={'Lap for ' + d.label} onChange={(e) => { const v = +e.target.value; setLapPick((p) => ({ ...p, [r.id]: v })); }}>{c.laps.map((x) => <option key={x.n} value={x.n}>{lapOpt(c, x)}</option>)}</select></span></td>
                <td><b style={{ color: COLORS.text }}>{fraFmt(l && l.t)}</b></td>
                <td style={{ color: gap == null ? COLORS.textDim : gap < 0 ? FLC_GREEN : COLORS.text }}>{gap == null ? '—' : flcSigned(gap)}</td>
                <td>{fmtV(sp)}</td>
                <td>{isRef ? null : <button className="flc-btn" onClick={() => setRefId(r.id)}>Set ref</button>}</td>
              </tr>);
          })}</tbody>
        </table>
      </div>
      <div className="flc-grid">
        <div className="flc-card">
          <div className="flc-ch">
            <span className="flc-ct">Track map</span>
            <span className="fra-tabs" role="group" aria-label="Map colouring">{[['sectors', 'Sectors'], ['speed', 'Speed'], ...(solo ? [] : [['dom', 'Dominance']])].map(([k, l]) => <button key={k} className="fra-tab" aria-pressed={mm === k ? 'true' : 'false'} onClick={() => setMode(k)}>{l}</button>)}</span>
          </div>
          {calc ? <FRALapMap T={T} calc={calc} rows={vr} cols={vcols} dom={dcols} mode={mm} hover={hover} setHover={setHover} /> : wait}
          {calc && <div className="fra-lgd">
            {mm === 'speed' && <React.Fragment><span><i style={{ width: 120, height: 10, background: `linear-gradient(90deg, ${[0, 0.25, 0.5, 0.75, 1].map(flcSpeedCol).join(', ')})` }}></i>{`${Math.round(calc.lo * conv)} → ${Math.round(calc.hi * conv)} ${uLbl}`}</span><span>{`Speed on ${drv[vref.id].label}’s lap ${vref.n}`}</span></React.Fragment>}
            {mm === 'dom' && <React.Fragment>{vr.map((r, i) => ({ r, i })).filter((x) => !allMode || calc.cnt[x.i] > 0).sort((a, b) => (allMode ? calc.cnt[b.i] - calc.cnt[a.i] : 0)).map(({ r, i }) => <span key={r.id}><i style={{ background: flcDomSw(dcols[i]) }}></i>{`${drv[r.id].label} · ${calc.cnt[i]} of ${FLC_MINI}`}</span>)}<span>{allMode ? `Fastest of all ${vr.length} drivers’ best laps through each of ${FLC_MINI} mini-sectors` : `Faster through each of ${FLC_MINI} mini-sectors`}</span></React.Fragment>}
            {mm === 'sectors' && (solo ? <span>Sectors = equal thirds of the lap</span> : <React.Fragment>{calc.sec.map((s, j) => <span key={j}><i style={{ background: s.w >= 0 ? flcDomSw(dcols[s.w]) : '#555' }}></i>{`S${j + 1} · ${s.w >= 0 ? drv[vr[s.w].id].label : '—'}`}</span>)}<span>{allMode ? 'Fastest best lap through each third' : 'Fastest through each third of the lap'}</span></React.Fragment>)}
          </div>}
        </div>
        <div className="flc-card">
          <div className="flc-ch"><span className="flc-ct">Gap along the lap</span>{!allMode && !solo && <span className="fra-cap">to <b style={{ color: COLORS.text }}>{`${drv[vref.id].label} L${vref.n}`}</b> · dashed line</span>}</div>
          {allMode || solo ? <div className="flc-wait fra-note">Pin drivers above to race their laps against this one.</div> : calc ? <FRALapGap calc={calc} rows={vr} cols={vcols} sty={dcols.map((x) => (x.mate === vri ? {} : x))} ri={vri} hover={hover} setHover={setHover} /> : wait}
          <FRALapSectors S={S} rows={rows} drv={drv} cols={cols} mode={mm} secSty={calc ? calc.sec.map((s) => (s.w >= 0 ? dcols[s.w] : null)) : null} />
        </div>
      </div>
      <div className="fra-note" style={{ marginTop: 14 }}>Speeds come from each car’s position in the replay, 60 times a second. km/h is an estimate: the game’s car length scaled to a 5.6 m F1 car. Sectors are equal thirds of the lap; dominance splits it into {FLC_MINI} equal mini-sectors. Move over the map or the graph to read any point of the lap.</div>
    </div>);
}

Object.assign(window, { FRALapCompare });
