// ─── RACE REPLAYS — Bituko's broadcast replays (self-contained HTML per race, ~2–3 MB) ─────────────
// LIST = the replay Worker's /index.json (Bituko uploads → listed here, no redeploy) merged over the local
// replays/ folder below (offline fallback; on the same id the Worker entry wins, the local file stays as backup).
// Worker entry { file:'jl/s23a/r2.html', league: ul|cl|jl|al (cl = the site's 'hl'), season: 23, half: 'A'|'B'|null,
// round (league round, halves restart at 1), session: 'race'|'sprint', bytes, uploaded }.
// calSeason + calRound key the shared calendar (track art, minimap, GP name): league season ↔ calendar season by
// index (the alignment URTrackHero uses); JL/AL B-half round n = calendar round n + 5.
// FILE = downloaded once (urReplayText), shared by the theater (sandboxed srcdoc iframe) and the stats.
// Replay consumers (theater, analysis, gap column) wait for the list to settle so a replay isn't downloaded twice.
const FRP_WORKER = 'https://ur2d-replays.mika2mika.workers.dev';
const UR2D_REPLAYS_LOCAL = [
  { id: 'ul-s26-r2', league: 'ul', season: 'S26', round: 2, calSeason: 'S26', calRound: 2, file: 'replays/UL_S26_R2_Barcelona.html', by: 'Bituko', laps: 44, drivers: 17, added: '2026-10-12' },
  { id: 'cl-s23-r2', league: 'hl', season: 'S23', round: 2, calSeason: 'S26', calRound: 2, file: 'replays/CL_S23_R2_Barcelona.html', by: 'Bituko', laps: 33, drivers: 12, added: '2026-10-12' },
  { id: 'jl-s23a-r2', league: 'jl', season: 'S23A', round: 2, calSeason: 'S26', calRound: 2, file: 'replays/JL_S23A_R2_Barcelona.html', by: 'Bituko', laps: 24, drivers: 17, added: '2026-10-11' },
].map((x) => ({ session: 'race', ...x, local: x.file, url: x.file, src: 'local' }));
const FRP_LG_IN = { ul: 'ul', cl: 'hl', hl: 'hl', jl: 'jl', al: 'al' };
const FRP_NONE = [];
const frpKey = (r) => r.id + '@' + (r.added || '');
// League season → calendar season: a league's Nth distinct season (A/B halves count once) races calendar season N.
function frpCalSeason(league, label) {
  try {
    const base = (s) => String(s).replace(/[AB]$/i, '').toUpperCase();
    const cal = getCalendarSeasons().filter((s) => !s.fromDb);
    const bases = [...new Set(getSeasons(league).filter((s) => !s.dbOnly).map((s) => base(s.id)))];
    const i = bases.indexOf(base(label));
    return ((i >= 0 && cal[i]) || cal.find((s) => s.isCurrent) || cal[0] || {}).id || null;
  } catch (e) { return null; }
}
function _frpFromWorker(e) {
  const league = FRP_LG_IN[String(e.league || '').toLowerCase()], round = Number(e.round);
  const m = String(e.season == null ? '' : e.season).match(/^s?(\d+)([ab])?$/i);
  if (!league || !m || !(round > 0) || !e.file) return null;
  const half = String(e.half || m[2] || '').toUpperCase() || null, season = 'S' + m[1] + (half || ''), path = String(e.file).replace(/^\/+/, '');
  return { id: path.replace(/\.html$/i, '').replace(/\//g, '-'), league, season, round, session: e.session === 'sprint' ? 'sprint' : 'race',
    calSeason: frpCalSeason(league, season), calRound: half === 'B' ? round + 5 : round, file: FRP_WORKER + '/' + path, url: FRP_WORKER + '/' + path,
    by: 'Bituko', added: e.uploaded || null, bytes: e.bytes || null, src: 'worker' };
}
const _frpMeta = {}; // id → { laps, drivers } read from a downloaded file (the Worker list carries neither)
const _frpFill = (x) => { const m = _frpMeta[x.id]; return m ? { ...x, laps: x.laps != null ? x.laps : m.laps, drivers: x.drivers != null ? x.drivers : m.drivers } : x; };
function _frpMerge(remote) {
  const loc = {}, out = []; UR2D_REPLAYS_LOCAL.forEach((x) => { loc[x.id] = x; });
  remote.forEach((e) => { const x = _frpFromWorker(e); if (!x) return; const l = loc[x.id]; delete loc[x.id]; out.push(l ? { ...l, ...x, calSeason: x.calSeason || l.calSeason, local: l.file } : x); });
  return [...out, ...Object.values(loc)].map(_frpFill);
}
// Store { list, status }: idle → loading → live (Worker answered) | offline (local folder only).
let _frpState = { list: UR2D_REPLAYS_LOCAL, status: 'idle' }, _frpAt = 0, _frpBusy = null, _frpRaw = null;
const _frpSubs = new Set();
function _frpEmit(next) { _frpState = { ..._frpState, ...next }; window.UR2D_REPLAYS = _frpState.list; _frpSubs.forEach((f) => f()); }
function frpRefresh(maxAge = 60000) {
  if (_frpBusy) return _frpBusy;
  if (_frpAt && Date.now() - _frpAt < maxAge) return Promise.resolve(_frpState);
  if (_frpState.status === 'idle') _frpEmit({ status: 'loading' });
  _frpBusy = fetch(FRP_WORKER + '/index.json', { cache: 'no-cache' })
    .then((res) => { if (!res.ok) throw new Error('index ' + res.status); return res.text(); })
    .then((t) => { const rows = (JSON.parse(t) || {}).replays; if (t !== _frpRaw || _frpState.status !== 'live') { _frpRaw = t; _frpEmit({ list: _frpMerge(Array.isArray(rows) ? rows : []), status: 'live' }); } })
    .catch((e) => { console.warn('[replays] Worker list unavailable, using the local replays/ folder', e); if (_frpState.status !== 'live') _frpEmit({ status: 'offline' }); })
    .then(() => { _frpAt = Date.now(); _frpBusy = null; return _frpState; });
  return _frpBusy;
}
const _frpSub = (f) => { _frpSubs.add(f); return () => { _frpSubs.delete(f); }; };
const _frpSnap = () => _frpState;
const frpSettled = (s) => s.status === 'live' || s.status === 'offline';
function useReplayIndex() { const s = React.useSyncExternalStore(_frpSub, _frpSnap); React.useEffect(() => { frpRefresh(); }, []); return s; }
// For components that DOWNLOAD a replay: empty until the list settles (no local-then-Worker double download).
function useReplayList() { const s = useReplayIndex(); return frpSettled(s) ? s.list : FRP_NONE; }
// One download per replay (+ upload time), shared by theater + stats. Worker copy first, local copy if that fails.
const _frpText = {};
function urReplayText(r) {
  const k = frpKey(r);
  if (!_frpText[k]) {
    const get = (u) => fetch(encodeURI(u)).then((res) => { if (!res.ok) throw new Error('replay ' + res.status + ' ' + u); return res.text(); });
    _frpText[k] = get(r.file)
      .catch((e) => { if (!r.local || r.local === r.file) throw e; console.warn('[replays] Worker file unavailable, using the local copy', e); return get(r.local); })
      .then((t) => { _frpNoteMeta(r, t); return t; })
      .catch((e) => { delete _frpText[k]; throw e; });
  }
  return _frpText[k];
}
function _frpNoteMeta(r, t) {
  if ((r.laps != null && r.drivers != null) || _frpMeta[r.id]) return;
  setTimeout(() => {
    try {
      const key = 'window.REPLAY_DATA = ', i = t.indexOf(key); if (i < 0) return;
      let js = t.slice(i + key.length, t.indexOf('\n', i)).trim(); if (js.endsWith(';')) js = js.slice(0, -1);
      const D = JSON.parse(js); _frpMeta[r.id] = { laps: D.laps, drivers: (D.cars || []).length };
      _frpEmit({ list: _frpState.list.map((x) => (x.id === r.id ? _frpFill(x) : x)) });
    } catch (e) {}
  }, 0);
}
function frpLatest(list) { // newest race by calendar (season, round), race before sprint, league order, then upload time
  let cal = []; try { cal = getCalendarSeasons().filter((s) => !s.fromDb).map((s) => s.id); } catch (e) {}
  const ci = (r) => { const i = cal.indexOf(r.calSeason); return i < 0 ? 999 : i; }, li = (r) => { const i = LEAGUES.findIndex((l) => l.id === r.league); return i < 0 ? 99 : i; };
  return [...list].sort((a, b) => ci(a) - ci(b) || (b.calRound || 0) - (a.calRound || 0) || (a.session === 'sprint') - (b.session === 'sprint') || li(a) - li(b) || String(b.added || '').localeCompare(String(a.added || '')))[0] || null;
}

const frpCss = `
.frp{padding:56px clamp(18px,4vw,44px) 96px;max-width:1480px;margin:0 auto}
.frp-tabs{display:flex;flex-wrap:wrap;gap:6px;margin:-12px 0 32px}
.frp-tab{all:unset;cursor:pointer;font:600 12px/1 'JetBrains Mono',monospace;letter-spacing:.18em;text-transform:uppercase;color:${COLORS.textMute};padding:10px 14px;border:1px solid ${COLORS.hairline};border-radius:2px}
.frp-tab:hover{color:${COLORS.text}}
.frp-tab[aria-pressed="true"]{color:${COLORS.text};border-color:${COLORS.red};box-shadow:inset 0 -2px 0 ${COLORS.red}}
.frp-feat{all:unset;cursor:pointer;position:relative;display:grid;align-content:end;min-height:clamp(300px,38vw,520px);border:1px solid ${COLORS.hairline};overflow:hidden;background:#0a0a0c;width:100%;box-sizing:border-box}
.frp-feat img.bg{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;opacity:.55;transition:transform .8s cubic-bezier(.2,.7,.2,1),opacity .4s}
.frp-feat:hover img.bg{transform:scale(1.03);opacity:.7}
.frp-feat::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0) 30%,rgba(0,0,0,.92) 100%)}
.frp-feat .in{position:relative;z-index:1;display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:24px;padding:clamp(20px,3vw,40px)}
.frp-k{font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.28em;text-transform:uppercase;color:${COLORS.textMute}}
.frp-feat h3{margin:12px 0 0;font:900 clamp(40px,6vw,88px)/.9 'Archivo',sans-serif;letter-spacing:-.03em;text-transform:uppercase;color:${COLORS.text}}
.frp-feat .gp{margin-top:10px;font:500 17px/1.3 'Archivo',sans-serif;color:${COLORS.textMute}}
.frp-meta{display:flex;flex-wrap:wrap;gap:6px 18px;margin-top:16px;font:500 12px/1 'JetBrains Mono',monospace;letter-spacing:.14em;text-transform:uppercase;color:${COLORS.textMute}}
.frp-meta b{color:${COLORS.text};font-weight:700}
.frp-acts{display:flex;flex-wrap:wrap;gap:10px}
.frp-btn{all:unset;cursor:pointer;display:inline-flex;align-items:center;gap:10px;font:800 13px/1 'Archivo',sans-serif;letter-spacing:.12em;text-transform:uppercase;padding:15px 22px;background:${COLORS.red};color:#fff}
.frp-btn:hover{background:#ff1a12}
.frp-btn.ghost{background:rgba(0,0,0,.4);border:1px solid rgba(255,255,255,.28)}
.frp-btn.ghost:hover{border-color:#fff;background:rgba(0,0,0,.6)}
.frp-btn .tri{width:0;height:0;border-left:10px solid currentColor;border-top:6px solid transparent;border-bottom:6px solid transparent}
.frp-chip{display:inline-block;white-space:nowrap;font:700 12px/1 'JetBrains Mono',monospace;letter-spacing:.16em;color:${COLORS.text};padding:6px 9px;border:1px solid}
.frp-lg{margin-top:64px}
.frp-lgh{display:flex;flex-wrap:wrap;align-items:center;gap:10px 16px;padding-bottom:16px;border-bottom:1px solid ${COLORS.hairline};margin-bottom:18px}
.frp-lgh img{height:30px;width:auto}
.frp-lgh h4{margin:0;font:900 26px/1 'Archivo',sans-serif;letter-spacing:-.01em;text-transform:uppercase;color:${COLORS.text}}
.frp-lgh .nx{margin-left:auto;font:500 12px/1.4 'JetBrains Mono',monospace;letter-spacing:.14em;text-transform:uppercase;color:${COLORS.textDim}}
.frp-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:12px}
.frp-tile{all:unset;box-sizing:border-box;position:relative;display:grid;grid-template-rows:auto 1fr;border:1px solid ${COLORS.hairline};background:#08080a;min-height:208px}
.frp-tile.on{cursor:pointer}
.frp-tile.on:hover{border-color:${COLORS.red}}
.frp-tile .mm{height:118px;overflow:hidden;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:14px;border-bottom:1px solid ${COLORS.hairline};background:repeating-linear-gradient(135deg,rgba(255,255,255,.018) 0 8px,transparent 8px 16px)}
.frp-tile .mm img{max-height:90px;max-width:100%;width:auto;min-height:0;object-fit:contain;opacity:.9}
.frp-tile.off .mm img{opacity:.3;filter:grayscale(1)}
.frp-tile .tx{padding:14px 14px 16px;display:grid;gap:6px;align-content:start}
.frp-tile .r{font:600 12px/1 'JetBrains Mono',monospace;letter-spacing:.18em;text-transform:uppercase;color:${COLORS.textMute}}
.frp-tile .t{font:800 19px/1.05 'Archivo',sans-serif;text-transform:uppercase;color:${COLORS.text}}
.frp-tile.off .t{color:${COLORS.textMute}}
.frp-tile .st{margin-top:6px;font:600 12px/1 'JetBrains Mono',monospace;letter-spacing:.14em;text-transform:uppercase;color:${COLORS.textDim}}
.frp-tile.on .st{color:${COLORS.red}}
.frp-empty{font:500 13px/1.6 'JetBrains Mono',monospace;letter-spacing:.08em;color:${COLORS.textDim};padding:22px 0}
.frp-th{padding:24px clamp(14px,3vw,36px) 72px;max-width:1440px;margin:0 auto}
.frp-thbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px 20px;margin-bottom:18px}
.frp-back{all:unset;cursor:pointer;font:600 12px/1 'JetBrains Mono',monospace;letter-spacing:.18em;text-transform:uppercase;color:${COLORS.textMute};padding:10px 0}
.frp-back:hover{color:${COLORS.text}}
.frp-thbar h2{margin:0;font:900 clamp(24px,3vw,40px)/1 'Archivo',sans-serif;letter-spacing:-.02em;text-transform:uppercase;color:${COLORS.text}}
.frp-thbar .sp{flex:1}
.frp-frame{position:relative;border:1px solid ${COLORS.hairline};background:#0b0d10;min-height:420px}
.frp-frame iframe{display:block;width:100%;border:0;background:#0b0d10}
.frp-load{position:absolute;inset:0;display:grid;place-items:center;font:600 12px/1 'JetBrains Mono',monospace;letter-spacing:.24em;text-transform:uppercase;color:${COLORS.textMute};pointer-events:none}
.frp-note{margin-top:14px;font:500 12px/1.6 'JetBrains Mono',monospace;letter-spacing:.06em;color:${COLORS.textDim}}
`;

const frpLeague = (id) => LEAGUES.find((l) => l.id === id) || LEAGUES[0];
const frpRace = (r) => { try { return getCalendar(r.league, r.calSeason).races.find((x) => x.round === r.calRound) || null; } catch (e) { return null; } };
const frpMinimap = (race) => race && (race.minimap || (typeof urRaceMinimap === 'function' ? urRaceMinimap(race) : null));
const frpHref = (r) => encodeURI(r.url || r.file);
// Open Calendar & Results on this round + league, scrolled to the race analysis.
const frpAnalysis = (r, go) => { window.__fdCalDeep = { season: r.calSeason, round: r.calRound, league: r.league }; window.__fdScrollAnalysis = true; go('calres'); };
// League's own round naming for calendar round n (JL/AL split into A/B halves of 5).
function frpLeagueRound(league, n) {
  let cur = null; try { cur = (getSeasons(league) || []).find((s) => s.isCurrent); } catch (e) {}
  const base = cur ? String(cur.label).replace(/[AB]$/, '') : '';
  if (league === 'jl' || league === 'al') return n > 5 ? { season: base + 'B', round: n - 5 } : { season: base + 'A', round: n };
  return { season: cur ? cur.label : '', round: n };
}

function FDReplayFeature({ r, go }) {
  const L = frpLeague(r.league), race = frpRace(r);
  return (
    <div style={{ position: 'relative' }}>
      <div className="frp-feat" onClick={() => go('replay/' + r.id)}>
        {race && race.trackImage && <img className="bg" src={race.trackImage} alt="" />}
        <div className="in">
          <div>
            <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 12 }}>
              <span className="frp-chip" style={{ borderColor: L.accent }}>{`${L.short} · ${r.season}`}</span>
              <span className="frp-k">Latest replay · Round {r.round}{r.session === 'sprint' ? ' · Sprint' : ''}</span>
            </div>
            <h3>{race ? race.track : 'Round ' + r.round}</h3>
            {race && <div className="gp">{race.grandPrix}{race.creator ? ' · track by ' + race.creator : ''}</div>}
            <div className="frp-meta">{r.laps != null && <span><b>{r.laps}</b> laps</span>}{r.drivers != null && <span><b>{r.drivers}</b> drivers</span>}<span>Replay by <b>{r.by}</b></span></div>
          </div>
          <div className="frp-acts" onClick={(e) => e.stopPropagation()}>
            <button className="frp-btn" onClick={() => go('replay/' + r.id)}><span className="tri"></span>Watch</button>
            <button className="frp-btn ghost" onClick={() => frpAnalysis(r, go)}>Results &amp; analysis</button>
            <a className="frp-btn ghost" href={frpHref(r)} target="_blank" rel="noopener" style={{ textDecoration: 'none' }}>Full page ↗</a>
          </div>
        </div>
      </div>
    </div>);
}

function FDReplayLeague({ L, go, list }) {
  let cal = null; try { cal = getCalendar(L.id); } catch (e) {}
  const races = cal ? cal.races : [];
  const mine = list.filter((r) => r.league === L.id && r.calSeason === (cal && cal.season));
  const shown = races.filter((x) => x.status === 'done' || mine.some((r) => r.calRound === x.round));
  const next = races.find((x) => x.status !== 'done' && !mine.some((r) => r.calRound === x.round));
  return (
    <div className="frp-lg">
      <div className="frp-lgh">
        <img src={L.logo} alt="" />
        <h4>{L.name}</h4>
        {next && <span className="nx">Next · R{frpLeagueRound(L.id, next.round).round} {next.track} · {next.localDate}</span>}
      </div>
      {shown.length ?
        <div className="frp-grid">
          {shown.flatMap((x) => {
            const here = mine.filter((m) => m.calRound === x.round), mm = frpMinimap(x);
            const race = here.find((m) => m.session !== 'sprint') || null, sprint = here.find((m) => m.session === 'sprint') || null;
            const tile = (r, sp) => {
              const lr = r ? { season: r.season, round: r.round } : frpLeagueRound(L.id, x.round), k = x.round + (sp ? '-sprint' : '');
              const body = <React.Fragment>
                <div className="mm">{mm && <img src={mm} alt="" />}</div>
                <div className="tx">
                  <span className="r">{lr.season} · R{lr.round}{sp ? ' · Sprint' : ''}</span>
                  <span className="t">{x.track}</span>
                  <span className="st">{r ? sp ? '▶ Watch sprint' : '▶ Watch replay' : 'Replay not uploaded'}</span>
                </div>
              </React.Fragment>;
              return r ?
                <button key={k} className="frp-tile on" onClick={() => go('replay/' + r.id)}>{body}</button> :
                <div key={k} className="frp-tile off">{body}</div>;
            };
            return sprint ? [tile(race, false), tile(sprint, true)] : [tile(race, false)];
          })}
        </div> :
        <div className="frp-empty">No races run yet this season.</div>}
    </div>);
}

function FDReplays({ go }) {
  const [lg, setLg] = React.useState('all');
  const { list } = useReplayIndex();
  const latest = frpLatest(list);
  const cur = (typeof getCalendarSeasons === 'function' ? getCalendarSeasons() : []).find((s) => s.isCurrent);
  return (
    <section className="frp" data-screen-label="Final — Race Replays">
      <style>{frpCss}</style>
      <UR2SectionHead
        kicker={`Race replays${cur && cur.period ? ' · ' + cur.period : ''}`}
        title={<span>Watch it <span style={{ color: COLORS.red }}>again.</span></span>}
        right={<div style={{ maxWidth: 380, color: COLORS.textMute, fontSize: 15, lineHeight: 1.55 }}>Every race rebuilt from the lobby data by Bituko. Follow any driver, scrub the whole race or fly the free camera.</div>} />
      <div className="frp-tabs" role="group" aria-label="League">
        {[['all', 'All leagues'], ...LEAGUES.map((L) => [L.id, L.short])].map(([k, l]) =>
          <button key={k} className="frp-tab" aria-pressed={lg === k ? 'true' : 'false'} onClick={() => setLg(k)}>{l}</button>)}
      </div>
      {latest && (lg === 'all' || lg === latest.league) && <FDReplayFeature r={latest} go={go} />}
      {LEAGUES.filter((L) => lg === 'all' || L.id === lg).map((L) => <FDReplayLeague key={L.id} L={L} go={go} list={list} />)}
    </section>);
}

// Theater: the downloaded file in a sandboxed iframe (scripts only, no access to the site) via srcdoc, its own title
// bar hidden; the injected reporter posts the document height so the frame grows to fit (no inner scroll).
const FRP_FRAME_CSS = '<style>html,body{margin:0;background:#0b0d10}.wrap>header{display:none}.wrap{padding-block:14px 20px}</style>';
const FRP_FRAME_JS = '<script>(function(){var last=0;function send(){var h=Math.ceil(document.documentElement.scrollHeight);if(h!==last){last=h;parent.postMessage({ur2dReplay:{h:h}},"*")}}if(window.ResizeObserver)new ResizeObserver(send).observe(document.body);addEventListener("load",send);document.addEventListener("toggle",send,true);send()})()<\/script>';
function frpSrcdoc(t) {
  const s = /<\/head>/i.test(t) ? t.replace(/<\/head>/i, () => FRP_FRAME_CSS + '</head>') : FRP_FRAME_CSS + t, i = s.lastIndexOf('</body>');
  return i >= 0 ? s.slice(0, i) + FRP_FRAME_JS + s.slice(i) : s + FRP_FRAME_JS;
}
function FDReplayTheater({ id, go }) {
  const ix = useReplayIndex(), settled = frpSettled(ix);
  const r = settled ? ix.list.find((x) => x.id === id) || null : null, k = r ? frpKey(r) : null;
  const fr = React.useRef(null);
  const [h, setH] = React.useState(900);
  const [doc, setDoc] = React.useState(null);
  const [err, setErr] = React.useState(false);
  const [ready, setReady] = React.useState(false);
  React.useEffect(() => {
    if (!r) return; let live = true; setDoc(null); setErr(false); setReady(false);
    urReplayText(r).then((t) => { if (live) setDoc(frpSrcdoc(t)); }).catch((e) => { console.warn('[replays]', e); if (live) setErr(true); });
    return () => { live = false; };
  }, [k]);
  React.useEffect(() => {
    const onMsg = (e) => { const el = fr.current, v = e.data && e.data.ur2dReplay; if (el && v && e.source === el.contentWindow && v.h > 200 && v.h < 40000) setH(Math.ceil(v.h)); };
    window.addEventListener('message', onMsg); return () => window.removeEventListener('message', onMsg);
  }, []);
  const onLoad = () => { if (!doc) return; setReady(true); setTimeout(() => { try { fr.current.contentWindow.focus(); } catch (e) {} }, 50); };
  // srcdoc is assigned AFTER the frame is in the page: a sandboxed frame created with srcdoc already set gets a 0×0 viewport.
  React.useLayoutEffect(() => { const el = fr.current; if (el && doc && el.getAttribute('srcdoc') !== doc) el.setAttribute('srcdoc', doc); }, [doc, r && r.id]);
  if (!r) return (
    <section className="frp-th"><style>{frpCss}</style>
      <button className="frp-back" onClick={() => go('replays')}>← All replays</button>
      <div className="frp-empty">{settled ? 'That replay isn\u2019t available.' : 'Loading replay…'}</div>
    </section>);
  const L = frpLeague(r.league), race = frpRace(r);
  return (
    <section className="frp-th" data-screen-label="Final — Race Replay">
      <style>{frpCss}</style>
      <div className="frp-thbar">
        <button className="frp-back" onClick={() => go('replays')}>← All replays</button>
        <span className="frp-chip" style={{ borderColor: L.accent }}>{`${L.short} · ${r.season} · R${r.round}${r.session === 'sprint' ? ' · Sprint' : ''}`}</span>
        <h2>{race ? race.track : 'Round ' + r.round}</h2>
        {race && <span className="frp-k">{race.grandPrix}</span>}
        <span className="sp"></span>
        <button className="frp-btn ghost" onClick={() => frpAnalysis(r, go)}>Results &amp; analysis</button>
        <a className="frp-btn ghost" href={frpHref(r)} target="_blank" rel="noopener" style={{ textDecoration: 'none' }}>Full page ↗</a>
      </div>
      <div className="frp-frame">
        {r && <iframe ref={fr} sandbox="allow-scripts" onLoad={onLoad} title={`${L.short} ${r.season} round ${r.round}${r.session === 'sprint' ? ' sprint' : ''} replay`} style={{ height: h, visibility: ready ? 'visible' : 'hidden' }} allow="fullscreen" allowFullScreen></iframe>}
        {!ready && <div className="frp-load">{err ? 'Couldn\u2019t load this replay · try Full page ↗' : 'Loading replay…'}</div>}
      </div>
      <div className="frp-note">Replay by {r.by}{r.laps != null ? ' · ' + r.laps + ' laps' : ''}{r.drivers != null ? ' · ' + r.drivers + ' drivers' : ''} · click the replay, then press ? for keyboard shortcuts · F for fullscreen</div>
    </section>);
}

window.UR2D_REPLAYS = _frpState.list;
Object.assign(window, { FRP_WORKER, UR2D_REPLAYS_LOCAL, frpRefresh, useReplayIndex, useReplayList, urReplayText, FDReplays, FDReplayTheater });
