// UR2D Final — REAL team data for the team page (TPTeamPage, ur2d-teampage.jsx).
// Builds a profile in the exact shape of ur2d-teampage-data.js makeTeam() — { TEAM, seasons[], all } —
// from the live league sheets, so the team page renders real data with no layout changes.
// · Seasons: every league season with a liveSource whose team list carries the team's code (or its old
//   code via FDT_ALIAS) — follows promotions across leagues. Calendar via the store's index alignment
//   (adapter.seasons[i] ↔ getCalendarSeasons()[i]); JL "B" halves race calendar rounds 6-10.
// · Per season: Leaderboard (team pos/pts, driver pos/pts, lineup) + Data gatherer (per-round quali /
//   finish / points) + per-round Driver|Team tabs where configured (catches subs).
// · Two slots per round: the team's two most frequent starters are the mains; anyone else who raced for
//   the team that round fills the absent main's slot (sub). No classified result → `dns` cell ('—').
// · Not in the sheets → left blank, never invented: fastest laps ('—'), team country (no flag),
//   founding season (no "Est."). HL Data-gatherer points are feature-only, so HL season + main-driver
//   points use the Leaderboard totals (round points stay feature-only).
// Load order: after ur2d-final-home.jsx, before ur2d-final-teams.jsx.

const FDT_ALIAS = { AJR: ['ASE', 'ASR'], ASE: ['AJR', 'ASR'], ASR: ['AJR', 'ASE'], ASX: ['RKA'], RKA: ['ASX'], ASN: ['RSR'], RSR: ['ASN'], '8SR': ['8SM'], '8SM': ['8SR'] }; // re-acronymed teams
const FDT_VACANT = 'r:_vacant';
const fdtNrm = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]/g, ''); // same fold as loadRoundDriverTeams
const fdtR1 = (x) => Math.round((Number(x) || 0) * 10) / 10;
const fdtTP = () => window.TP;

// ── Identity (sync) ──────────────────────────────────────────────────────────
// Name + colour: walk season-level teamNames/teamColors → liveSource back through the league's seasons,
// then the other leagues; codes = [short, ...aliases].
function fdtTeamMeta(league, seasonId, short) {
  const keys = [short, ...(FDT_ALIAS[short] || [])];
  const order = [league, ...Object.keys(UR2D_ADAPTERS).filter((l) => l !== league)];
  let name = null, color = null;
  for (const lg of order) {
    const seasons = (UR2D_ADAPTERS[lg] && UR2D_ADAPTERS[lg].seasons) || [];
    const i0 = lg === league ? Math.max(0, seasons.findIndex((s) => s.label === seasonId)) : 0;
    for (const s of seasons.slice(i0)) for (const o of [s, s.liveSource || {}]) for (const k of keys) {
      if (!name && o.teamNames && o.teamNames[k]) name = o.teamNames[k];
      if (!color && o.teamColors && o.teamColors[k]) color = o.teamColors[k];
    }
    if (name && color) break;
  }
  return { name: name || short, color };
}
const fdtLogo = (short) => (typeof UR_TEAM_LOGO !== 'undefined' && UR_TEAM_LOGO[short]) || null;
const fdtLines = (name) => { const w = String(name || '').trim().split(/\s+/); return w.length < 2 ? [w[0] || '', ''] : [w[0], w.slice(1).join(' ')]; };
const fdtPersonName = (p) => (p && (p.sheetName || p.display || p.handle)) || null;
function fdtCurrentSid(league) { const s = (getSeasons(league) || []).find((x) => x.isCurrent); return s ? s.id : null; }

// Current-season teams of a league, from the roster. Order = previous season's constructors' standings
// (champion first, via fdtOrderCache — filled by useFdtTeamOrder); teams new to the league go last, A→Z.
const fdtOrderCache = {};
function fdtPrevSid(league) {
  const ad = UR2D_ADAPTERS[league];
  if (!ad) return null;
  const i = ad.seasons.findIndex((s) => s.isCurrent);
  const prev = ad.seasons.slice(i + 1).find((s) => s.snapshot || s.liveSource || s.db); // s.db = DB-only finished season (e.g. JL S22B)
  return prev ? prev.label : null;
}
function fdtCurrentTeams(league) {
  const sid = fdtCurrentSid(league);
  const r = typeof getRoster === 'function' ? getRoster(league, sid) : null;
  if (!r) return [];
  const order = fdtOrderCache[league] || {};
  const rank = (t) => { for (const c of [t.short, ...(FDT_ALIAS[t.short] || [])]) if (order[c] != null) return order[c]; return Infinity; };
  return Object.keys(r.teams).map((short) => {
    const tr = getTeamRoster(league, short, sid), m = fdtTeamMeta(league, sid, short);
    return { short, name: m.name, color: m.color, logo: fdtLogo(short), drivers: tr ? tr.mains.filter(Boolean).map(fdtPersonName).filter(Boolean) : [] };
  }).sort((a, b) => rank(a) - rank(b) || a.name.localeCompare(b.name));
}
// Hook: previous season's constructors' order → fdtOrderCache[league]. Returns { ready, prevSid }.
// The previous season is FINISHED → read it from Bituko's DB (one small request); that season's sheet is the fallback.
const fdtOrderDone = {};
function useFdtTeamOrder(league) {
  const prevSid = fdtPrevSid(league);
  const [, bump] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => {
    if (!prevSid || fdtOrderCache[league] || fdtOrderDone[league]) return;
    let alive = true;
    const sdef = UR2D_ADAPTERS[league].seasons.find((s) => s.label === prevSid);
    const viaSheet = () => (sdef.liveSource ? loadLeaderboardStandings(sdef.liveSource).then((raw) => [...(raw.teams || [])].sort((a, b) => b.pts - a.pts).map((t) => t.short)) : Promise.reject(new Error('no sheet')));
    const p = typeof window.dbTeamOrder === 'function' ? window.dbTeamOrder(league, sdef).catch(viaSheet) : viaSheet();
    p.then((codes) => { if (codes && codes.length) { const o = {}; codes.forEach((c, i) => { if (o[c] == null) o[c] = i; }); fdtOrderCache[league] = o; } })
      .catch(() => {})
      .then(() => { fdtOrderDone[league] = true; if (alive) bump(); });
    return () => { alive = false; };
  }, [league, prevSid]);
  return { ready: !prevSid || !!fdtOrderCache[league] || !!fdtOrderDone[league], prevSid };
}

// Round-special liveries for a season (season-level specialLiveries, {CODE:[{rounds,name,file}]}); first matching code wins.
function fdtSpecials(sd, codes) {
  const sp = (sd && sd.specialLiveries) || {}, c = codes.find((x) => x && sp[x]);
  return c ? sp[c].slice().sort((a, b) => (parseInt(a.rounds, 10) || 99) - (parseInt(b.rounds, 10) || 99)) : []; // rounds null (archive) → last
}
function fdtIdentity(league, short) {
  const sid = fdtCurrentSid(league), m = fdtTeamMeta(league, sid, short);
  const ad = UR2D_ADAPTERS[league], sd = ad && ad.seasons.find((s) => s.label === sid);
  const ls = (sd && sd.liveSource) || {};
  const lv = sd ? { ...(ls.teamLiveries || {}), ...(sd.teamLiveries || {}) } : {};
  const ln = sd ? { ...(ls.liveryNames || {}), ...(sd.liveryNames || {}) } : {};
  return {
    league, short, sid, suffix: sd ? sd.suffix : null, name: m.name, color: m.color || '#9AA0A6', lines: fdtLines(m.name),
    logo: fdtLogo(short), roster: typeof getTeamRoster === 'function' ? getTeamRoster(league, short, sid) : null,
    livery: lv[short] || null, lname: ln[short] || null,
  };
}

// ── People → team-page PEOPLE map ('r:' prefix keeps real people apart from the sample profiles) ──
function fdtPeopleKey(name, d) {
  const P = fdtTP().PEOPLE;
  const who = typeof resolvePerson === 'function' ? resolvePerson(name) : null;
  const key = 'r:' + (who ? who.handle : 'x' + fdtNrm(name));
  if (!P[key]) P[key] = { name: String(name).trim(), cc: (who && who.country) || (d && d.country) || 'un', num: who && who.carNumber != null ? who.carNumber : d && d.carNumber != null ? d.carNumber : null };
  return key;
}
function fdtHandleKey(handle) {
  const P = fdtTP().PEOPLE, p = typeof getPerson === 'function' ? getPerson(handle) : null, key = 'r:' + handle;
  const reg = window.UR_DRIVER_NUMBER || {}, nm = fdtPersonName(p) || handle;
  const regNum = [nm, p && p.display, handle, ...((p && p.aliases) || [])].map((x) => x && reg[String(x).trim().toLowerCase()]).find((x) => x != null);
  if (!P[key]) P[key] = { name: nm, cc: (p && p.country) || 'un', num: p && p.carNumber != null ? p.carNumber : regNum != null ? regNum : null };
  return key;
}
function fdtVacant() { const P = fdtTP().PEOPLE; if (!P[FDT_VACANT]) P[FDT_VACANT] = { name: '—', cc: null, num: null, vacant: true }; }

// ── Which seasons did this team race? (newest first; one league per calendar season) ──
function fdtDataSeasons(league, short) {
  const codes = [short, ...(FDT_ALIAS[short] || [])], byIdx = {};
  ['ul', 'hl', 'jl'].forEach((lg) => {
    const ad = UR2D_ADAPTERS[lg];
    if (!ad || ad.teamBased === false) return;
    ad.seasons.forEach((sd, idx) => {
      if (!sd.liveSource) return;
      const names = { ...(sd.liveSource.teamNames || {}), ...(sd.teamNames || {}) };
      const code = codes.find((c) => names[c]);
      if (code && (!byIdx[idx] || lg === league)) byIdx[idx] = { lg, sd, idx, code };
    });
  });
  return Object.values(byIdx).sort((a, b) => a.idx - b.idx);
}

// ── One league season's sheets (cached; shared by every team in that season) ──
const fdtSeasonCache = {};
function fdtSeasonData(lg, sd) {
  const k = lg + ':' + sd.label;
  if (!fdtSeasonCache[k]) {
    const ls = sd.liveSource;
    // driverTeamTable dropped here: the round tabs are read once below (loadRoundDriverTeams), not twice.
    // topN dropped: low-scoring teams' drivers (e.g. HL S21 RGT) sit below the standings page's top-18 cut.
    const src = { ...ls, driverTeamTable: null, roundTeamTable: null, topN: null, teamLiveries: { ...(ls.teamLiveries || {}), ...(sd.teamLiveries || {}) }, teamNames: { ...(ls.teamNames || {}), ...(sd.teamNames || {}) }, sheetTeamNames: ls.teamNames || {} };
    fdtSeasonCache[k] = Promise.all([
      loadLeaderboardStandings(src).then((raw) => normalizeStandings(raw, { teamBased: true, label: sd.label })),
      loadRaceResults(lg, sd.label),
      loadRoundDriverTeams(lg, sd.label).catch(() => ({})),
    ]).then(([st, res, rmap]) => ({ st, res: res || {}, rmap: rmap || {} }))
      .catch((e) => { console.warn('[ur2d-teamdata] ' + k + ' failed:', e && e.message); delete fdtSeasonCache[k]; return null; });
  }
  return fdtSeasonCache[k];
}

function fdtTrack(race, n) {
  if (!race) return { key: 'r' + n, gp: 'Round ' + n, circuit: 'Round ' + n, cc: null, map: null, img: null };
  const mm = typeof urRaceMinimap === 'function' ? urRaceMinimap(race) : race.minimap;
  const map = mm ? mm.replace(/^assets\/minimaps\//, '').replace(/\.png$/i, '') : null;
  const img = race.trackImage && /^assets\/tracks\/sm\//.test(race.trackImage) ? race.trackImage.replace(/^assets\/tracks\/sm\//, '').replace(/\.jpg$/i, '') : null;
  return { key: map || race.track, gp: String(race.grandPrix || race.track).replace(/\s*Grand Prix$/i, ' GP'), circuit: race.track, cc: race.iso || (window.POSTER_GP_ISO && window.POSTER_GP_ISO[race.grandPrix]) || null, map, img };
}
const fdtDate = (race, cal) => race && race.date ? new Date(race.date + 'T12:00:00Z') : new Date(Date.UTC((cal && cal.year) || 2026, 0, 1));
const fdtGlyph = (r, c) => ({ round: r.n, pos: c.f, map: r.track.map, gp: r.track.gp, who: c.who });
function fdtAgg(cells) {
  const o = { pts: 0, wins: 0, podiums: 0, poles: 0, fl: 0, top5: 0, top10: 0, starts: 0, dnf: 0 };
  cells.forEach((c) => { o.starts++; o.pts += c.pts; if (c.f === 1) o.wins++; if (c.f <= 3) o.podiums++; if (c.f <= 5) o.top5++; if (c.f <= 10) o.top10++; if (c.pole) o.poles++; });
  o.pts = fdtR1(o.pts);
  return o;
}

function fdtBuildSeason(e, data, cal) {
  const { lg, sd, code } = e, { st, res, rmap } = data;
  const tRow = (st.teams || []).find((t) => t.short === code);
  if (!tRow) return null;
  const ns = Object.keys(res).map(Number).filter((n) => n > 0).sort((a, b) => a - b);
  if (!ns.length) return null;
  const orig = {}, byName = {};
  st.drivers.forEach((d) => { const k = fdtNrm(d.name); byName[k] = d; orig[k] = orig[k] || d.name; });
  ns.forEach((n) => res[n].forEach((r) => { const k = fdtNrm(r.name); orig[k] = orig[k] || r.name; }));
  // MAINS come from the sheet's LINEUPS (slots 1-2; user rule 2026-10-05) — not from who started most.
  // Anyone else who raced for the team fills an absent main's slot as a sub (reserve or emergency reserve).
  const luRaw = (st.lineups && [code, ...(FDT_ALIAS[code] || [])].map((c) => st.lineups[c]).find((x) => x && x.length)) || null;
  const luN = luRaw ? luRaw.map((x) => (x ? fdtNrm(x) : null)) : [];
  luN.forEach((k, i) => { if (k) orig[k] = orig[k] || luRaw[i]; });
  const lineup = luN.some(Boolean) ? new Set(luN.filter(Boolean)) : new Set(st.drivers.filter((d) => d.team === code).map((d) => fdtNrm(d.name)));
  const per = ns.map((n) => {
    const rm = rmap[n], has = rm && Object.keys(rm).length;
    return { n, racers: res[n].filter((r) => { const k = fdtNrm(r.name); return has ? rm[k] === code : lineup.has(k); }) };
  });
  const cnt = {};
  per.forEach((p) => p.racers.forEach((r) => { const k = fdtNrm(r.name); cnt[k] = (cnt[k] || 0) + 1; }));
  const cands = [...new Set([...Object.keys(cnt), ...lineup])].sort((a, b) =>
    (cnt[b] || 0) - (cnt[a] || 0) || (lineup.has(b) ? 1 : 0) - (lineup.has(a) ? 1 : 0) || ((byName[b] && byName[b].pts) || 0) - ((byName[a] && byName[a].pts) || 0));
  if (!cands.length) return null;
  const key = (k) => fdtPeopleKey(orig[k] || k, byName[k]);
  const useLu = luN.slice(0, 2).some(Boolean);
  const mainsN = useLu ? [luN[0] || null, luN[1] || null] : cands.slice(0, 2);
  const mains = mainsN.map((k) => { if (k) return key(k); fdtVacant(); return FDT_VACANT; });
  while (mains.length < 2) { fdtVacant(); mains.push(FDT_VACANT); mainsN.push(null); }
  const off = sd.suffix === 'B' ? 5 : 0;
  const rounds = per.map(({ n, racers }) => {
    const race = cal ? cal.races[off + n - 1] : null, taken = new Set();
    const cells = [0, 1].map((slot) => {
      const mk = mainsN[slot];
      let r = mk ? racers.find((x) => fdtNrm(x.name) === mk) : null, sub = false;
      if (!r) { r = racers.find((x) => { const k = fdtNrm(x.name); return !mainsN.includes(k) && !taken.has(k); }) || null; sub = !!r; }
      if (!r) return { slot, who: mains[slot], sub: false, dns: true, q: null, f: 0, pts: 0, pole: false, fl: false };
      const k = fdtNrm(r.name); taken.add(k);
      return { slot, who: sub ? key(k) : mains[slot], sub, q: typeof r.quali === 'number' ? r.quali : null, f: r.finish, pts: fdtR1(r.points), pole: r.quali === 1, fl: false };
    });
    return { n, track: fdtTrack(race, n), date: fdtDate(race, cal), cells, pts: fdtR1(cells.reduce((t, c) => t + c.pts, 0)) };
  });
  const lv = { ...(sd.liveSource.teamLiveries || {}), ...(sd.teamLiveries || {}) };
  const ln = { ...(sd.liveSource.liveryNames || {}), ...(sd.liveryNames || {}) };
  const s = {
    id: sd.label, sid: sd.label, code, n: sd.number, num: sd.label.replace(/^S/, ''), lg: lg.toUpperCase(), label: (cal && cal.period) || '',
    live: !!sd.isCurrent, pos: tRow.pos, ...fdtAgg(rounds.flatMap((r) => r.cells.filter((c) => !c.dns))),
    rounds, drivers: mains, livery: lv[code] || null, lname: ln[code] || null, specials: fdtSpecials(sd, [code, ...(FDT_ALIAS[code] || [])]),
  };
  s.pts = fdtR1(tRow.pts); s.fl = 0; // real count filled from the DB in fdtBuild (fdtDbFastestLaps)
  s.oneTwo = rounds.filter((r) => r.cells.every((c) => c.f && c.f <= 2)).length;
  s.dbl = rounds.filter((r) => r.cells.every((c) => c.f && c.f <= 3)).length;
  s.by = {}; s.winList = [];
  mains.forEach((w) => { s.by[w] = { pts: 0, wins: 0, podiums: 0, starts: 0, glyphs: [] }; });
  rounds.forEach((r) => r.cells.forEach((c) => {
    if (c.dns) return;
    const b = s.by[c.who] || (s.by[c.who] = { pts: 0, wins: 0, podiums: 0, starts: 0, glyphs: [] });
    b.starts++; b.pts += c.pts;
    if (c.f === 1) { b.wins++; s.winList.push(fdtGlyph(r, c)); }
    if (c.f <= 3) { b.podiums++; b.glyphs.push(fdtGlyph(r, c)); }
  }));
  Object.values(s.by).forEach((b) => { b.pts = fdtR1(b.pts); });
  if (lg === 'hl') mainsN.forEach((k, i) => { if (k && byName[k]) s.by[mains[i]].pts = fdtR1(byName[k].pts); });
  const h = { q: [0, 0], r: [0, 0] };
  rounds.forEach((r) => {
    const [a, b] = r.cells;
    if (a.sub || b.sub || a.dns || b.dns) return;
    if (a.q != null && b.q != null && a.q !== b.q) h.q[a.q < b.q ? 0 : 1]++;
    if (a.f !== b.f) h.r[a.f < b.f ? 0 : 1]++;
  });
  s.h2h = h;
  s.champ = {};
  mainsN.forEach((k, i) => { s.champ[mains[i]] = k && byName[k] ? byName[k].pos : null; });
  if (!sd.isCurrent) { // titles only for finished seasons
    s.wcc = tRow.pos === 1;
    const top = st.drivers[0], ti = top && top.pos === 1 ? mainsN.indexOf(fdtNrm(top.name)) : -1;
    if (ti >= 0) s.wdc = mains[ti];
  }
  return s;
}

function fdtAll(seasons, curKeys) {
  const P = fdtTP().PEOPLE;
  const flat = seasons.flatMap((s) => s.rounds.map((r) => ({ s, r })));
  const A = fdtAgg(flat.flatMap((x) => x.r.cells.filter((c) => !c.dns)));
  A.pts = fdtR1(seasons.reduce((t, s) => t + s.pts, 0)); A.fl = seasons.reduce((t, s) => t + (Number(s.fl) || 0), 0);
  A.oneTwo = seasons.reduce((t, s) => t + s.oneTwo, 0); A.dbl = seasons.reduce((t, s) => t + s.dbl, 0);
  A.wcc = seasons.filter((s) => s.wcc).length; A.wdc = seasons.filter((s) => s.wdc).length; A.seasons = seasons.length;
  const D = {};
  seasons.forEach((s) => Object.entries(s.by).forEach(([w, b]) => {
    if (w === FDT_VACANT) return;
    const d = D[w] || (D[w] = { w, p: P[w], pts: 0, wins: 0, podiums: 0, starts: 0, ids: [], titles: 0 });
    d.pts = fdtR1(d.pts + b.pts); d.wins += b.wins; d.podiums += b.podiums; d.starts += b.starts; d.ids.push(s.id);
  }));
  seasons.forEach((s) => { if (s.wdc && D[s.wdc]) D[s.wdc].titles++; });
  A.drivers = Object.values(D).map((d) => {
    const ids = [...d.ids].reverse();
    return Object.assign(d, { span: ids.length > 2 ? ids[0] + '–' + ids[ids.length - 1] : ids.join(', '), nSeasons: ids.length, current: curKeys.has(d.w) });
  }).sort((a, b) => b.pts - a.pts || b.starts - a.starts);
  A.bestDriver = A.drivers[0];
  const finLabel = (cells) => { const f = cells.filter((c) => !c.dns).map((c) => c.f).sort((a, b) => a - b); return f.length ? f.map((x) => 'P' + x).join(' & ') : 'No finish'; };
  const res = flat.map(({ s, r }) => ({ season: s.id, lg: s.lg, n: r.n, gp: r.track.gp, track: r.track, cells: r.cells, pts: r.pts, date: r.date, label: finLabel(r.cells) }));
  // Best/worst round ranked by FINISHING POSITIONS, not points — points scales differ across leagues/eras
  // (HL S21 P2+P4 = 60 pts outscored UL S25's P1-P2 one-two). Key = both slots' finishes sorted (no finish = 99).
  const fkey = (cells) => { const f = cells.map((c) => (!c.dns && c.f) ? c.f : 99).sort((a, b) => a - b); while (f.length < 2) f.push(99); return f; };
  const cmp = (a, b) => { for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return a[i] - b[i]; return 0; };
  const ran = res.filter((x) => x.cells.some((c) => !c.dns));
  const pick = (want) => {
    const pool = ran.length ? ran : res;
    const k0 = pool.map((x) => fkey(x.cells)).reduce((m, k) => (want === 'best' ? cmp(k, m) < 0 : cmp(k, m) > 0) ? k : m);
    const l = pool.filter((x) => cmp(fkey(x.cells), k0) === 0).sort((a, b) => a.date - b.date), x = l[l.length - 1];
    return { pts: x.pts, label: x.label, count: l.length, first: l[0], last: x, track: x.track, cells: x.cells, occ: l, byFinish: true };
  };
  A.best = pick('best'); A.worst = pick('worst');
  const oldest = seasons[seasons.length - 1], r0 = oldest.rounds[0], newest = seasons[0], rN = newest.rounds[newest.rounds.length - 1];
  A.first = { season: oldest.id, n: r0.n, gp: r0.track.gp, date: r0.date };
  A.last = { season: newest.id, n: rN.n, gp: rN.track.gp, date: rN.date };
  let lw = null;
  for (const s of seasons) {
    for (let i = s.rounds.length - 1; i >= 0 && !lw; i--) {
      const r = s.rounds[i], c = r.cells.find((x) => x.f === 1);
      if (c) lw = { season: s.id, lg: s.lg, n: r.n, track: r.track, date: r.date, who: c.who, q: c.q, pts: r.pts, mate: r.cells.find((x) => x !== c) };
    }
    if (lw) break;
  }
  A.lastWin = lw;
  A.titles = seasons.flatMap((s) => {
    const r = s.rounds[s.rounds.length - 1], o = [];
    if (s.wcc) o.push({ season: s.id, lg: s.lg, kind: 'wcc', round: r.n, track: r.track });
    if (s.wdc) o.push({ season: s.id, lg: s.lg, kind: 'wdc', who: s.wdc, round: r.n, track: r.track });
    return o;
  });
  A.bestPos = seasons.reduce((m, s) => (s.pos < m.pos ? s : m), seasons[0]);
  A.podList = seasons.flatMap((s) => s.rounds.flatMap((r) => r.cells.filter((c) => !c.dns && c.f <= 3).map((c) => ({ season: s.id, n: r.n, pos: c.f, map: r.track.map, gp: r.track.gp, who: c.who }))));
  A.places = seasons.filter((s) => s.pos === 2 || s.pos === 3).map((s) => ({ season: s.id, lg: s.lg, pos: s.pos, track: s.rounds[s.rounds.length - 1].track }));
  return A;
}

// Upcoming season (current, no workbook yet) as a pre-season entry: roster mains, calendar rounds with no
// results, this season's livery. Prepended to the selector only — never counted in the all-time stats.
function fdtPreseason(league, short, id, cals) {
  const ad = UR2D_ADAPTERS[league];
  if (!ad || !id.roster) return null;
  const idx = ad.seasons.findIndex((s) => s.isCurrent), sd = ad.seasons[idx];
  if (!sd || sd.liveSource || sd.snapshot) return null;
  const mains = id.roster.mains.filter(Boolean).slice(0, 2).map((p) => fdtHandleKey(p.handle));
  while (mains.length < 2) { fdtVacant(); mains.push(FDT_VACANT); }
  const c = cals[idx], cal = c ? { ...getCalendar(league, c.id), period: c.period } : null;
  const off = sd.suffix === 'B' ? 5 : 0, nR = ad.numRaces || 10;
  const rounds = Array.from({ length: nR }, (_, i) => {
    const n = i + 1, race = cal && cal.races ? cal.races[off + i] : null;
    return { n, track: fdtTrack(race, n), date: fdtDate(race, cal), pts: 0, cells: [0, 1].map((slot) => ({ slot, who: mains[slot], sub: false, dns: true, q: null, f: 0, pts: 0, pole: false, fl: false })) };
  });
  const by = {};
  mains.forEach((w) => { by[w] = { pts: 0, wins: 0, podiums: 0, starts: 0, glyphs: [] }; });
  if (id.roster.reserve) by[fdtHandleKey(id.roster.reserve.handle)] = { pts: 0, wins: 0, podiums: 0, starts: 0, glyphs: [] };
  return {
    id: sd.label, sid: sd.label, code: short, n: sd.number, num: sd.label.replace(/^S/, ''), lg: league.toUpperCase(), label: (cal && cal.period) || '',
    live: true, preseason: true, pos: '—', pts: 0, wins: 0, podiums: 0, poles: 0, fl: 0, top5: 0, top10: 0, starts: 0, dnf: 0,
    rounds, drivers: mains, livery: id.livery, lname: id.lname, oneTwo: 0, dbl: 0, by, winList: [], h2h: { q: [0, 0], r: [0, 0] }, champ: {},
  };
}

// ── Every OTHER season the team raced, built from Bituko's results DB (2026-10-07) ──
// Sheet-built seasons (current + recent workbooks) stay as they are; the DB fills the rest of the team's history,
// following its DB team_id through promotions and code changes. Mains = the 2 drivers with the most starts; anyone
// else fills an absent main's slot as a sub. Round points include sprints. Season position/points: the league Mega
// Table by default (ur2d-megacheck.jsx), the DB where the Mega Table has no value; disagreements kept in s.disputed.
const FDT_DB_SITE = { UL: 'ul', CL: 'hl', JL: 'jl', AL: 'al' };
function fdtChrono(s) { // UL-equivalent season clock, so seasons from different leagues order correctly
  const lg = String(s.lg || '').toLowerCase(), m = String(s.sid || s.id || '').match(/^S(\d+)([AB])?/);
  if (!m) return 0;
  const n = +m[1], half = m[2] === 'B' ? 0.5 : 0;
  if (lg === 'ul') return n;
  if (lg === 'al') return n + 3 + half;
  const mm = typeof urUlSeasonFor === 'function' ? urUlSeasonFor(lg === 'hl' ? 'CL' : 'JL', 'S' + n + (m[2] || '')) : null;
  return mm ? Number(mm.ul.slice(1)) + (mm.half === 2 ? 0.5 : 0) : n + 3 + half;
}
// Newest first on the UL-equivalent clock; same-clock ties → top league first (so the oldest = lowest league, matching
// the Historic tile's "first" season). Labels shared by two leagues (CL S6 + AL S6) get a league prefix on BOTH.
const FDT_TIER = { UL: 0, HL: 1, CL: 1, JL: 2, AL: 3 };
function fdtSortSeasons(seasons) {
  seasons.sort((a, b) => fdtChrono(b) - fdtChrono(a) || (FDT_TIER[a.lg] ?? 9) - (FDT_TIER[b.lg] ?? 9));
  const cnt = {};
  seasons.forEach((s) => { cnt[s.id] = (cnt[s.id] || 0) + 1; });
  seasons.forEach((s) => { if (cnt[s.id] > 1) s.id = s.lg + ' ' + s.id; });
  return seasons;
}
const fdtDbTeamIds = {};
async function fdtDbTeamId(short) { // shared codes (SPR = Sparta / Spark…) → the team that raced most recently
  if (fdtDbTeamIds[short] !== undefined) return fdtDbTeamIds[short];
  const codes = [short, ...(FDT_ALIAS[short] || [])];
  const teams = await dbGet('teams?select=id,name,short_name');
  const hit = teams.filter((t) => codes.includes(t.short_name));
  if (hit.length < 2) return (fdtDbTeamIds[short] = hit[0] ? { id: hit[0].id, code: hit[0].short_name } : null);
  const rows = await dbGet('v_team_standings?select=team_id,league,number,division&team_id=in.(' + hit.map((t) => t.id).join(',') + ')');
  let best = null, bt = -1;
  rows.forEach((r) => { const t = fdtChrono({ lg: FDT_DB_SITE[r.league], sid: 'S' + r.number + (r.division || '') }); if (t > bt) { bt = t; best = r.team_id; } });
  const t = hit.find((x) => x.id === best) || hit[0];
  return (fdtDbTeamIds[short] = { id: t.id, code: t.short_name });
}
function fdtDbUl(code, label) { // which UL calendar season (and half) a league season raced on
  if (code === 'UL') return { ul: label, off: 0 };
  const m = typeof urUlSeasonFor === 'function' ? urUlSeasonFor(code, label) : null;
  return m ? { ul: m.ul, off: m.half === 2 ? 5 : 0 } : null;
}
function fdtDbRace(code, label, n, dbRace) { // DB track/date + GP name, flag, minimap from the matching UL raceday
  const base = { track: dbRace.track || 'Round ' + n, grandPrix: null, date: dbRace.race_date || null, minimap: null, trackImage: null };
  const u = fdtDbUl(code, label);
  let cal = null;
  try { cal = u && typeof getCalendar === 'function' ? getCalendar('ul', u.ul) : null; } catch (e) { cal = null; }
  if (!cal || cal.season !== u.ul || !cal.races) return base;
  const same = (r) => { const a = fdtNrm(r && r.track), b = fdtNrm(dbRace.track); return !!a && !!b && (a.includes(b) || b.includes(a) || a.slice(0, 5) === b.slice(0, 5)); };
  const at = cal.races[u.off + n - 1];
  const cr = same(at) ? at : cal.races.find(same);
  return cr ? { ...cr, date: dbRace.race_date || cr.date } : base;
}
function fdtDbPeriod(code, label) {
  const own = code === 'JL' && window.UR_JL_SCHEDULES && window.UR_JL_SCHEDULES[label];
  if (own && own.name) return own.name;
  const u = fdtDbUl(code, label), c = u && getCalendarSeasons().find((x) => x.id === u.ul);
  return (c && c.period) || '';
}
function fdtDbSeason(w, ctx) {
  const { short, dbCode, rows, races, standing, champId, mega } = ctx;
  const raceRows = rows.filter((r) => r.session === 'race');
  if (!raceRows.some((r) => r.position != null)) return null;
  const nameOf = {}, ccOf = {};
  rows.forEach((r) => { if (r.drivers) { nameOf[r.driver_id] = r.drivers.name; ccOf[r.driver_id] = r.drivers.country_code ? String(r.drivers.country_code).toLowerCase() : null; } });
  const key = (id) => fdtPeopleKey(nameOf[id] || 'Driver ' + id, { country: ccOf[id] });
  const starts = {}, tot = {};
  raceRows.forEach((r) => { if (r.position != null) starts[r.driver_id] = (starts[r.driver_id] || 0) + 1; });
  rows.forEach((r) => { tot[r.driver_id] = (tot[r.driver_id] || 0) + (Number(r.points) || 0); });
  const mainIds = Object.keys(starts).map(Number).sort((a, b) => starts[b] - starts[a] || tot[b] - tot[a]).slice(0, 2);
  const mains = mainIds.map(key);
  while (mains.length < 2) { fdtVacant(); mains.push(FDT_VACANT); mainIds.push(null); }
  const ns = [...new Set([...races.map((r) => r.round), ...raceRows.map((r) => r.round)])].sort((a, b) => a - b);
  const rounds = ns.map((n) => {
    const race = fdtDbRace(w.league, w.label, n, races.find((r) => r.round === n) || { round: n });
    const here = raceRows.filter((r) => r.round === n && r.position != null), taken = new Set(), spr = {};
    rows.forEach((r) => { if (r.session === 'sprint' && r.round === n) spr[r.driver_id] = (spr[r.driver_id] || 0) + (Number(r.points) || 0); });
    const cells = [0, 1].map((slot) => {
      const mid = mainIds[slot];
      let r = mid != null ? here.find((x) => x.driver_id === mid) : null, sub = false;
      if (!r) { r = here.find((x) => !mainIds.includes(x.driver_id) && !taken.has(x.driver_id)) || null; sub = !!r; }
      if (!r) return { slot, who: mains[slot], sub: false, dns: true, q: null, f: 0, pts: 0, pole: false, fl: false };
      taken.add(r.driver_id);
      return { slot, who: sub ? key(r.driver_id) : mains[slot], sub, q: r.grid != null ? r.grid : null, f: r.position, pts: fdtR1((Number(r.points) || 0) + (spr[r.driver_id] || 0)), pole: r.grid === 1, fl: !!r.fastest_lap, dnf: !!r.dnf };
    });
    return { n, track: fdtTrack(race, n), date: fdtDate(race, null), cells, pts: fdtR1(cells.reduce((t, c) => t + c.pts, 0)) };
  });
  let pos = w.standing, pts = Number(w.points) || 0, disputed = null;
  const mt = mega && mega.seasons[w.label] && mega.seasons[w.label].teams;
  if (mt && !mt.incomplete) {
    const e = [dbCode, short, ...(FDT_ALIAS[short] || [])].map((c) => mt.byKey[c]).find(Boolean);
    if (e && e.mt != null) {
      pos = mt.entries.filter((x) => x.mt != null).sort((a, b) => b.mt - a.mt).indexOf(e) + 1;
      pts = e.mt;
      if (e.db != null && Math.abs(e.mt - e.db) > 0.001) disputed = { mt: e.mt, db: e.db };
    }
  }
  const ad = UR2D_ADAPTERS[w.lg], sd = ad && ad.seasons.find((s) => s.label === w.label);
  const lvAll = sd ? { ...((sd.liveSource && sd.liveSource.teamLiveries) || {}), ...(sd.teamLiveries || {}) } : {};
  const lnAll = sd ? { ...((sd.liveSource && sd.liveSource.liveryNames) || {}), ...(sd.liveryNames || {}) } : {};
  const lvCode = [short, ...(FDT_ALIAS[short] || []), dbCode].find((c) => lvAll[c]);
  const s = {
    id: w.label, sid: w.label, code: dbCode, n: w.number, num: w.label.replace(/^S/, ''), lg: w.lg.toUpperCase(), label: fdtDbPeriod(w.league, w.label),
    live: false, fromDb: true, disputed, pos, ...fdtAgg(rounds.flatMap((r) => r.cells.filter((c) => !c.dns))),
    rounds, drivers: mains, livery: lvCode ? lvAll[lvCode] : null, lname: lvCode ? lnAll[lvCode] || null : null,
    specials: fdtSpecials(sd, [short, ...(FDT_ALIAS[short] || []), dbCode]),
  };
  s.pts = fdtR1(pts);
  s.fl = rows.filter((r) => r.session === 'race' && r.fastest_lap).length;
  s.dnf = rounds.reduce((t, r) => t + r.cells.filter((c) => c.dnf).length, 0);
  s.oneTwo = rounds.filter((r) => r.cells.every((c) => c.f && c.f <= 2)).length;
  s.dbl = rounds.filter((r) => r.cells.every((c) => c.f && c.f <= 3)).length;
  s.by = {}; s.winList = [];
  mains.forEach((m) => { s.by[m] = { pts: 0, wins: 0, podiums: 0, starts: 0, glyphs: [] }; });
  rounds.forEach((r) => r.cells.forEach((c) => {
    if (c.dns) return;
    const b = s.by[c.who] || (s.by[c.who] = { pts: 0, wins: 0, podiums: 0, starts: 0, glyphs: [] });
    b.starts++; b.pts += c.pts;
    if (c.f === 1) { b.wins++; s.winList.push(fdtGlyph(r, c)); }
    if (c.f <= 3) { b.podiums++; b.glyphs.push(fdtGlyph(r, c)); }
  }));
  Object.values(s.by).forEach((b) => { b.pts = fdtR1(b.pts); });
  const h = { q: [0, 0], r: [0, 0] };
  rounds.forEach((r) => {
    const [a, b] = r.cells;
    if (a.sub || b.sub || a.dns || b.dns) return;
    if (a.q != null && b.q != null && a.q !== b.q) h.q[a.q < b.q ? 0 : 1]++;
    if (a.f !== b.f) h.r[a.f < b.f ? 0 : 1]++;
  });
  s.h2h = h;
  s.champ = {};
  mainIds.forEach((id, i) => { s.champ[mains[i]] = id != null && standing[id] != null ? standing[id] : null; });
  s.wcc = pos === 1;
  // WDC: the DB champion, unless the Mega Table crowns someone else (then the Mega Table's champion).
  const md = mega && mega.seasons[w.label] && mega.seasons[w.label].drivers;
  const mtChamp = md && !md.incomplete && md.champion ? [md.champion.mt.mtName, md.champion.mt.dbName].filter(Boolean).map(fdtNrm) : null;
  const ci = mainIds.findIndex((id) => id != null && (mtChamp ? mtChamp.includes(fdtNrm(nameOf[id])) : id === champId));
  if (ci >= 0) s.wdc = mains[ci];
  return s;
}
// DB side of a profile — FETCH ONLY, runs in parallel with the sheet reads (speed fix 2026-10-07: was a serial
// waterfall after the sheets: teamId → standings → results → driver standings → fastest laps). Seasons the sheets
// cover are filtered out at merge time (fdtDbSeasons), so a failed sheet season still falls back to the DB.
function fdtMega(lg) { return typeof loadMegaCompare === 'function' ? loadMegaCompare(lg).catch(() => null) : Promise.resolve(null); }
async function fdtDbFetch(short, pinned) { // pinned = { id, code }: a specific DB team (former teams — codes get reused)
  if (typeof dbGet !== 'function' || typeof dbGetAll !== 'function') return null;
  const team = pinned || await fdtDbTeamId(short);
  if (!team) return null;
  const flP = Promise.all([dbGetAll('v_result_points?select=season_id&session=eq.race&fastest_lap=is.true&team_id=eq.' + team.id), dbGet('seasons?select=id,number,division,leagues(code)')]);
  flP.catch(() => {});
  const cur = {};
  Object.entries(UR2D_ADAPTERS).forEach(([lg, ad]) => { const c = ad.seasons.find((s) => s.isCurrent); if (c) cur[lg] = c.label; });
  const st = await dbGet('v_team_standings?select=season_id,league,number,division,points,standing&team_id=eq.' + team.id);
  const want = st.map((r) => ({ ...r, lg: FDT_DB_SITE[r.league], label: 'S' + r.number + (r.division || '') })).filter((r) => r.lg && cur[r.lg] !== r.label);
  if (!want.length) return { team, want, flP };
  const sids = want.map((r) => r.season_id).join(',');
  const megaP = {};
  [...new Set(want.map((r) => r.lg))].forEach((lg) => { megaP[lg] = fdtMega(lg); });
  const [res, races, dst] = await Promise.all([
    dbGetAll('v_result_points?select=season_id,round,session,position,grid,points,dnf,dsq,fastest_lap,driver_id,drivers(name,country_code)&team_id=eq.' + team.id + '&season_id=in.(' + sids + ')&order=season_id,round'),
    dbGetAll('v_races?select=season_id,round,track,race_date&season_id=in.(' + sids + ')&order=season_id,round'),
    dbGetAll('v_driver_standings?select=season_id,driver_id,standing&season_id=in.(' + sids + ')'), // all drivers: champions + this team's drivers in one read
  ]);
  return { team, want, flP, megaP, res, races, dst };
}
// Fastest laps for the sheet-built seasons too (the sheets don't carry them): race-session FLs per season from the DB.
async function fdtDbFastestLaps(db, seasons) {
  const [rows, ss] = await db.flP;
  const keyOf = {};
  ss.forEach((x) => { if (x.leagues) keyOf[x.id] = FDT_DB_SITE[x.leagues.code] + ':S' + x.number + (x.division || ''); });
  const n = {};
  rows.forEach((r) => { const k = keyOf[r.season_id]; if (k) n[k] = (n[k] || 0) + 1; });
  seasons.forEach((s) => { if (!s.fromDb && !s.preseason) s.fl = n[s.lg.toLowerCase() + ':' + s.sid] || 0; });
}
async function fdtDbSeasons(db, short, built) {
  const covered = new Set(built.map((s) => s.lg.toLowerCase() + ':' + s.sid));
  const want = db.want.filter((r) => !covered.has(r.lg + ':' + r.label));
  if (!want.length || !db.res) return [];
  const out = [];
  for (const w of want) {
    try {
      const standing = {};
      let champId = null;
      db.dst.forEach((d) => { if (d.season_id === w.season_id) { standing[d.driver_id] = d.standing; if (d.standing === 1) champId = d.driver_id; } });
      const s = fdtDbSeason(w, { short, dbCode: db.team.code, rows: db.res.filter((r) => r.season_id === w.season_id), races: db.races.filter((r) => r.season_id === w.season_id), standing, champId, mega: await db.megaP[w.lg] });
      if (s) out.push(s);
    } catch (e) { console.warn('[ur2d-teamdata] DB season', w.league, w.label, e); }
  }
  return out;
}
// Shared lookups every team page needs — fetched once at idle so the first team click doesn't wait on them.
function fdtWarmShared(leagues) {
  if (typeof dbGet !== 'function') return;
  dbGet('teams?select=id,name,short_name').catch(() => {});
  dbGet('seasons?select=id,number,division,leagues(code)').catch(() => {});
  (leagues || []).forEach(fdtMega);
}

async function fdtBuild(league, short) {
  const id = fdtIdentity(league, short), entries = fdtDataSeasons(league, short), cals = getCalendarSeasons();
  fdtMega(league); // league-wide Mega Table compare is the slowest DB read — start it now, not after the team's standings
  const dbP = fdtDbFetch(short).catch((err) => { console.warn('[ur2d-teamdata] DB history', league, short, err); return null; });
  const datas = await Promise.all(entries.map((e) => fdtSeasonData(e.lg, e.sd)));
  const seasons = [];
  entries.forEach((e, i) => {
    if (!datas[i]) return;
    const c = cals[e.idx], cal = c ? { ...getCalendar(e.lg, c.id), period: c.period } : null;
    try { const s = fdtBuildSeason(e, datas[i], cal); if (s) seasons.push(s); } catch (err) { console.warn('[ur2d-teamdata] season', e.lg, e.sd.label, err); }
  });
  const db = await dbP;
  if (db) {
    try { seasons.push(...(await fdtDbSeasons(db, short, seasons))); } catch (err) { console.warn('[ur2d-teamdata] DB history', league, short, err); }
    try { await fdtDbFastestLaps(db, seasons); } catch (err) { console.warn('[ur2d-teamdata] DB fastest laps', short, err); }
  }
  fdtSortSeasons(seasons);
  const dataSeasons = seasons.slice();
  const pre = dataSeasons.length ? fdtPreseason(league, short, id, cals) : null;
  if (pre && dataSeasons[0].id !== pre.id) seasons.unshift(pre);
  const tr = id.roster, latest = seasons[0];
  let reserve = null;
  if (latest) { const r = getTeamRoster(latest.lg.toLowerCase(), latest.code, latest.sid); if (r && r.reserve) reserve = fdtHandleKey(r.reserve.handle); }
  const TEAM = {
    short, name: id.name, color: id.color, lines: id.lines, cc: null, logo: id.logo, mini: id.logo, noEst: true, reserve,
    owner: (tr && tr.owners.map(fdtPersonName).filter(Boolean).join(' · ')) || '—', principal: (tr && tr.principal && fdtPersonName(tr.principal)) || '—',
  };
  const curKeys = new Set(tr ? tr.mains.filter(Boolean).map((p) => 'r:' + p.handle) : []);
  const nav = {
    league,
    teams: (lg) => fdtCurrentTeams(lg).map((t) => ({ short: t.short, name: t.name, mini: t.logo })),
    go: (lg, sh) => { if (window.fdGo) window.fdGo('team/' + lg + '/' + encodeURIComponent(sh)); },
  };
  const prof = { TEAM, seasons, nav, all: dataSeasons.length ? fdtAll(dataSeasons, curKeys) : null, fresh: !dataSeasons.length, failed: !seasons.length && entries.length > 0 && datas.every((d) => !d) };
  fdtTP().profiles['real:' + league + ':' + short] = prof;
  return prof;
}

// Cached + de-duplicated. fdtProfileReady[k] = resolved profile (lets the page render with no loading flash).
const fdtProfileCache = {}, fdtProfileReady = {};
function fdtLoadProfile(league, short) {
  const k = league + ':' + short;
  if (!fdtProfileCache[k]) {
    fdtProfileCache[k] = (league === 'old' ? fdtBuildOld(short) : fdtBuild(league, short))
      .then((p) => { if (p.failed) delete fdtProfileCache[k]; else fdtProfileReady[k] = p; return p; })
      .catch((e) => { console.warn('[ur2d-teamdata] ' + k, e); delete fdtProfileCache[k]; return { fresh: true, failed: true, seasons: [] }; });
  }
  return fdtProfileCache[k];
}

// ── FORMER TEAMS (Teams → Historic tab) — route #team/old/<DB team id> ──
// Keyed by DB team id, not code: codes get reused by different teams over the years (SPR, BAR, TUR…).
// Whole history from the results DB (fdtDbFetch pinned to that team) — no sheets, no roster, no owner/principal.
// Display-name corrections where the results DB has the wrong name (user/community, 2026-10-09). Keyed by DB code.
const FDT_NAME_FIX = { MCJ: 'Meca Joni' }; // DB says "EcoBoost" — that was a different team; MCJ's JL S14A–S15B seasons = Meca Joni (ManBearGoat)
const fdtOldById = {};
let fdtOldList = null, fdtOldResolved = null;
const FDT_SITE_DB = { ul: 'UL', hl: 'CL', jl: 'JL', al: 'AL' };
// Every DB team not on a current grid (not in a league's latest DB season, not on our rosters by name / code+alias —
// the DB can lag a round). Sorted by constructors' titles top league first (UL → CL → JL → AL), then wins, then A–Z.
function fdtFormerTeams() {
  if (!fdtOldList) {
    fdtOldList = dbTeamHistory().then((teams) => {
      teams.forEach((t) => { const fix = FDT_NAME_FIX[t.code]; if (fix) t.name = fix; fdtOldById[t.id] = t; });
      const codes = new Set(), names = new Set();
      ['ul', 'hl', 'jl'].forEach((lg) => fdtCurrentTeams(lg).forEach((t) => { [t.short, ...(FDT_ALIAS[t.short] || [])].forEach((c) => codes.add(c)); names.add(fdtNrm(t.name)); }));
      const onGrid = (t) => names.has(fdtNrm(t.name)) || (!t.sharedCode && codes.has(t.code));
      const TIERS = ['UL', 'CL', 'JL', 'AL'], titlesIn = (t, lg) => t.titles.filter((x) => x.startsWith(lg + ' ')).length;
      fdtOldResolved = teams.filter((t) => !t.current && !onGrid(t)).sort((a, b) => {
        for (const lg of TIERS) { const x = titlesIn(b, lg), y = titlesIn(a, lg); if (x !== y) return x - y; }
        for (const lg of TIERS) { const x = (b.winsBy || {})[lg] || 0, y = (a.winsBy || {})[lg] || 0; if (x !== y) return x - y; }
        return a.name.localeCompare(b.name);
      });
      return fdtOldResolved;
    }).catch((e) => { fdtOldList = null; throw e; });
  }
  return fdtOldList;
}
function useFdtFormer() {
  const [st, setSt] = React.useState(() => (fdtOldResolved ? { status: 'ready', teams: fdtOldResolved } : { status: 'loading', teams: [] }));
  React.useEffect(() => {
    if (fdtOldResolved) return;
    let alive = true;
    fdtFormerTeams().then((teams) => alive && setSt({ status: 'ready', teams })).catch(() => alive && setSt({ status: 'error', teams: [] }));
    return () => { alive = false; };
  }, []);
  return st;
}
// One former team by DB id → { st: 'loading' | 'ready' | 'missing' | 'error', t }.
function useFdtOldEntry(on, key) {
  const [e, setE] = React.useState(() => (on && fdtOldById[key] ? { st: 'ready', t: fdtOldById[key] } : { st: 'loading', t: null }));
  React.useEffect(() => {
    if (!on) return;
    if (fdtOldById[key]) { setE({ st: 'ready', t: fdtOldById[key] }); return; }
    let alive = true;
    setE({ st: 'loading', t: null });
    fdtFormerTeams().then(() => alive && setE(fdtOldById[key] ? { st: 'ready', t: fdtOldById[key] } : { st: 'missing', t: null })).catch(() => alive && setE({ st: 'error', t: null }));
    return () => { alive = false; };
  }, [on, key]);
  return e;
}
const fdtOldLogo = (t) => { // shared code → only a name-keyed logo (UR_TEAM_LOGO_NAMED), else the code's logo could be the other team's
  if (!t) return null;
  const named = window.UR_TEAM_LOGO_NAMED && window.UR_TEAM_LOGO_NAMED[fdtNrm(t.name)];
  return named || (!t.sharedCode ? fdtLogo(t.code) : null);
};
const fdtOldLogoById = (key) => fdtOldLogo(fdtOldById[key]);
// Newest season car on record (season-level teamLiveries / liveSource), for the tile hover + identity.
function fdtOldCar(t) {
  const list = (t.list || [t.last]).slice().reverse();
  for (const s of list) {
    const sd = UR2D_ADAPTERS[FDT_DB_SITE[s.lg]] && UR2D_ADAPTERS[FDT_DB_SITE[s.lg]].seasons.find((x) => x.label === s.label);
    if (!sd) continue;
    const lv = { ...((sd.liveSource && sd.liveSource.teamLiveries) || {}), ...(sd.teamLiveries || {}) };
    const ln = { ...((sd.liveSource && sd.liveSource.liveryNames) || {}), ...(sd.liveryNames || {}) };
    if (lv[t.code]) return { file: lv[t.code], name: ln[t.code] || null, sid: s.label };
  }
  return null;
}
// Former teams have no colours on record → sampled from their cars (urLiveryColor, ur2d-livery-archive): the newest car with a
// real hue; only black/white/grey cars on record → that neutral.
const fdtOldColorCache = {};
function fdtOldColor(t) {
  if (fdtOldColorCache[t.id] !== undefined) return fdtOldColorCache[t.id];
  if (typeof urLiveryColor !== 'function') return null;
  let neutral = null;
  for (const s of (t.list || [t.last]).slice().reverse()) {
    const sd = UR2D_ADAPTERS[FDT_DB_SITE[s.lg]] && UR2D_ADAPTERS[FDT_DB_SITE[s.lg]].seasons.find((x) => x.label === s.label);
    const f = sd && ({ ...((sd.liveSource && sd.liveSource.teamLiveries) || {}), ...(sd.teamLiveries || {}) })[t.code];
    const c = f && urLiveryColor(f);
    if (!c) continue;
    if (!UR_LIVERY_NEUTRAL.has(c)) return (fdtOldColorCache[t.id] = c);
    neutral = neutral || c;
  }
  return (fdtOldColorCache[t.id] = neutral);
}
function fdtOldIdentity(t) {
  const lg = FDT_DB_SITE[t.last.lg] || 'ul', m = t.sharedCode ? { color: null } : fdtTeamMeta(lg, t.last.label, t.code), car = fdtOldCar(t);
  return {
    league: lg, short: t.code, key: String(t.id), old: true, sid: t.last.label, suffix: null, name: t.name, color: m.color || fdtOldColor(t) || '#3A3F45', lines: fdtLines(t.name), // known team colour wins; livery sample only fills the gaps
    logo: fdtOldLogo(t), roster: null, livery: car ? car.file : null, lname: car ? car.name : null,
  };
}
// Team-page switcher on a former team: the former teams that raced in that league (any season).
const fdtOldNavTeams = (lg) => (fdtOldResolved || []).filter((t) => t.leagues.includes(FDT_SITE_DB[lg])).map((t) => ({ short: t.code, key: String(t.id), name: t.name, mini: fdtOldLogo(t) }));
async function fdtBuildOld(key) {
  await fdtFormerTeams();
  const t = fdtOldById[key];
  if (!t) return { fresh: true, failed: true, seasons: [] };
  const id = fdtOldIdentity(t), short = t.code;
  fdtMega(id.league);
  const db = await fdtDbFetch(short, { id: t.id, code: t.code }).catch((err) => { console.warn('[ur2d-teamdata] DB history old:' + key, err); return null; });
  let seasons = [];
  if (db) { try { seasons = await fdtDbSeasons(db, short, []); } catch (err) { console.warn('[ur2d-teamdata] DB history old:' + key, err); } }
  fdtSortSeasons(seasons);
  const TEAM = { short, key: id.key, name: t.name, color: id.color, lines: id.lines, cc: null, logo: id.logo, mini: id.logo, noEst: true, reserve: null, owner: '—', principal: '—' };
  const nav = { league: id.league, teams: fdtOldNavTeams, go: (lg, sh, x) => { if (window.fdGo && x && x.key) window.fdGo('team/old/' + x.key); } };
  const prof = { TEAM, seasons, nav, all: seasons.length ? fdtAll(seasons, new Set()) : null, fresh: !seasons.length, failed: !db };
  fdtTP().profiles['real:old:' + key] = prof;
  return prof;
}

Object.assign(window, { fdtFormerTeams, useFdtFormer, useFdtOldEntry, fdtOldIdentity, fdtOldLogo, fdtOldLogoById, fdtOldCar });
Object.assign(window, { fdtNrm, fdtR1, fdtTrack, fdtDate, fdtChrono, fdtSortSeasons, fdtDbRace, fdtDbPeriod, fdtSeasonData, fdtMega, FDT_NAME_FIX }); // shared with driver pages (ur2d-final-driverdata.jsx)
Object.assign(window, { FDT_ALIAS, fdtTeamMeta, fdtLogo, fdtLines, fdtPersonName, fdtCurrentSid, fdtCurrentTeams, useFdtTeamOrder, fdtIdentity, fdtLoadProfile, fdtProfileReady, fdtWarmShared });
