/* Who Still Qualifies? — app. Requires wsq-data.js + wsq-engine.js first. */
const { useState, useEffect, useRef, useMemo, useCallback } = React;
const { table, fates, remainingKeys, bestThirds, mergeLiveScores } = window.WSQEngine;
const WSQ = window.WSQ;
const T = WSQ.teams;

const FATE = {
  through:   { label: 'Through',        cls: 'through',   mark: '✓', note: 'Into the last 32' },
  inhands:   { label: 'In their hands', cls: 'inhands',   mark: '●', note: "Win and you're in" },
  needshelp: { label: 'Needs help',     cls: 'needshelp', mark: '●', note: 'Depends on other results' },
  thirdhope: { label: 'Third hope',     cls: 'thirdhope', mark: '◆', note: 'Chasing a best-third place' },
  out:       { label: 'Out',            cls: 'out',       mark: '✕', note: 'Eliminated' },
};

const CONTENT = window.WSQ_CONTENT;
const EX = window.WSQ_Extras;

function ChantRibbon({ color, rev }) {
  const line = CONTENT.chants.map((c) => c.t).join('\u2003\u2605\u2003') + '\u2003\u2605\u2003';
  return (
    <div className={`ribbon ${color || 'green'} ${rev ? 'rev' : ''}`} aria-hidden="true">
      <div className="marquee-track"><span>{line}</span><span>{line}</span></div>
    </div>
  );
}

/* ---------- small pieces ---------- */
function Chip({ code, size = 20 }) {
  const t = T[code] || { c: ['#888', '#555'] };
  return (
    <span className="chip" style={{
      width: size, height: size,
      background: `linear-gradient(125deg, ${t.c[0]} 0 52%, ${t.c[1]} 52% 100%)`,
    }} aria-hidden="true" />
  );
}

function Stepper({ value, onInc, onDec }) {
  return (
    <span className="stepper">
      <button className="step up" onClick={onInc} aria-label="increase">▲</button>
      <span className="step-val">{value == null ? '–' : value}</span>
      <button className="step dn" onClick={onDec} aria-label="decrease">▼</button>
    </span>
  );
}

const koTime = (iso) => { try { return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } catch (e) { return ''; } };

/* ---------- today's matches ticker (replaces the static flags strip) ---------- */
function TodayScores({ rows }) {
  const today = new Date().toISOString().slice(0, 10);
  let list = rows.filter((r) => (r.utc_date || '').slice(0, 10) === today);
  let label = "Today's matches";
  if (!list.length) {
    list = rows.filter((r) => r.status === 'TIMED' && r.utc_date).sort((a, b) => a.utc_date.localeCompare(b.utc_date)).slice(0, 6);
    label = 'Next up';
  } else {
    list = list.sort((a, b) => (a.utc_date || '').localeCompare(b.utc_date || ''));
  }
  if (!list.length) return null;
  const Cell = ({ m }) => {
    const live = m.status === 'IN_PLAY' || m.status === 'PAUSED';
    const done = m.status === 'FINISHED';
    return (
      <span className={`td-m ${live ? 'live' : ''} ${done ? 'done' : ''}`}>
        <Chip code={m.home} size={14} /><b>{m.home}</b>
        <span className="td-sc">{(live || done) ? `${m.home_goals ?? 0}–${m.away_goals ?? 0}` : koTime(m.utc_date)}</span>
        <b>{m.away}</b><Chip code={m.away} size={14} />
        {live && <i className="td-flag live">LIVE</i>}
        {done && <i className="td-flag ft">FT</i>}
      </span>
    );
  };
  return (
    <div className="today-strip" aria-label={label}>
      <div className="today-inner">
        <span className="today-lbl">⚽ {label}</span>
        {list.map((m, i) => <Cell key={i} m={m} />)}
      </div>
    </div>
  );
}

/* ---------- FIFA seeding for TAG ----------
   Official FIFA/Coca-Cola Men's World Ranking, approved update of 11 June 2026 (window
   INTL_MW_2026_04 — the final ranking before the tournament; next update 20 July 2026, so
   frozen for the whole group stage). Source: api.fifa.com/api/v3/fifarankings/rankings/live,
   the feed inside.fifa.com renders. Lower number = stronger. This is the embedded mirror of
   the worker's canonical copy (served at /rankings) — kept for instant + offline render. */
const RANKINGS = {
  MEX: 13, RSA: 61, KOR: 22, CZE: 43, // A
  CAN: 31, BIH: 63, QAT: 50, SUI: 19, // B
  BRA: 6, MAR: 7, HAI: 84, SCO: 37,   // C
  USA: 15, PAR: 42, AUS: 27, TUR: 23, // D
  GER: 10, CUW: 82, CIV: 33, ECU: 24, // E
  NED: 8, JPN: 18, SWE: 39, TUN: 45,  // F
  BEL: 9, EGY: 29, IRN: 20, NZL: 85,  // G
  ESP: 2, CPV: 67, KSA: 60, URU: 17,  // H
  FRA: 3, SEN: 16, IRQ: 57, NOR: 30,  // I
  ARG: 1, ALG: 28, AUT: 25, JOR: 64,  // J
  POR: 5, COD: 46, UZB: 51, COL: 14,  // K
  ENG: 4, CRO: 11, GHA: 73, PAN: 34,  // L
};
// Active ranking map: the embedded snapshot, replaceable once by the worker's /rankings feed
// (source of truth) so a FIFA correction ships without a JS cache-bust. 99 = unseeded fallback
// so an unknown code never reads as a giant.
let ACTIVE_RANKS = RANKINGS;
const rk = (code) => ACTIVE_RANKS[code] || 99;

/* ---------- TAG (Talk About the Group): a headline generated ONLY from real results.
   "Giant" and "underdog" come from the real FIFA ranking above, and an upset is weighted by
   the ranking GAP — not draw-slot order. So two evenly-ranked sides splitting points isn't
   spun as a shock, and a genuine minnow toppling a top side is. ---------- */
function groupStory(group, rows) {
  const teams = group.teams;
  const gm = (rows || []).filter((r) => r.group_code === group.id && r.status === 'FINISHED' && r.home_goals != null);
  let best = null;
  for (const m of gm) {
    if (teams.indexOf(m.home) < 0 || teams.indexOf(m.away) < 0) continue;
    const rh = rk(m.home), ra = rk(m.away);
    const giantHome = rh <= ra;                          // lower rank number = stronger side
    const giant = giantHome ? m.home : m.away, under = giantHome ? m.away : m.home;
    const gRank = Math.min(rh, ra), uRank = Math.max(rh, ra), gap = uRank - gRank;
    const gG = giantHome ? m.home_goals : m.away_goals;  // giant's goals
    const uG = giantHome ? m.away_goals : m.home_goals;  // underdog's goals
    const gn = (T[giant] || {}).name || giant, un = (T[under] || {}).name || under;
    const fame = Math.max(0, 50 - gRank);                // toppling an elite side is bigger news
    let text = null, weight = 0;
    if (uG > gG && (gap >= 8 || uG - gG >= 3)) {          // the lower-ranked side won
      weight = 60 + gap * 1.4 + fame * 0.8 + (uG - gG) * 3;
      text = gap >= 25
        ? `🚨 SHOCK — ${un} (#${uRank}) stun ${gn} (#${gRank}) ${uG}–${gG}!`
        : `😮 UPSET — ${un} (#${uRank}) beat ${gn} (#${gRank}) ${uG}–${gG}.`;
    } else if (uG === gG && gap >= 18) {                  // a genuine minnow held a top side
      weight = 35 + gap + fame * 0.6;
      text = `😯 ${un} (#${uRank}) hold ${gn} (#${gRank}) to a ${m.home_goals}–${m.away_goals} draw — points dropped.`;
    } else if (gG - uG >= 4) {                            // statement win to form
      weight = 25 + (gG - uG) * 2;
      text = `💥 ${gn} sweep aside ${un} ${gG}–${uG}.`;
    }
    if (text && weight > (best ? best.weight : 0)) best = { text, weight, gid: group.id };
  }
  // No standout? Call the trend by ranking: did every decided game go to the favourite?
  if (!best && gm.length) {
    let toForm = 0, decisive = 0;
    for (const m of gm) {
      if (teams.indexOf(m.home) < 0 || teams.indexOf(m.away) < 0) continue;
      const favHome = rk(m.home) <= rk(m.away);
      const favG = favHome ? m.home_goals : m.away_goals, dogG = favHome ? m.away_goals : m.home_goals;
      if (favG === dogG) continue;                        // draws are neither chalk nor upset
      decisive++; if (favG > dogG) toForm++;
    }
    if (decisive && toForm === decisive) best = { text: `✅ Form holds — the favourites are doing their job. No upsets in Group ${group.id}… yet.`, weight: 1, gid: group.id };
    else best = { text: `Group ${group.id} is taking shape — ${gm.length} game${gm.length === 1 ? '' : 's'} in, all to play for.`, weight: 1, gid: group.id };
  }
  return best;
}

/* ---------- per-group live pulse: real scores + a verified computed stat ---------- */
function GroupPulse({ group, rows, rev }) {
  const story = groupStory(group, rows);
  const gid = group.id;
  const useM = (rows || []).filter((r) => r.group_code === gid);
  const fin = useM.filter((m) => m.status === 'FINISHED' && m.home_goals != null);
  const live = useM.filter((m) => m.status === 'IN_PLAY' || m.status === 'PAUSED');
  const shown = [...live, ...fin].slice(0, 6);
  let stat = '';
  if (fin.length) {
    const goals = fin.reduce((s, m) => s + m.home_goals + m.away_goals, 0);
    const big = fin.slice().sort((a, b) => Math.abs(b.home_goals - b.away_goals) - Math.abs(a.home_goals - a.away_goals))[0];
    const draws = fin.filter((m) => m.home_goals === m.away_goals).length;
    const bits = [`${goals} goal${goals === 1 ? '' : 's'} in ${fin.length} game${fin.length === 1 ? '' : 's'}`];
    if (big && big.home_goals !== big.away_goals) bits.push(`biggest: ${big.home} ${big.home_goals}–${big.away_goals} ${big.away}`);
    if (draws) bits.push(`${draws} draw${draws === 1 ? '' : 's'}`);
    stat = bits.join(' · ');
  }
  if (!shown.length && !stat) return null;
  return (
    <div className="gpulse">
      <div className="gpulse-head">Group {gid} · live<span className="gp-note">official results</span></div>
      {shown.length > 0 && (
        <div className="gpulse-rows">
          {shown.map((m, i) => {
            const lv = m.status === 'IN_PLAY' || m.status === 'PAUSED';
            return (
              <span className={`gp-m ${lv ? 'live' : ''}`} key={i}>
                <span className="gp-t">{m.home}</span>
                <b>{m.home_goals ?? 0}–{m.away_goals ?? 0}</b>
                <span className="gp-t">{m.away}</span>
                {lv ? <i className="gp-live">LIVE</i> : <i className="gp-ft">FT</i>}
              </span>
            );
          })}
        </div>
      )}
      {story && <p className="gtag" title="TAG = Talk About the Group">🗣️ <b>TAG</b> <span className="gtag-x">(Talk About the Group)</span>: {story.text}</p>}
      {stat && <p className="gpulse-stat">📊 {stat}</p>}
    </div>
  );
}

/* ---------- standings table ---------- */
function Standings({ group, picks, rev }) {
  const rows = useMemo(() => table(group, picks), [group, picks, rev]);
  const fate = useMemo(() => fates(group, picks), [group, picks, rev]);
  return (
    <div className="table">
      <div className="t-head">
        <span className="c-pos">#</span>
        <span className="c-team">Team</span>
        <span className="c-n">P</span>
        <span className="c-n">W</span>
        <span className="c-n">D</span>
        <span className="c-n">L</span>
        <span className="c-gd">GD</span>
        <span className="c-pts">Pts</span>
        <span className="c-fate">Fate</span>
      </div>
      {rows.map((r, i) => {
        const code = group.teams[r.idx];
        const f = FATE[fate[r.idx]];
        return (
          <React.Fragment key={code}>
            <div className={`t-row ${r.rank <= 2 ? 'qual' : ''}`}>
              <span className="c-pos">{r.rank}</span>
              <span className="c-team">
                <Chip code={code} />
                <span className="t-code">{code}</span>
                <span className="t-name">{T[code].name}</span>
                {r.dagger && <span className="dagger" title="Level beyond goals scored — fair-play/ranking would decide">†</span>}
              </span>
              <span className="c-n dim">{r.P}</span>
              <span className="c-n">{r.W}</span>
              <span className="c-n">{r.D}</span>
              <span className="c-n">{r.L}</span>
              <span className="c-gd">{r.GD > 0 ? '+' + r.GD : r.GD}</span>
              <span className="c-pts">{r.Pts}</span>
              <span className="c-fate">
                <span className={`badge ${f.cls}`}><i className="dot" />{f.label}</span>
                <span className="fate-note">{f.note}</span>
              </span>
            </div>
            {r.rank === 2 && <div className="qual-line"><span>Qualification line · top two advance</span></div>}
          </React.Fragment>
        );
      })}
    </div>
  );
}

/* ---------- match editor ---------- */
function MatchCard({ group, mkey, picks, locked, started, live, sc, onSet, onClear }) {
  const [i, j] = mkey.split('-').map(Number);
  const a = group.teams[i], b = group.teams[j];
  const hs = sc ? sc[0] : null, as = sc ? sc[1] : null;
  const set = (nh, na) => onSet(mkey, [Math.max(0, Math.min(9, nh)), Math.max(0, Math.min(9, na))]);
  const outcome = sc ? (hs > as ? 'H' : hs < as ? 'A' : 'D') : null;

  if (locked) {
    return (
      <div className="match locked">
        <span className="m-team left"><span className="t-code">{a}</span><Chip code={a} size={18} /></span>
        <span className="m-score"><b>{hs}</b><span className="sep">–</span><b>{as}</b></span>
        <span className="m-team right"><Chip code={b} size={18} /><span className="t-code">{b}</span></span>
        <span className="m-lock" title="Official result — locked">🔒</span>
      </div>
    );
  }

  // Kicked off but not yet an official final: the server already ignores edits to it, so the
  // UI goes read-only to match what scores. We show the live score (if any) and freeze the
  // user's pre-kickoff pick — that pick is what counts.
  if (started) {
    const lh = live ? (live.home === a ? live.home_goals : live.away_goals) : null;
    const la = live ? (live.home === a ? live.away_goals : live.home_goals) : null;
    const inPlay = live && (live.status === 'IN_PLAY' || live.status === 'PAUSED');
    return (
      <div className="match kickoff">
        <div className="m-main">
          <span className="m-team left"><span className="t-code">{a}</span><Chip code={a} size={18} /></span>
          <span className="m-score">
            {lh != null ? <React.Fragment><b>{lh}</b><span className="sep">–</span><b>{la}</b></React.Fragment> : <span className="m-kotxt">KICK&#8209;OFF</span>}
          </span>
          <span className="m-team right"><Chip code={b} size={18} /><span className="t-code">{b}</span></span>
          <span className="m-lock" title="Locked at kick-off — your pre-kickoff pick is what scores">🔒</span>
        </div>
        <div className="m-kofoot">
          <span className={`m-kostate ${inPlay ? 'live' : ''}`}>{inPlay ? '● LIVE · locked' : 'Locked at kick-off'}</span>
          <span className="m-kopick">{sc ? `Your pick stands: ${hs}–${as}` : 'No pick locked in'}</span>
        </div>
      </div>
    );
  }
  return (
    <div className={`match editable ${sc ? 'set' : ''}`}>
      <div className="m-main">
        <span className="m-team left"><span className="t-code">{a}</span><Chip code={a} size={18} /></span>
        <span className="m-edit">
          <Stepper value={hs} onInc={() => set((hs ?? 0) + 1, as ?? 0)} onDec={() => set((hs ?? 0) - 1, as ?? 0)} />
          <span className="sep">–</span>
          <Stepper value={as} onInc={() => set(hs ?? 0, (as ?? 0) + 1)} onDec={() => set(hs ?? 0, (as ?? 0) - 1)} />
        </span>
        <span className="m-team right"><Chip code={b} size={18} /><span className="t-code">{b}</span></span>
      </div>
      <div className="m-quick">
        <button className={outcome === 'H' ? 'on' : ''} onClick={() => set(1, 0)}>{a} win</button>
        <button className={outcome === 'D' ? 'on' : ''} onClick={() => set(1, 1)}>Draw</button>
        <button className={outcome === 'A' ? 'on' : ''} onClick={() => set(0, 1)}>{b} win</button>
        <button className="clear" disabled={!sc} onClick={() => onClear(mkey)} title="Clear">✕</button>
      </div>
    </div>
  );
}

function Matches({ group, picks, rows, onSet, onClear }) {
  const played = WSQ.schedule.filter((s) => group.results[s.key]);
  const openAll = WSQ.schedule.filter((s) => !group.results[s.key]);
  // Find the live row for a schedule key (orientation-agnostic), and treat any match that has
  // kicked off (status != TIMED) as locked — same rule the worker enforces server-side.
  const liveFor = (key) => {
    const [i, j] = key.split('-').map(Number);
    const a = group.teams[i], b = group.teams[j];
    return (rows || []).find((r) => r.group_code === group.id && ((r.home === a && r.away === b) || (r.home === b && r.away === a)));
  };
  const startedRow = {};
  openAll.forEach((s) => { const r = liveFor(s.key); if (r && r.status && r.status !== 'TIMED') startedRow[s.key] = r; });
  const open = openAll.filter((s) => !startedRow[s.key]);
  const started = openAll.filter((s) => startedRow[s.key]);
  const setN = open.filter((s) => picks[s.key]).length;
  return (
    <div className="matches">
      <div className="m-block">
        <div className="m-block-head">
          <span className="lbl">Matchday 3 · your call</span>
          <span className="prog">{setN}/{open.length} set</span>
        </div>
        {open.map((s) => (
          <MatchCard key={s.key} group={group} mkey={s.key} picks={picks}
            locked={false} sc={picks[s.key] || null} onSet={onSet} onClear={onClear} />
        ))}
        {open.length === 0 && <p className="m-empty">Every match here has kicked off — predictions are locked 🔒</p>}
      </div>
      {started.length > 0 && (
        <div className="m-block">
          <div className="m-block-head"><span className="lbl">Kicked off · locked 🔒</span><span className="prog dim">your pick stands</span></div>
          {started.map((s) => (
            <MatchCard key={s.key} group={group} mkey={s.key} picks={picks} started={true} live={startedRow[s.key]} sc={picks[s.key] || null} />
          ))}
        </div>
      )}
      <div className="m-block">
        <div className="m-block-head"><span className="lbl dim">Already played</span><span className="prog dim">locked 🔒</span></div>
        {played.map((s) => (
          <MatchCard key={s.key} group={group} mkey={s.key} picks={picks} locked={true} sc={group.results[s.key]} />
        ))}
      </div>
    </div>
  );
}

/* ---------- best thirds ---------- */
function BestThirds({ allPicks, rev }) {
  const ranked = useMemo(() => bestThirds(allPicks), [allPicks, rev]);
  return (
    <div className="thirds">
      {ranked.map((x, i) => (
        <React.Fragment key={x.gid}>
          {i === 8 && <div className="cut"><span>Cut</span></div>}
          <div className={`third ${i < 8 ? 'in' : 'outc'}`}>
            <span className="th-seed">{x.seed}</span>
            <span className="th-grp">Grp {x.gid}</span>
            <span className="th-team"><Chip code={x.code} size={18} /><span className="t-code">{x.code}</span></span>
            <span className="th-meta">{x.Pts} pts · {x.GD > 0 ? '+' + x.GD : x.GD}</span>
            {!x.decided && <span className="th-live">live</span>}
          </div>
        </React.Fragment>
      ))}
    </div>
  );
}

/* ---------- group strip ---------- */
function GroupStrip({ active, picks, onPick }) {
  return (
    <div className="strip">
      {WSQ.groups.map((g) => {
        const edited = picks[g.id] && Object.keys(picks[g.id]).length > 0;
        return (
          <button key={g.id} className={`pill ${g.id === active ? 'on' : ''}`} onClick={() => onPick(g.id)}>
            <span className="pl-letter">{g.id}</span>
            <span className="pl-teams">{g.teams.join(' ')}</span>
            {edited && <i className="pl-dot" title="You edited this group" />}
          </button>
        );
      })}
    </div>
  );
}

/* ---------- countdown to the next prediction lock (kick-off) ---------- */
function fmtCountdown(ms) {
  if (ms <= 0) return '0:00';
  const s = Math.floor(ms / 1000);
  const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60), sec = s % 60;
  const p = (n) => String(n).padStart(2, '0');
  return (d > 0 ? d + 'd ' : '') + p(h) + ':' + p(m) + ':' + p(sec);
}
function LockCountdown({ rows, onPredict }) {
  const [now, setNow] = useState(Date.now());
  useEffect(() => { const id = setInterval(() => setNow(Date.now()), 1000); return () => clearInterval(id); }, []);
  const next = (rows || [])
    .filter((r) => r.status === 'TIMED' && r.utc_date && new Date(r.utc_date).getTime() > now)
    .sort((a, b) => new Date(a.utc_date) - new Date(b.utc_date))[0];
  if (!next) return null;
  const ms = new Date(next.utc_date).getTime() - now;
  const urgent = ms < 3600 * 1000;
  return (
    <div className={`lockbar ${urgent ? 'urgent' : ''}`}>
      <span className="lockbar-msg">
        <b>🔒 Predictions lock at kick-off.</b> Tweak your scores anytime — but a change after a match starts <b>won't count</b>. Get it in before then.
      </span>
      <span className="lockbar-count">
        <span className="lc-lbl">Next lock-in · {T[next.home] ? next.home : next.home} v {next.away} (Grp {next.group_code})</span>
        <span className="lc-clock">{fmtCountdown(ms)}</span>
      </span>
      <button className="cta lockbar-cta" onClick={() => onPredict(next.group_code)}>Predict now →</button>
    </div>
  );
}

/* ---------- feedback / issue report (emails Raja) ---------- */
function FeedbackForm({ me }) {
  const [msg, setMsg] = useState('');
  const [email, setEmail] = useState(me ? me.email : '');
  const [state, setState] = useState('idle'); // idle | sending | sent
  const [err, setErr] = useState('');
  const API = window.WSQ_API || '';
  useEffect(() => { if (me && !email) setEmail(me.email); }, [me]);
  if (!API) return null;
  const submit = async (e) => {
    e.preventDefault();
    if (msg.trim().length < 5) { setErr('Add a sentence or two so it’s actionable.'); return; }
    setState('sending'); setErr('');
    try {
      const r = await fetch(API + '/feedback', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message: msg.trim(), email: email.trim(), context: 'Who Still Qualifies · predictor leaderboard' }),
      });
      if (r.ok) { setState('sent'); setMsg(''); }
      else { const d = await r.json().catch(() => ({})); setErr(d.error || 'Could not send — try again later.'); setState('idle'); }
    } catch (_) { setErr('Could not reach the server — try again later.'); setState('idle'); }
  };
  if (state === 'sent') return (
    <div className="fb-box sent"><b>Thanks — that went straight to Raja. 🙌</b><p>If it’s a bug, he’ll dig in. Want to add more? Reload and send again.</p></div>
  );
  return (
    <div className="fb-box">
      <div className="fb-head">Still looks wrong? Tell Raja directly</div>
      <p className="fb-sub">Leaderboard not updating, a score looks off, your name’s missing and you <em>did</em> predict in time — describe it and it lands in his inbox.</p>
      <form onSubmit={submit}>
        <textarea className="fb-msg" rows={3} maxLength={2000} placeholder="What happened? (what you did, what you expected, what you saw)" value={msg} onChange={(e) => setMsg(e.target.value)} />
        <div className="fb-row">
          <input type="email" className="fb-email" placeholder="Your email (optional — for a reply)" value={email} onChange={(e) => setEmail(e.target.value)} maxLength={120} />
          <button className="cta" type="submit" disabled={state === 'sending'}>{state === 'sending' ? 'Sending…' : 'Send to Raja →'}</button>
        </div>
        {err && <p className="fb-err">{err}</p>}
      </form>
    </div>
  );
}

/* ---------- app ---------- */
function encode(p) { return btoa(unescape(encodeURIComponent(JSON.stringify(p)))); }
function decode(s) { try { return JSON.parse(decodeURIComponent(escape(atob(s)))); } catch (e) { return null; } }
function prune(p) { const o = {}; for (const k in p) if (p[k] && Object.keys(p[k]).length) o[k] = p[k]; return o; }

function App() {
  const [active, setActive] = useState('A');
  const [picks, setPicks] = useState({});
  const [toast, setToast] = useState(null);
  const [scoresRev, setScoresRev] = useState(0); // bumps when live official results change
  const [scoresRaw, setScoresRaw] = useState([]); // raw /live score rows (today's matches + per-group fixtures)
  const toastTimer = useRef(null);
  const firstScores = useRef(true);

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    const s = params.get('s');
    let init = s && decode(s);
    if (!init) { try { init = JSON.parse(localStorage.getItem('wsq_picks_v1') || '{}'); } catch (e) {} }
    if (init && typeof init === 'object') setPicks(init);
  }, []);

  useEffect(() => {
    try { localStorage.setItem('wsq_picks_v1', JSON.stringify(prune(picks))); } catch (e) {}
  }, [picks]);

  const flash = (msg) => {
    setToast(msg);
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(null), 2200);
  };

  // ---- one batched, self-pacing poll powering everything live ----
  // /live returns scores + community + nations + leaderboard in a single request (4x fewer
  // than separate polls). Cadence adapts: ~25s while a match is in play, ~3 min otherwise —
  // near-real-time when it matters, cheap the rest of the time. Embedded results stay as the
  // instant-render + offline fallback; we merge official results over them, never fabricate.
  const API = window.WSQ_API || '';
  useEffect(() => {
    if (!API) return;
    let alive = true, timer = null;
    const pull = async () => {
      let nextDelay = 180000;
      try {
        const r = await fetch(API + '/live');
        if (r.ok) {
          const d = await r.json();
          if (!alive) return;
          if (Array.isArray(d.scores)) {
            setScoresRaw(d.scores);
            const changed = mergeLiveScores(d.scores);
            if (changed) { setScoresRev((v) => v + 1); if (!firstScores.current) flash('Official results updated 🔒'); }
            firstScores.current = false;
          }
          if (d.community) setCommunity(d.community);
          if (d.stats && d.stats.teams) {
            setNations(Object.entries(d.stats.teams).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([code, count]) => ({ code, count })));
          }
          if (Array.isArray(d.leaderboard)) setBoard(d.leaderboard);
          nextDelay = d.live > 0 ? 25000 : 180000; // fast only while a match is live
        }
      } catch (e) { /* offline — keep current state, retry on the slow cadence */ }
      if (alive) timer = setTimeout(pull, nextDelay);
    };
    pull();
    return () => { alive = false; if (timer) clearTimeout(timer); };
  }, []);

  // count this page load once (not per poll)
  useEffect(() => {
    if (!API) return;
    fetch(API + '/hit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ path: location.pathname }) }).catch(() => {});
  }, []);

  // One-time: prefer the worker's official ranking snapshot (source of truth) over the embedded
  // mirror, so a FIFA correction ships via a worker deploy without a JS cache-bust. Bumps
  // scoresRev so the TAG headlines (computed inline) recompute against the fresh ranking.
  useEffect(() => {
    if (!API) return;
    fetch(API + '/rankings').then((r) => (r.ok ? r.json() : null)).then((d) => {
      if (d && d.rankings && typeof d.rankings === 'object' && Object.keys(d.rankings).length) {
        ACTIVE_RANKS = d.rankings;
        setScoresRev((v) => v + 1);
      }
    }).catch(() => {});
  }, []);

  const onSet = useCallback((key, sc) => {
    setPicks((prev) => ({ ...prev, [activeRef.current]: { ...(prev[activeRef.current] || {}), [key]: sc } }));
  }, []);
  const onClear = useCallback((key) => {
    setPicks((prev) => {
      const g = { ...(prev[activeRef.current] || {}) };
      delete g[key];
      const next = { ...prev };
      if (Object.keys(g).length) next[activeRef.current] = g; else delete next[activeRef.current];
      return next;
    });
  }, []);

  // keep a ref so callbacks read the latest active group
  const activeRef = useRef(active);
  useEffect(() => { activeRef.current = active; }, [active]);
  // mirror picks so async callbacks (cross-device restore) can read the latest without clobbering edits
  const picksRef = useRef(picks);
  useEffect(() => { picksRef.current = picks; }, [picks]);
  const lastSubmitHash = useRef(''); // dedup auto-submit: only POST when the bracket actually changed

  // ---- accounts (login / registration; shared identity with Adopt a Team) ----
  const SKEY = 'wc26_session';
  const [me, setMe] = useState(null);
  const [saved, setSaved] = useState([]);
  const [authMsg, setAuthMsg] = useState('');
  const [authEmail, setAuthEmail] = useState('');
  const [authName, setAuthName] = useState('');
  const [community, setCommunity] = useState(null);   // { players, adoptions }
  const [nations, setNations] = useState([]);          // [{ code, count }] top 10
  const [board, setBoard] = useState([]);              // [{ handle, score, exact, played }] top 10 predictors
  const [myEntry, setMyEntry] = useState(null);        // my leaderboard entry { handle, score, rank, total, ... } or null
  const [lbHandle, setLbHandle] = useState('');        // chosen public handle
  const apiFetch = (path, opts = {}) => {
    const s = localStorage.getItem(SKEY);
    const headers = Object.assign({ 'Content-Type': 'application/json' }, opts.headers || {});
    if (s) headers.Authorization = 'Bearer ' + s;
    return fetch(API + path, { ...opts, headers });
  };
  const loadSaved = async () => { try { const r = await apiFetch('/me/scenarios'); if (r.ok) setSaved(await r.json()); } catch (e) {} };
  const loadMyEntry = async () => {
    try {
      const r = await apiFetch('/me/leaderboard');
      if (!r.ok) return;
      const d = await r.json();
      setMyEntry(d.entered ? d : null);
      if (d.entered) {
        if (d.hash) lastSubmitHash.current = d.hash + '|' + (d.handle || ''); // don't auto-resubmit unchanged on load
        if (d.handle) setLbHandle(d.handle);
        // cross-device: if this device has no bracket in progress, restore the one
        // submitted from another device. Never clobber edits already on this device.
        if (d.hash && Object.keys(prune(picksRef.current)).length === 0) {
          const restored = decode(d.hash);
          if (restored) { setPicks(restored); flash('Loaded your submitted bracket from your account 🔄'); }
        }
      }
    } catch (e) {}
  };
  const loadMyBracket = () => {
    if (!myEntry || !myEntry.hash) return;
    const p = decode(myEntry.hash);
    if (p) { setPicks(p); flash('Your submitted bracket loaded ↻'); window.scrollTo({ top: 0, behavior: 'smooth' }); }
  };
  useEffect(() => {
    if (!API) return;
    (async () => {
      const params = new URLSearchParams(location.search);
      const tok = params.get('login');
      if (tok) {
        try {
          const r = await fetch(API + '/auth/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token: tok }) });
          if (r.ok) { const d = await r.json(); if (d && d.session) localStorage.setItem(SKEY, d.session); }
        } catch (e) {}
        params.delete('login');
        history.replaceState(null, '', location.pathname + (params.toString() ? '?' + params.toString() : '') + location.hash);
      }
      const s = localStorage.getItem(SKEY);
      if (!s) return;
      try {
        const r = await fetch(API + '/me', { headers: { Authorization: 'Bearer ' + s } });
        if (r.status === 401) { localStorage.removeItem(SKEY); return; }
        if (r.ok) { setMe(await r.json()); loadSaved(); loadMyEntry(); }
      } catch (e) {}
    })();
  }, []);


  const requestLink = async (e) => {
    e.preventDefault();
    setAuthMsg('Sending…');
    try {
      const r = await fetch(API + '/auth/request', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: authEmail, name: authName, next: '/wc26/who-still-qualifies' }) });
      if (r.ok) setAuthMsg('Check your inbox 📬 — your one-time link works once, expires in 15 min.');
      else if (r.status === 429) setAuthMsg('Too many requests for that email — try again in an hour.');
      else if (r.status === 503) setAuthMsg('Sign-in opens soon — your scenario still shares via the link.');
      else setAuthMsg('Could not send the link — try again later.');
    } catch (e) { setAuthMsg('Could not reach the server — try again later.'); }
  };
  const saveScenario = async () => {
    const hash = encode(prune(picks));
    if (!hash) { flash('Set at least one score first'); return; }
    try {
      const r = await apiFetch('/me/scenarios', { method: 'POST', body: JSON.stringify({ hash, label: Object.keys(prune(picks)).length + ' group(s) · last: ' + activeRef.current }) });
      if (r.ok) { await loadSaved(); flash('Saved to your account ✓'); }
      else if (r.status === 401) { localStorage.removeItem(SKEY); setMe(null); flash('Session expired — sign in again'); }
      else flash('Could not save — try again');
    } catch (e) { flash('Could not reach the server'); }
  };
  const loadScenario = (hash) => { const p = decode(hash); if (p) { setPicks(p); flash('Scenario loaded'); window.scrollTo({ top: 0, behavior: 'smooth' }); } };
  const deleteSaved = async (id) => { try { const r = await apiFetch('/me/scenarios/delete', { method: 'POST', body: JSON.stringify({ id }) }); if (r.ok) setSaved((prev) => prev.filter((x) => x.id !== id)); } catch (e) {} };
  const refreshBoard = async () => { try { const r = await fetch(API + '/leaderboard'); if (r.ok) setBoard(await r.json()); } catch (e) {} };
  // Core submit. Auto-fires (silent) whenever a signed-in user changes their predictions,
  // so predicting === being on the leaderboard. No manual click required.
  const doSubmit = async (silent) => {
    const pr = prune(picks);
    if (!Object.keys(pr).length) { if (!silent) flash('Set at least one matchday-3 score first'); return; }
    const hash = encode(pr);
    const handle = (lbHandle || (me && (me.name || '').split(' ')[0]) || '').trim();
    try {
      const r = await apiFetch('/me/leaderboard', { method: 'POST', body: JSON.stringify({ hash, handle }) });
      if (r.ok) {
        const firstTime = !myEntry;
        lastSubmitHash.current = hash + '|' + handle;
        await loadMyEntry(); await refreshBoard();
        if (firstTime) flash("🏆 You're on the leaderboard!");
        else if (!silent) flash('Leaderboard updated 🏆');
      } else if (r.status === 401) { localStorage.removeItem(SKEY); setMe(null); if (!silent) flash('Session expired — sign in again'); }
      else if (r.status === 400) { if (!silent) flash('Add a valid name and at least one score'); }
      else if (!silent) flash('Could not submit — try again');
    } catch (e) { if (!silent) flash('Could not reach the server'); }
  };
  const submitLeaderboard = () => doSubmit(false);
  // Auto-submit: a signed-in user with predictions is automatically on the board; any change
  // (scores or display name) re-submits after a short debounce. No clicking required.
  useEffect(() => {
    if (!me) return;
    const pr = prune(picks);
    if (!Object.keys(pr).length) return;
    const handle = (lbHandle || (me.name || '').split(' ')[0] || '').trim();
    if (encode(pr) + '|' + handle === lastSubmitHash.current) return;
    const t = setTimeout(() => doSubmit(true), 1800);
    return () => clearTimeout(t);
  }, [picks, lbHandle, me]);
  const leaveLeaderboard = async () => {
    try { const r = await apiFetch('/me/leaderboard/remove', { method: 'POST', body: '{}' }); if (r.ok) { setMyEntry(null); flash('Removed from the leaderboard'); await refreshBoard(); } } catch (e) {}
  };
  const signOut = () => { localStorage.removeItem(SKEY); setMe(null); setSaved([]); setMyEntry(null); };
  const challenge = async () => {
    const enc = encode(prune(picks));
    const url = location.origin + location.pathname + (enc ? '?s=' + enc : '');
    const text = "I just called the World Cup group stage. Beat my bracket 👇";
    // Mobile gets the native share sheet; if the user dismisses it (AbortError) that's
    // intentional, so stop. Any other share failure falls through to copy-to-clipboard.
    if (navigator.share) {
      try { await navigator.share({ title: 'Who Still Qualifies?', text, url }); return; }
      catch (e) { if (e && e.name === 'AbortError') return; }
    }
    if (navigator.clipboard && navigator.clipboard.writeText) {
      try { await navigator.clipboard.writeText(url); flash('Challenge link copied 📣'); return; }
      catch (e) { /* clipboard blocked — fall through to address bar */ }
    }
    history.replaceState(null, '', url); // last resort: it's at least in the address bar to copy
    flash('Challenge link is in the address bar 📣');
  };

  const group = WSQ.groups.find((g) => g.id === active);
  const gpicks = picks[active] || {};

  const share = () => {
    const enc = encode(prune(picks));
    const url = location.origin + location.pathname + (enc ? '?s=' + enc : '');
    history.replaceState(null, '', url);
    if (navigator.clipboard) navigator.clipboard.writeText(url).then(() => flash('Link copied — your scenario is in it'), () => flash('Copy failed — URL is in the address bar'));
    else flash('Scenario saved to the address bar');
  };
  const resetGroup = () => { onClearAll(active); flash('Group ' + active + ' reset'); };
  const onClearAll = (gid) => setPicks((prev) => { const n = { ...prev }; delete n[gid]; return n; });
  const resetAll = () => { setPicks({}); history.replaceState(null, '', location.pathname); flash('All picks reset'); };

  const idx = WSQ.groups.findIndex((g) => g.id === active);
  const go = (d) => setActive(WSQ.groups[(idx + d + 12) % 12].id);

  return (
    <React.Fragment>
      <header className="hero">
        <div className="wrap">
          <div className="eyebrow"><i className="ball" />Group stage · scenario machine · 48 nations</div>
          <h1>Who <em>still</em> <span className="hl">qualifies?</span></h1>
          <p className="lede">12 groups. Top two go through, plus the eight best third&#8209;placed teams. Set the matchday&#8209;three scores yourself and watch every table react&#160;&#8212;&#160;instantly.</p>
          <div className="stat-stickers">
            <div className="stat"><b>48</b><span>Nations</span></div>
            <div className="stat"><b>12</b><span>Groups</span></div>
            <div className="stat"><b>8</b><span>Best thirds</span></div>
          </div>
          <div className="how">
            <span className="how-step"><b>1</b> Pick a group</span>
            <span className="how-step"><b>2</b> Set the open scores</span>
            <span className="how-step"><b>3</b> Read every team's fate</span>
            <span className="how-step"><b>4</b> Share the link</span>
          </div>
          <div className="h2h">
            <b>New for 2026:</b> level on points? <em>Head-to-head</em> decides before overall goal difference. Most fans don't know this yet. Now you do.
          </div>
          <div className="legend">
            {['through', 'inhands', 'needshelp', 'thirdhope', 'out'].map((k) => (
              <span key={k} className={`lg ${FATE[k].cls}`}><i className="dot" />{FATE[k].label}</span>
            ))}
          </div>
        </div>
      </header>

      <TodayScores rows={scoresRaw} />

      {(() => {
        const tod = WSQ.groups.map((g) => groupStory(g, scoresRaw)).filter(Boolean).sort((a, b) => b.weight - a.weight)[0];
        return tod ? (
          <div className="wrap">
            <div className="tagofday"><span className="tod-badge">🗣️ TAG of the Day</span><span className="tod-text">{tod.text}</span><span className="tod-grp">Group {tod.gid}</span></div>
            <p className="tod-gloss"><b>TAG</b> = <b>T</b>alk <b>A</b>bout the <b>G</b>roup — the standout storyline a group is generating, drawn only from real results and seeded by the official FIFA ranking.</p>
          </div>
        ) : null;
      })()}

      {API && (
        <section className="section acct-band">
          <div className="wrap">
            {me ? (
              <div className="acct">
                <div className="acct-hi">👋 Welcome back, <b>{me.name || me.email.split('@')[0]}</b>{me.team ? (' · your team ' + (T[me.team] ? T[me.team].name : me.team)) : ''}</div>
                <div className="acct-row">
                  <button className="cta" onClick={saveScenario}>💾 Save this scenario</button>
                  <button className="ghost" onClick={challenge}>📣 Challenge a friend</button>
                  <button className="ghost" onClick={signOut}>Sign out</button>
                </div>
                {saved.length > 0 && (
                  <div className="acct-saved">
                    {saved.map((s) => (
                      <span className="acct-sc" key={s.id}>
                        <button className="acct-load" onClick={() => loadScenario(s.hash)}>{s.label || 'Saved scenario'}</button>
                        <button className="acct-del" onClick={() => deleteSaved(s.id)} title="Delete">✕</button>
                      </span>
                    ))}
                  </div>
                )}
                <div className={`acct-lb ${myEntry ? 'on' : ''}`}>
                  {myEntry ? (
                    <>
                      <div className="lb-live">🏆 You're LIVE on the leaderboard</div>
                      <p className="lb-status"><b>{myEntry.handle}</b> · <b>{myEntry.score} pts</b> · rank <b>#{myEntry.rank}</b> of {myEntry.total}{myEntry.played === 0 ? ' · scoring starts when matchday-3 results land' : ` · ${myEntry.exact} exact`}</p>
                      <div className="lb-row">
                        <input type="text" className="lb-handle" placeholder="Your leaderboard name" value={lbHandle} onChange={(e) => setLbHandle(e.target.value)} aria-label="Leaderboard display name" maxLength={24} />
                        <span className="lb-auto">auto-saves ✓</span>
                        <button className="ghost small" onClick={loadMyBracket}>↻ My bracket</button>
                        <button className="ghost small" onClick={leaveLeaderboard}>Leave</button>
                      </div>
                      <p className="lb-note">Your latest prediction counts automatically — no need to submit. Your name shows publicly; your email never does.</p>
                    </>
                  ) : (
                    <>
                      <div className="lb-live prompt">🏆 Join the live leaderboard</div>
                      <p className="lb-note">Set scores in any group and you're <b>on automatically</b> — 3 pts for an exact score, 1 for the right result, scored live.</p>
                      <div className="lb-row">
                        <input type="text" className="lb-handle" placeholder="Your leaderboard name (optional)" value={lbHandle} onChange={(e) => setLbHandle(e.target.value)} aria-label="Leaderboard display name" maxLength={24} />
                        <button className="cta big" onClick={submitLeaderboard}>🏆 Join the leaderboard now</button>
                      </div>
                    </>
                  )}
                </div>
              </div>
            ) : (
              <div className="acct">
                <div className="lb-live prompt">🏆 Sign in to join the live leaderboard</div>
                <div className="acct-hi">Save your scenarios — sign in, no password</div>
                <p className="acct-sub">We email a one-time link. Then your predictions count on the leaderboard <b>automatically</b>, and your saved what-ifs + adopted team follow you across both games.</p>
                <form className="acct-row" onSubmit={requestLink}>
                  <input type="text" required placeholder="Your name" value={authName} onChange={(e) => setAuthName(e.target.value)} aria-label="Your name" maxLength={28} />
                  <input type="email" required placeholder="you@example.com" value={authEmail} onChange={(e) => setAuthEmail(e.target.value)} aria-label="Email for sign-in link" />
                  <button className="cta" type="submit">Send link</button>
                </form>
              </div>
            )}
            {authMsg && <p className="acct-msg">{authMsg}</p>}
          </div>
        </section>
      )}

      <section className="section" style={{ paddingTop: '34px' }}>
        <div className="wrap">
          <div className="pitch">
            <div className="pitch-head">
              <div>
                <span className="kicker">⚽ The scenario machine</span>
                <h2 className="sec-title">Run the group stage</h2>
                <p className="sec-sub">Pick a group, set matchday three, and every table — and the best-thirds cut — reacts as you go.</p>
                {API && <span className="live-flag"><i />Official results lock in live as matches finish</span>}
              </div>
            </div>

            <GroupStrip active={active} picks={picks} onPick={setActive} />

            <LockCountdown rows={scoresRaw} onPredict={(g) => { setActive(g); setTimeout(() => { const el = document.querySelector('.matches-panel'); if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, 60); }} />

            <main className="board">
              <section className="panel standings-panel">
                <div className="panel-head">
                  <div className="ph-left">
                    <button className="nav" onClick={() => go(-1)} aria-label="previous group">‹</button>
                    <h2>Group {active}</h2>
                    <button className="nav" onClick={() => go(1)} aria-label="next group">›</button>
                  </div>
                  <button className="ghost" onClick={resetGroup} disabled={!Object.keys(gpicks).length}>Reset group</button>
                </div>
                <Standings group={group} picks={gpicks} rev={scoresRev} />
                <GroupPulse group={group} rows={scoresRaw} rev={scoresRev} />
              </section>

              <section className="panel matches-panel">
                <div className="panel-head"><h3>Set results</h3><span className="ph-hint">tap ▲▼ or a quick result</span></div>
                <Matches group={group} picks={gpicks} rows={scoresRaw} onSet={onSet} onClear={onClear} />
              </section>
            </main>

            <section className="panel thirds-panel">
              <div className="panel-head">
                <div>
                  <h3>Best thirds — live cut</h3>
                  <p className="ph-sub">The eight best third-placed teams advance — by points, then goal difference, then goals scored. Updates as you set scores in any group.</p>
                </div>
              </div>
              <BestThirds allPicks={picks} rev={scoresRev} />
            </section>
          </div>

          <div className="actions" style={{ marginTop: '26px' }}>
            <button className="cta" onClick={share}>Share this scenario →</button>
            <button className="ghost" onClick={resetAll}>Reset everything</button>
          </div>
        </div>
      </section>

      {API && (
        <section className="section community">
          <div className="wrap">
            <div className="comm-head">
              <span className="kicker saffron">🌍 The community</span>
              <h2 className="sec-title">Live, right now</h2>
            </div>
            <div className="comm-counts-row">
              <div className="comm-stat">
                <b>{community ? community.players.toLocaleString() : '—'}</b>
                <span><i className="live-dot" />fans signed in</span>
              </div>
              <div className="comm-stat">
                <b>{community ? community.adoptions.toLocaleString() : '—'}</b>
                <span><i className="live-dot" />teams adopted</span>
              </div>
              <div className="comm-stat">
                <b>{community && community.views != null ? community.views.toLocaleString() : '—'}</b>
                <span><i className="live-dot" />page visits</span>
              </div>
              <a className="comm-cta" href="https://trykarkedekho.com/wc26/adopt-a-team" target="_blank" rel="noopener">No team yet? Adopt one →</a>
            </div>
            <div className="comm-boards">
              <div className="comm-board">
                <h3>🏆 Top adopted nations</h3>
                {nations.length === 0 ? (
                  <p className="comm-empty">Be the first to adopt a nation — the board fills up as fans join.</p>
                ) : (
                  <ol className="comm-list">
                    {nations.map((x, i) => (
                      <li key={x.code} className={i < 3 ? 'top' : ''}>
                        <span className="rk">{i + 1}</span>
                        <Chip code={x.code} size={18} />
                        <span className="nm">{T[x.code] ? T[x.code].name : x.code}</span>
                        <span className="ct">{x.count.toLocaleString()}</span>
                      </li>
                    ))}
                  </ol>
                )}
              </div>
              <div className="comm-board">
                <h3>🏆 Top predictors</h3>
                {board.length === 0 ? (
                  <div className="comm-empty loud">
                    <b>🏆 Be the first predictor!</b>
                    <p>{me ? 'Set your matchday-3 scores — you join automatically.' : 'Sign in, set your matchday-3 scores — you join automatically.'} 3 pts for an exact score, 1 for the right result.</p>
                  </div>
                ) : (
                  <ol className="comm-list">
                    {board.map((x, i) => (
                      <li key={x.handle + i} className={`${i < 3 ? 'top' : ''} ${myEntry && myEntry.handle === x.handle ? 'me' : ''}`}>
                        <span className="rk">{i + 1}</span>
                        <span className="nm">{x.handle}{myEntry && myEntry.handle === x.handle ? ' (you)' : ''}</span>
                        <span className="ct">{x.score} pts</span>
                      </li>
                    ))}
                  </ol>
                )}
                <p className="comm-foot">Scored live from official matchday-3 results.</p>
                <div className="lb-help">
                  <p><b>Why is everyone on 0?</b> You score only when a match you predicted <b>finishes</b> — 3 pts for an exact score, 1 for the right result. Most predicted games are still to come, so points start landing as results do.</p>
                  <p><b>Your name not on the board?</b> You appear only after you set predictions <b>while signed in</b>, and each pick has to be in <b>before that match kicks off</b> to count. Predict early.</p>
                </div>
              </div>
            </div>
            <FeedbackForm me={me} />
          </div>
        </section>
      )}

      <ChantRibbon color="green" />

      <EX.Quotes />
      <EX.Facts />

      <ChantRibbon color="blue" rev />

      <EX.Legends />

      <footer className="foot">
        <div className="wrap">
          <a className="adopt" href="https://trykarkedekho.com/wc26/adopt-a-team" target="_blank" rel="noopener">
            <b>New to football, or no team to call your own?</b> The group stage just kicked off — everyone's still in it. Adopt a nation in 60 seconds, then run their scenarios. <span className="arrow">Adopt a team →</span>
          </a>
          <p className="fine">Built for fun at <a href="https://trykarkedekho.com" target="_blank" rel="noopener">trykarkedekho.com</a> — <em>try kar ke dekho</em>. Unofficial fan project, not affiliated with any governing body. Official results locked 🔒; everything else is your imagination. Ties beyond goals scored fall to fair-play points and rankings — we flag those (†) instead of guessing.</p>
        </div>
      </footer>

      <div className={`toast ${toast ? 'show' : ''}`}>{toast}</div>
    </React.Fragment>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);
