// screens/matchday.jsx — the daily competition
//
// One featured fixture a day generates a short trivia set about the two clubs.
// Everyone plays the same frozen question set, which is what makes scores
// comparable. Opens 24h before kickoff, closes at kickoff, no catch-up.
//
// Question types implemented here:
//   top_scorers    ranked slots, progressive reveal — type a name and it drops
//                  into its correct rank
//   shared_players two kit swatches, name anyone who played for both
//   teammates      one kit plus a named anchor player, name anyone who
//                  overlapped with them at that club
//
// Answer matching is accent- and punctuation-insensitive, and accepts surname
// only, because typing "Hamsik" for "Marek Hamsik" has to work.

(function () {
  const T = () => window.T;

  // ── Answer matching ────────────────────────────────────────────────────────
  function norm(s) {
    return (s || '')
      .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')  // strip accents
      .toLowerCase().replace(/[^a-z0-9 ]/g, '').replace(/\s+/g, ' ').trim();
  }
  // Accept full name, or surname alone when it's unambiguous within the set.
  function matchAnswer(guess, candidates) {
    const g = norm(guess);
    if (!g) return null;
    let hit = candidates.find(c => norm(c.name) === g);
    if (hit) return hit;
    const bySurname = candidates.filter(c => {
      const parts = norm(c.name).split(' ');
      return parts[parts.length - 1] === g;
    });
    if (bySurname.length === 1) return bySurname[0];
    hit = candidates.find(c => norm(c.name).includes(g) && g.length >= 4);
    return hit || null;
  }

  // ── Hint ladders ───────────────────────────────────────────────────────────
  // A hint should shrink the search space, never close it — except `reveal`,
  // which is explicitly "I want to move on".
  //
  // Cost is BOTH tokens and points. That's what keeps the leaderboard honest
  // without locking anyone out: a casual can buy their way through a matchday,
  // and their score truthfully reflects the help. The top of the table is still
  // people who went in clean, so "no hints" stays the real brag.
  //
  // First hint of each matchday is free, so an empty wallet is never a wall.
  // ── Hint model ─────────────────────────────────────────────────────────────
  // Slots show one blank per letter, so you can see the shape of the answer.
  // The first letter is free; after that you buy letters left to right and the
  // price doubles each time. That curve is deliberate — spelling out a whole
  // name should cost more than the slot is worth, so letters are a nudge when
  // you're close, never a way to buy the answer.
  //
  // No reveal option. In a name-guessing game "show me the answer" isn't a
  // hint, it's the give-up button, which already exists.
  const LETTER_BASE = 5;          // tokens for the first purchased letter
  const LETTER_STEP = 2;          // doubles each subsequent letter
  const LETTER_PENALTY = 2;       // points forfeited per letter
  const NATIONALITY_COST = 5;
  const NATIONALITY_PENALTY = 2;

  // Cost of the next letter, given how many have already been bought here.
  function letterCost(boughtSoFar) {
    return LETTER_BASE * Math.pow(LETTER_STEP, boughtSoFar);
  }

  // How many answers a question needs to be complete.
  //
  // Types with a large valid answer set carry `required` in their payload and
  // you name that many of them; ranked types want the whole list. This was
  // hardcoded to shared_players in three places, which meant any new
  // many-answer type silently demanded every valid answer instead of a few.
  function requiredCount(q) {
    return (q?.payload?.required) || (q?.payload?.answers || []).length;
  }

  // Which types hand you the first letter free.
  //
  // Only where the slot has ONE knowable answer. shared_players has many valid
  // answers, so prefilling the first letter of whichever one we happened to
  // rank first is misleading — it points at a specific player when any would
  // do. career_path starts blank because the whole puzzle is the deduction.
  // teammates is the shared_players case: many valid answers, no free letter.
  const FREE_FIRST_LETTER = {
    top_scorers: true,
    top_appearances: true,
    shared_players: false,
    teammates: false,
    career_path: false,
  };

  function revealedLetters(hints, rank, typeKey) {
    const base = FREE_FIRST_LETTER[typeKey] ? 1 : 0;
    return base + (hints.letters?.[rank] || 0);
  }

  // Does the payload carry what this hint would reveal?
  function hintHasData(key, payload) {
    const a = (payload?.answers || [])[0] || {};
    if (key === 'nationality') return a.nationality != null;
    if (key === 'era')         return a.year != null;
    return true;
  }

  // Small flag, same flagcdn source the World Cup app uses.
  function NatFlag({ code, title, size = 18 }) {
    if (!code) return null;
    return (
      <img
        src={`https://flagcdn.com/32x24/${code}.png`}
        srcSet={`https://flagcdn.com/64x48/${code}.png 2x`}
        width={size} height={Math.round(size * 0.75)}
        alt={title || ''} title={title || ''}
        loading="lazy"
        style={{
          display: 'block', borderRadius: 2, flexShrink: 0,
          objectFit: 'cover', boxShadow: '0 1px 3px rgba(0,0,0,0.4)',
        }}
      />
    );
  }

  // ── Kit shirt — the club identity system ───────────────────────────────────
  // No crests (trademarked, not licensable at this scale). Identity is kit
  // colour + pattern, which fans read just as fast. The pattern is clipped
  // inside the shirt silhouette, then the outline and collar stroke on top.
  let _kitId = 0;
  function KitSwatch({ club, size = 56 }) {
    const TT = T();
    const p = club?.primary_hex || '#1F2A40';
    const sec = club?.secondary_hex || '#FFFFFF';
    const pat = club?.pattern || 'solid';
    const cid = React.useMemo(() => `kit${++_kitId}`, []);

    const BODY = "M66.5 14.4375C75.7944 14.4375 81.7119 12.375 81.7119 12.375C82.6469 12.3753 83.5752 12.5322 84.4576 12.8391L124.688 26.8125L120.368 49.5L107.67 50.9231C106.115 51.0978 104.683 51.8464 103.659 53.0204C102.634 54.1943 102.092 55.7078 102.14 57.2602L103.906 119.625H29.0938L30.8602 57.2602C30.9079 55.7078 30.3657 54.1943 29.3411 53.0204C28.3166 51.8464 26.8847 51.0978 25.3297 50.9231L12.6324 49.5L8.3125 26.8125L48.5424 12.8391C49.4248 12.5322 50.3531 12.3753 51.2881 12.375C51.2881 12.375 57.2056 14.4375 66.5 14.4375Z";
    const COLLAR = "M86.5829 13.5764C85.405 17.97 82.7974 21.8541 79.1656 24.6249C75.5338 27.3958 71.0813 28.898 66.5004 28.898C61.9196 28.898 57.4671 27.3958 53.8353 24.6249C50.2035 21.8541 47.5959 17.97 46.418 13.5764";

    const fills = [];
    if (pat === 'stripes') {
      const n = 6, w = 133 / n;
      for (let i = 0; i < n; i++)
        fills.push(<rect key={i} x={i * w} y="0" width={w} height="132"
          fill={i % 2 ? sec : p} />);
    } else if (pat === 'hoops') {
      const n = 6, h = 132 / n;
      for (let i = 0; i < n; i++)
        fills.push(<rect key={i} x="0" y={i * h} width="133" height={h}
          fill={i % 2 ? sec : p} />);
    } else if (pat === 'halves') {
      fills.push(<rect key="a" x="0" y="0" width="66.5" height="132" fill={p} />);
      fills.push(<rect key="b" x="66.5" y="0" width="66.5" height="132" fill={sec} />);
    } else if (pat === 'sash') {
      fills.push(<rect key="bg" x="0" y="0" width="133" height="132" fill={p} />);
      fills.push(<polygon key="s" points="0,132 40,132 133,20 133,0 95,0 0,100"
        fill={sec} />);
    } else {
      fills.push(<rect key="bg" x="0" y="0" width="133" height="132" fill={p} />);
    }

    return (
      <svg width={size} height={size} viewBox="0 0 133 132"
        style={{ flexShrink: 0, display: 'block' }}
        role="img" aria-label={club?.name || 'club kit'}>
        <defs>
          <clipPath id={cid}><path d={BODY} /></clipPath>
        </defs>
        <g clipPath={`url(#${cid})`}>{fills}</g>
        <path d={BODY} fill="none" stroke="rgba(255,255,255,0.35)" strokeWidth="3"
          strokeLinecap="round" strokeLinejoin="round" />
        <path d={COLLAR} fill="none" stroke="rgba(255,255,255,0.35)" strokeWidth="3"
          strokeLinecap="round" strokeLinejoin="round" />
      </svg>
    );
  }

  // One blank per letter, spaces preserved between words, revealed letters
  // filled left to right. Seeing the shape of the name is itself a clue.
  function LetterBlanks({ name, revealed, dim }) {
    const TT = T();
    let seen = 0;
    return (
      <span style={{ display: 'inline-flex', flexWrap: 'wrap', gap: '0 3px',
        alignItems: 'baseline' }}>
        {(name || '').split('').map((ch, i) => {
          if (ch === ' ') {
            return <span key={i} style={{ width: 8, display: 'inline-block' }} />;
          }
          const idx = seen++;
          const show = idx < revealed;
          return (
            <span key={i} style={{
              display: 'inline-block', minWidth: 9, textAlign: 'center',
              fontFamily: 'DM Sans', fontSize: 14,
              fontWeight: show ? 700 : 400,
              color: show ? (dim ? TT.textSec : TT.text) : TT.textMute,
              borderBottom: show ? 'none' : `1px solid ${TT.navy600}`,
              lineHeight: 1.3,
            }}>{show ? ch : '\u00A0'}</span>
          );
        })}
      </span>
    );
  }

  // ── Ranked slots (top scorers / appearances) ───────────────────────────────
  function RankedQuestion({ q, found, revealed, onGuess, hints = {} }) {
    const TT = T();
    const [val, setVal] = React.useState('');
    const [shake, setShake] = React.useState(false);
    const answers = q.payload.answers || [];

    const submit = (e) => {
      e && e.preventDefault();
      if (!val.trim()) return;
      const ok = onGuess(val);
      if (!ok) { setShake(true); setTimeout(() => setShake(false), 400); }
      setVal('');
    };

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
          <KitSwatch club={q.club} size={44} />
          <div style={{
            fontFamily: 'Bakbak One,sans-serif', fontSize: 17,
            color: TT.text, lineHeight: 1.15,
          }}>{q.payload.prompt}</div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 14 }}>
          {answers.map((a) => {
            const got = found.has(a.rank);
            const show = got || revealed;
            const natShown = hints.nationality?.has(a.rank);
            return (
              <div key={a.rank} style={{
                display: 'flex', alignItems: 'center', gap: 10,
                padding: '9px 12px', borderRadius: 10,
                background: got ? 'rgba(45,140,78,0.14)'
                  : revealed ? 'rgba(232,122,122,0.10)' : TT.navy800,
                border: `1px solid ${got ? 'rgba(55,168,95,0.5)'
                  : revealed ? 'rgba(232,122,122,0.35)' : TT.border}`,
                transition: 'background .18s ease',
              }}>
                <div style={{
                  width: 22, height: 22, borderRadius: 6, flexShrink: 0,
                  background: show ? (got ? TT.green : 'rgba(232,122,122,0.25)') : TT.navy700,
                  color: show ? '#fff' : TT.textMute,
                  display: 'grid', placeItems: 'center',
                  fontFamily: 'Bakbak One,sans-serif', fontSize: 12,
                }}>{a.rank}</div>
                <div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 7 }}>
                  {natShown && <NatFlag code={a.flag} title={a.nationality} />}
                  {show ? (
                    <span style={{ fontFamily: 'DM Sans', fontSize: 14, fontWeight: 700,
                      color: got ? TT.text : TT.red }}>{a.player}</span>
                  ) : (
                    <LetterBlanks name={a.player} revealed={revealedLetters(hints, a.rank, q.type_key)} />
                  )}
                </div>
                {show && a.goals != null && (
                  <div style={{
                    fontFamily: 'Bakbak One,sans-serif', fontSize: 13,
                    color: got ? TT.gold400 : 'rgba(232,122,122,0.8)',
                  }}>{a.goals}</div>
                )}
              </div>
            );
          })}
        </div>

        {!revealed && (
          <GuessInput val={val} setVal={setVal} onSubmit={submit} shake={shake}
            placeholder="Type a player name…" />
        )}
      </div>
    );
  }

  // ── Shared players ─────────────────────────────────────────────────────────
  function SharedQuestion({ q, found, revealed, onGuess, hints = {} }) {
    const TT = T();
    const [val, setVal] = React.useState('');
    const [shake, setShake] = React.useState(false);
    const need = q.payload.required || 2;
    const foundList = q.payload.answers.filter(a => found.has(a.rank));
    // the slot hints currently apply to — lowest-ranked unsolved
    const openSlots = q.payload.answers.filter(a => !found.has(a.rank));

    const submit = (e) => {
      e && e.preventDefault();
      if (!val.trim()) return;
      const ok = onGuess(val);
      if (!ok) { setShake(true); setTimeout(() => setShake(false), 400); }
      setVal('');
    };

    return (
      <div>
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          gap: 16, marginBottom: 14,
        }}>
          <KitSwatch club={q.payload.club_a} size={60} />
          <div style={{
            fontFamily: 'Bakbak One,sans-serif', fontSize: 15, color: TT.textMute,
          }}>+</div>
          <KitSwatch club={q.payload.club_b} size={60} />
        </div>

        <div style={{
          textAlign: 'center', fontFamily: 'Bakbak One,sans-serif', fontSize: 17,
          color: TT.text, marginBottom: 4, lineHeight: 1.2,
        }}>{q.payload.prompt}</div>
        <div style={{
          textAlign: 'center', fontSize: 12, color: TT.textSec,
          fontFamily: 'DM Sans', marginBottom: 14,
        }}>
          {foundList.length} of {need} found
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 14 }}>
          {Array.from({ length: need }).map((_, i) => {
            const a = foundList[i];
            // unsolved slots show the shape of the next unfound answer
            const target = !a ? openSlots[i - foundList.length] : null;
            return (
              <div key={i} style={{
                padding: '11px 12px', borderRadius: 10,
                background: a ? 'rgba(45,140,78,0.14)' : TT.navy800,
                border: `1px solid ${a ? 'rgba(55,168,95,0.5)' : TT.border}`,
                display: 'flex', alignItems: 'center', gap: 7,
              }}>
                {target && hints.nationality?.has(target.rank) && (
                  <NatFlag code={target.flag} title={target.nationality} />
                )}
                {a ? (
                  <span style={{ fontFamily: 'DM Sans', fontSize: 14, fontWeight: 700,
                    color: TT.text }}>{a.player}</span>
                ) : target ? (
                  <LetterBlanks name={target.player}
                    revealed={revealedLetters(hints, target.rank, q.type_key)} />
                ) : (
                  <span style={{ color: TT.textMute, letterSpacing: '0.18em' }}>— — — —</span>
                )}
              </div>
            );
          })}
        </div>

        {revealed && (
          <div style={{
            padding: '10px 12px', borderRadius: 10, marginBottom: 12,
            background: 'rgba(232,122,122,0.08)',
            border: '1px solid rgba(232,122,122,0.25)',
          }}>
            <div style={{ fontSize: 10, color: TT.red, fontFamily: 'DM Sans',
              fontWeight: 700, letterSpacing: '0.1em', marginBottom: 6 }}>
              OTHERS WHO QUALIFIED
            </div>
            <div style={{ fontSize: 12, color: TT.textSec, fontFamily: 'DM Sans', lineHeight: 1.6 }}>
              {q.payload.answers.filter(a => !found.has(a.rank))
                .slice(0, 8).map(a => a.player).join(' · ')}
            </div>
          </div>
        )}

        {!revealed && (
          <GuessInput val={val} setVal={setVal} onSubmit={submit} shake={shake}
            placeholder="Name a player…" />
        )}
      </div>
    );
  }

  // ── Career path ────────────────────────────────────────────────────────────
  function CareerPathQuestion({ q, found, revealed, onGuess, hints = {} }) {
    const TT = T();
    const [val, setVal] = React.useState('');
    const [shake, setShake] = React.useState(false);
    const a = (q.payload.answers || [])[0] || {};
    const got = found.has(a.rank);
    const path = q.payload.path || [];

    const submit = (e) => {
      e && e.preventDefault();
      if (!val.trim()) return;
      const ok = onGuess(val);
      if (!ok) { setShake(true); setTimeout(() => setShake(false), 400); }
      setVal('');
    };

    return (
      <div>
        <div style={{
          textAlign: 'center', fontFamily: 'Bakbak One,sans-serif', fontSize: 17,
          color: TT.text, marginBottom: 16, lineHeight: 1.2,
        }}>{q.payload.prompt}</div>

        {/* the path — kits in chronological order */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 0, marginBottom: 16 }}>
          {path.map((c, i) => (
            <div key={i}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
                <KitSwatch club={c} size={38} />
                <div style={{ flex: 1 }}>
                  <div style={{ fontFamily: 'DM Sans', fontSize: 14, fontWeight: 700,
                    color: TT.text }}>{c.name}</div>
                  {(c.start_year || c.end_year) && (
                    <div style={{ fontSize: 11, color: TT.textMute, fontFamily: 'DM Sans' }}>
                      {c.start_year || '?'}{c.end_year ? `–${c.end_year}` : ''}
                    </div>
                  )}
                </div>
              </div>
              {i < path.length - 1 && (
                <div style={{ width: 2, height: 12, background: TT.navy600,
                  marginLeft: 18, marginTop: 2, marginBottom: 2 }} />
              )}
            </div>
          ))}
        </div>

        {/* answer slot */}
        <div style={{
          padding: '12px 14px', borderRadius: 10, marginBottom: 14,
          background: got ? 'rgba(45,140,78,0.14)'
            : revealed ? 'rgba(232,122,122,0.10)' : TT.navy800,
          border: `1px solid ${got ? 'rgba(55,168,95,0.5)'
            : revealed ? 'rgba(232,122,122,0.35)' : TT.border}`,
        }}>
          {(got || revealed) ? (
            <span style={{ fontFamily: 'DM Sans', fontSize: 15, fontWeight: 700,
              color: got ? TT.text : TT.red }}>{a.player}</span>
          ) : (
            <LetterBlanks name={a.player} revealed={revealedLetters(hints, a.rank, q.type_key)} />
          )}
        </div>

        {!revealed && !got && (
          <GuessInput val={val} setVal={setVal} onSubmit={submit} shake={shake}
            placeholder="Name the player…" />
        )}
      </div>
    );
  }

  // ── Shared input ───────────────────────────────────────────────────────────
  function GuessInput({ val, setVal, onSubmit, shake, placeholder }) {
    const TT = T();
    return (
      <div style={{
        display: 'flex', gap: 8,
        animation: shake ? 'mdShake .4s' : 'none',
      }}>
        <input
          value={val}
          onChange={e => setVal(e.target.value)}
          onKeyDown={e => { if (e.key === 'Enter') onSubmit(e); }}
          placeholder={placeholder}
          autoComplete="off" autoCorrect="off" autoCapitalize="words"
          style={{
            flex: 1, padding: '13px 14px', borderRadius: 10,
            background: TT.navy900, border: `1px solid ${shake ? TT.red : TT.border}`,
            color: TT.text, fontFamily: 'DM Sans', fontSize: 15, outline: 'none',
          }}
        />
        <button onClick={onSubmit} style={{
          padding: '0 20px', borderRadius: 10, border: 'none', cursor: 'pointer',
          background: TT.gold400, color: '#1A1200',
          fontFamily: 'Bakbak One,sans-serif', fontSize: 14, letterSpacing: '0.04em',
        }}>ADD</button>
      </div>
    );
  }

  // ── Hint bar ───────────────────────────────────────────────────────────────
  function HintBar({ payload, typeKey, balance, freeLeft, hints, maxPossible,
                     everUsed, onLetter, onNationality, disabled }) {
    const TT = T();
    const answers = payload?.answers || [];
    const bought = Object.values(hints.letters || {}).reduce((a, b) => a + b, 0);
    const cost = freeLeft > 0 ? 0 : letterCost(bought);
    const natAvailable = hintHasData('nationality', payload);
    const natCost = freeLeft > 0 ? 0 : NATIONALITY_COST;

    // target slot = lowest-ranked unsolved
    const target = answers.find(a => !hints._solved?.has(a.rank));
    const lettersLeft = target
      ? target.player.replace(/ /g, '').length - revealedLetters(hints, target.rank, typeKey)
      : 0;

    const canLetter = !disabled && lettersLeft > 0 &&
      (freeLeft > 0 || (balance ?? 0) >= cost);
    const canNat = !disabled && natAvailable &&
      !(hints.nationality?.has(target?.rank)) &&
      (freeLeft > 0 || (balance ?? 0) >= natCost);

    return (
      <div style={{ marginTop: 14 }}>
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          marginBottom: 7,
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            {everUsed
              ? <window.HexIcon size={11} />
              : <span style={{ fontSize: 12, lineHeight: 1 }}>💡</span>}
            <span style={{ fontSize: 10, letterSpacing: '0.12em', fontWeight: 700,
              color: everUsed ? TT.gold400 : TT.textMute, fontFamily: 'DM Sans' }}>
              HINTS
            </span>
          </div>
          <div style={{ fontSize: 10, color: TT.textMute, fontFamily: 'DM Sans' }}>
            {freeLeft > 0 ? 'First one free · ' : ''}Max possible {maxPossible}
          </div>
        </div>

        <div style={{ display: 'flex', gap: 6 }}>
          <button disabled={!canLetter} onClick={onLetter} style={{
            flex: 1, padding: '9px 6px', borderRadius: 9,
            background: canLetter ? TT.navy800 : TT.navy900,
            border: `1px solid ${canLetter ? 'rgba(245,200,66,0.35)' : TT.border}`,
            cursor: canLetter ? 'pointer' : 'default', opacity: canLetter ? 1 : 0.4,
          }}>
            <div style={{ fontSize: 11, fontFamily: 'DM Sans', fontWeight: 700,
              color: canLetter ? TT.text : TT.textMute }}>
              Buy a letter
            </div>
            <div style={{ fontSize: 9, fontFamily: 'DM Sans', marginTop: 3,
              display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3,
              color: canLetter ? TT.gold400 : TT.textMute }}>
              {lettersLeft === 0 ? 'None left'
                : cost === 0 ? 'FREE'
                : <><window.HexIcon size={9} />{cost}</>}
            </div>
          </button>

          {natAvailable && (
            <button disabled={!canNat} onClick={onNationality} style={{
              flex: 1, padding: '9px 6px', borderRadius: 9,
              background: canNat ? TT.navy800 : TT.navy900,
              border: `1px solid ${canNat ? 'rgba(245,200,66,0.35)' : TT.border}`,
              cursor: canNat ? 'pointer' : 'default', opacity: canNat ? 1 : 0.4,
            }}>
              <div style={{ fontSize: 11, fontFamily: 'DM Sans', fontWeight: 700,
                color: canNat ? TT.text : TT.textMute }}>Nationality</div>
              <div style={{ fontSize: 9, fontFamily: 'DM Sans', marginTop: 3,
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 3,
                color: canNat ? TT.gold400 : TT.textMute }}>
                {natCost === 0 ? 'FREE' : <><window.HexIcon size={9} />{natCost}</>}
              </div>
            </button>
          )}
        </div>
      </div>
    );
  }

  // ── Index — the list of matchdays ──────────────────────────────────────────
  // Shown when /matchday has no id or fixture param.

  // Keys must match md_fixtures.competition EXACTLY. Verified against
  // `select distinct competition from md_fixtures`: BUN, EFL Cup, L1, LALIGA,
  // PL, SERIEA, UCL, UNL. Anything missing falls back to the raw value, which
  // is why 'EFL Cup' needs no entry — it already reads correctly.
  const COMP_LABEL = { PL: 'Premier League', UCL: 'Champions League',
    UEL: 'Europa League', LALIGA: 'La Liga', SERIEA: 'Serie A',
    BUN: 'Bundesliga', L1: 'Ligue 1', UNL: 'Nations League' };

  function relTime(ts) {
    const ms = new Date(ts) - Date.now();
    const abs = Math.abs(ms);
    const mins = Math.round(abs / 60000);
    const hrs  = Math.round(abs / 3600000);
    const days = Math.round(abs / 86400000);
    const v = days >= 1 ? `${days}d` : hrs >= 1 ? `${hrs}h` : `${mins}m`;
    return { v, past: ms < 0 };
  }

  function fmtDate(d) {
    return new Date(d + 'T12:00:00').toLocaleDateString(undefined,
      { weekday: 'short', day: 'numeric', month: 'short' });
  }

  // Kickoffs are stored UTC; show them in the reader's own timezone. Passing
  // undefined as the locale lets the browser use the device setting, so a
  // 19:00 UTC kickoff reads 1pm in Denver and 8pm in London.
  function fmtKickoffLocal(ts) {
    if (!ts) return '';
    return new Date(ts).toLocaleTimeString(undefined,
      { hour: 'numeric', minute: '2-digit' });
  }

  function MatchdayCard({ md, score, onPlay }) {
    const TT = T();
    const fx = md.fixture || {};
    const now = Date.now();
    const opens = md.opens_at ? new Date(md.opens_at).getTime() : null;
    const closes = md.closes_at ? new Date(md.closes_at).getTime() : null;

    const played = !!score;
    const live = !played && (!opens || now >= opens) && (!closes || now < closes);
    const upcoming = !played && opens && now < opens;
    const closed = !played && closes && now >= closes;

    let statusText, statusColor;
    if (played)        { statusText = `Played · ${score.points} pts`; statusColor = TT.green; }
    else if (live)     { statusText = closes ? `Closes in ${relTime(closes).v}` : 'Open now';
                         statusColor = TT.gold400; }
    else if (upcoming) { statusText = `Opens in ${relTime(opens).v}`; statusColor = TT.textSec; }
    else               { statusText = 'Closed'; statusColor = TT.textMute; }

    const tappable = live || played;

    return (
      <div
        onClick={() => tappable && onPlay(md)}
        style={{
          borderRadius: 16, marginBottom: 12, overflow: 'hidden',
          background: TT.navy900,
          border: `1px solid ${live ? 'rgba(245,200,66,0.4)' : TT.border}`,
          cursor: tappable ? 'pointer' : 'default',
          opacity: closed ? 0.55 : 1,
        }}>
        {/* meta strip */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '9px 14px', background: TT.navy800,
          borderBottom: `1px solid ${TT.border}`,
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8,
            fontSize: 10, fontFamily: 'DM Sans', fontWeight: 700,
            letterSpacing: '0.08em', color: TT.textSec, textTransform: 'uppercase' }}>
            <span>{COMP_LABEL[fx.competition] || fx.competition}</span>
            {fx.matchweek && <><span style={{ color: TT.navy600 }}>·</span>
              <span>MW {fx.matchweek}</span></>}
          </div>
          <div style={{ fontSize: 10, fontFamily: 'DM Sans', color: TT.textMute }}>
            {fmtDate(md.play_date)}
            {fx.kickoff && <span style={{ color: TT.navy600 }}> · </span>}
            {fx.kickoff && fmtKickoffLocal(fx.kickoff)}
          </div>
        </div>

        {/* teams — kits at the edges, names filling the middle. Type scales to
            the longer of the two names so "Real Betis v Real Sociedad" and
            "Borussia Monchengladbach v Eintracht Frankfurt" both fill the space
            without one wrapping. Sized off the longer name, not each
            separately, so the two stay visually matched. */}
        <div style={{ display: 'flex', alignItems: 'center',
          padding: '16px 14px', gap: 12 }}>
          <KitSwatch club={fx.home} size={44} />
          <div style={{ flex: 1, textAlign: 'center', minWidth: 0 }}>
            <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 15,
              color: TT.text, lineHeight: 1.15, letterSpacing: '0.01em' }}>
              {fx.home?.name}
            </div>
            <div style={{ fontFamily: 'DM Sans', fontSize: 10, color: TT.textMute,
              margin: '2px 0' }}>v</div>
            <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 15,
              color: TT.text, lineHeight: 1.15, letterSpacing: '0.01em' }}>
              {fx.away?.name}
            </div>
          </div>
          <KitSwatch club={fx.away} size={44} />
        </div>

        {/* status */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '10px 14px', borderTop: `1px solid ${TT.border}`,
        }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            {live && <span style={{ width: 6, height: 6, borderRadius: '50%',
              background: TT.gold400, display: 'inline-block' }} />}
            <span style={{ fontSize: 11, fontFamily: 'DM Sans', fontWeight: 700,
              color: statusColor }}>{statusText}</span>
          </div>
          {tappable && (
            <span style={{ fontSize: 11, fontFamily: 'Bakbak One,sans-serif',
              letterSpacing: '0.05em', color: played ? TT.textSec : TT.gold400 }}>
              {played ? 'REVIEW ›' : 'PLAY ›'}
            </span>
          )}
        </div>
      </div>
    );
  }

  function ordinal(n) {
    const s = ['th', 'st', 'nd', 'rd'], v = n % 100;
    return n + (s[(v - 20) % 10] || s[v] || s[0]);
  }

  function RankTile({ label, value }) {
    const TT = T();
    return (
      <div style={{ flex: 1, minWidth: 0, background: TT.navy900,
        borderRadius: 10, padding: '10px 12px' }}>
        <div style={{ fontFamily: 'DM Sans', fontSize: 11, color: TT.textMute,
          marginBottom: 2, overflow: 'hidden', textOverflow: 'ellipsis',
          whiteSpace: 'nowrap' }}>{label}</div>
        <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 18,
          color: TT.text, lineHeight: 1.1 }}>{value}</div>
      </div>
    );
  }

  // ── Hero: today's matchday ─────────────────────────────────────────────────
  // The landing screen used to render a flat list and let you pick. There is
  // nothing to pick — there is one matchday and it closes at kickoff — so the
  // live fixture takes the whole top of the screen and the countdown is the
  // loudest thing on it. That 24h window is what turns "later" into "now", and
  // a row in a list of closed fixtures communicated none of it.
  function HeroMatchday({ md, score, onPlay }) {
    const TT = T();
    const fx = md.fixture || {};
    const closes = md.closes_at ? new Date(md.closes_at).getTime() : null;
    const played = !!score;

    return (
      <div style={{
        background: TT.navy900, borderRadius: 16, padding: 16, marginBottom: 10,
        border: `1px solid ${played ? TT.border : 'rgba(245,200,66,0.55)'}`,
      }}>
        <div style={{ display: 'flex', alignItems: 'center',
          justifyContent: 'space-between', marginBottom: 14 }}>
          <span style={{ fontSize: 10, fontFamily: 'DM Sans', fontWeight: 700,
            letterSpacing: '0.1em', color: TT.gold400, textTransform: 'uppercase' }}>
            {COMP_LABEL[fx.competition] || fx.competition}
            {fx.matchweek ? ` · MW ${fx.matchweek}` : ''}
          </span>
          {closes && !played && (
            <span style={{ fontSize: 11, fontFamily: 'DM Sans', fontWeight: 700,
              color: TT.red }}>
              CLOSES IN {relTime(closes).v}
            </span>
          )}
          {played && (
            <span style={{ fontSize: 11, fontFamily: 'DM Sans', fontWeight: 700,
              color: TT.green }}>{score.points} PTS</span>
          )}
        </div>

        <div style={{ display: 'flex', alignItems: 'center',
          justifyContent: 'center', gap: 16, marginBottom: 14 }}>
          <KitSwatch club={fx.home} size={58} />
          <span style={{ fontFamily: 'DM Sans', fontSize: 11, color: TT.textMute }}>v</span>
          <KitSwatch club={fx.away} size={58} />
        </div>

        <div style={{ textAlign: 'center', fontFamily: 'Bakbak One,sans-serif',
          fontSize: 19, color: TT.text, lineHeight: 1.2, marginBottom: 4 }}>
          {fx.home?.name} v {fx.away?.name}
        </div>
        <div style={{ textAlign: 'center', fontFamily: 'DM Sans', fontSize: 11.5,
          color: TT.textSec, marginBottom: 14 }}>
          {md.kind === 'marquee' ? 'Marquee' : 'Standard'}
          {fx.kickoff ? ` · Kick-off ${fmtKickoffLocal(fx.kickoff)}` : ''}
        </div>

        <div onClick={() => onPlay(md)} style={{
          background: played ? 'transparent' : TT.gold400,
          border: played ? `1px solid ${TT.border}` : 'none',
          borderRadius: 10, padding: '13px 0', textAlign: 'center', cursor: 'pointer',
          fontFamily: 'Bakbak One,sans-serif', fontSize: 15, letterSpacing: '0.03em',
          color: played ? TT.textSec : TT.navy950,
        }}>
          {played ? 'REVIEW YOUR ANSWERS' : "PLAY TODAY'S MATCHDAY"}
        </div>
      </div>
    );
  }

  // Compact result row. The old list showed every matchday including ones you
  // never played; this shows YOUR history, with misses visible, because that is
  // what makes a streak legible and gives a reason not to break it.
  function ResultRow({ md, score, onPlay }) {
    const TT = T();
    const fx = md.fixture || {};
    const missed = !score;
    return (
      <div onClick={() => !missed && onPlay(md)} style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '11px 2px', borderBottom: `1px solid ${TT.border}`,
        cursor: missed ? 'default' : 'pointer',
      }}>
        <span style={{ fontFamily: 'DM Sans', fontSize: 13, minWidth: 0,
          overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
          color: missed ? TT.textMute : TT.textSec }}>
          {fx.home?.name} v {fx.away?.name}
        </span>
        <span style={{ fontFamily: 'DM Sans', fontSize: 13, fontWeight: 700,
          marginLeft: 10, flexShrink: 0,
          color: missed ? TT.textMute : TT.gold400 }}>
          {missed ? 'missed' : score.points}
        </span>
      </div>
    );
  }

  // Current streak — consecutive matchdays PLAYED counting back from the most
  // recent one available. window.mdStreak returns the longest ever, which is an
  // achievement stat; the header wants the run you would break today.
  function currentStreak(playedDates, availableDates) {
    let run = 0;
    for (let i = availableDates.length - 1; i >= 0; i--) {
      if (playedDates.has(availableDates[i])) run += 1;
      else break;
    }
    return run;
  }

  function MatchdayIndex({ anonId, onOpen, onExit }) {
    const TT = T();
    const [list, setList] = React.useState(null);
    const [scores, setScores] = React.useState({});
    const [club, setClub] = React.useState(null);
    const [tokens, setTokens] = React.useState(null);
    const [showPicker, setShowPicker] = React.useState(false);
    const [showBoard, setShowBoard] = React.useState(false);
    const [boards, setBoards] = React.useState({ players: [], fanbases: [] });
    const [streak, setStreak] = React.useState(0);
    const [err, setErr] = React.useState(null);

    React.useEffect(() => {
      let dead = false;
      (async () => {
        try {
          const [mds, mine, board, fanboard, dates, allMine] = await Promise.all([
            window.fetchMatchdays(12),
            window.fetchMyMatchdayScores(anonId),
            window.fetchLeaderboard('week', 200).catch(() => []),
            window.fetchFanbaseLeaderboard('week', 3).catch(() => []),
            window.fetchAvailableDates().catch(() => []),
            window.fetchAllMyMatchdayScores(anonId).catch(() => []),
          ]);
          if (dead) return;
          const map = {};
          (mine || []).forEach(s => { map[s.matchday_id] = s; });
          setScores(map);
          setList(mds || []);
          setBoards({ players: board || [], fanbases: fanboard || [] });
          const playedDates = new Set((allMine || [])
            .map(s => s.matchday?.play_date).filter(Boolean));
          setStreak(currentStreak(playedDates, dates || []));
        } catch (e) { if (!dead) setErr(e.message); }
      })();
      window.fetchMyClub(anonId).then(c => { if (!dead) setClub(c); }).catch(() => {});
      window.fetchTokens?.().then(t => { if (!dead) setTokens(t?.balance ?? null); });
      return () => { dead = true; };
    }, [anonId]);

    if (err) return <Shell onExit={onExit}><Msg text={err} /></Shell>;
    if (!list) return <Shell onExit={onExit}><Msg text="Loading…" /></Shell>;

    const now = Date.now();
    const live = list.filter(m => {
      const o = m.opens_at ? new Date(m.opens_at).getTime() : 0;
      const c = m.closes_at ? new Date(m.closes_at).getTime() : Infinity;
      return now >= o && now < c;
    });
    const later = list.filter(m => m.opens_at && new Date(m.opens_at).getTime() > now);
    const past  = list.filter(m => m.closes_at && new Date(m.closes_at).getTime() <= now);

    // Matching on anon_id only, same as the social board. A signed-in identity
    // shows no tile rather than a wrong one — an incorrect rank is worse than
    // an absent one on the screen that is meant to build trust in the numbers.
    const myRank = boards.players.find(r => r.anon_id && anonId
      && r.anon_id === anonId)?.rank || null;
    const fanRank = club && boards.fanbases
      .find(r => r.club_slug === club.slug)?.rank || null;

    const Section = ({ label, items }) => !items.length ? null : (
      <>
        <div style={{ fontSize: 10, letterSpacing: '0.14em', fontWeight: 700,
          color: TT.textMute, fontFamily: 'DM Sans', margin: '4px 2px 10px',
          textTransform: 'uppercase' }}>{label}</div>
        {items.map(m => (
          <MatchdayCard key={m.id} md={m} score={scores[m.id]} onPlay={onOpen} />
        ))}
      </>
    );

    const total = Object.values(scores).reduce((a, s) => a + (s.points || 0), 0);

    return (
      <Shell onExit={onExit}>
        {/* identity + balance */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          gap: 10, padding: '2px 0 14px',
        }}>
          <window.ClubBadge club={club} onClick={() => setShowPicker(true)} />
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            {streak > 1 && (
              <div style={{ display: 'inline-flex', alignItems: 'center', gap: 5,
                padding: '5px 10px', borderRadius: 999, background: TT.navy800,
                border: `1px solid ${TT.border}`, color: TT.gold400,
                fontFamily: 'DM Sans', fontSize: 11.5, fontWeight: 600 }}>
                🔥 {streak}
              </div>
            )}
            {Object.keys(scores).length > 0 && (
              <div style={{ textAlign: 'right' }}>
                <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 15,
                  color: TT.text, lineHeight: 1 }}>{total}</div>
                <div style={{ fontSize: 8.5, color: TT.textMute,
                  fontFamily: 'DM Sans', letterSpacing: '0.06em' }}>PTS</div>
              </div>
            )}
            {tokens != null && (
              <div style={{ display: 'inline-flex', alignItems: 'center', gap: 5,
                padding: '5px 10px', borderRadius: 999,
                background: 'rgba(232,184,32,0.10)', border: `1px solid ${TT.gold600}`,
                color: TT.gold400, fontFamily: 'DM Sans', fontSize: 11.5, fontWeight: 600 }}>
                <window.HexIcon size={11} />{tokens}
              </div>
            )}
          </div>
        </div>

        {!list.length && (
          <Msg text="No matchdays scheduled yet — check back on matchday." />
        )}

        {/* One fixture, front and centre. If two are somehow open, the earlier
            closing one leads — it is the one about to be lost. */}
        {live.slice(0, 1).map(m => (
          <HeroMatchday key={m.id} md={m} score={scores[m.id]} onPlay={onOpen} />
        ))}
        {live.length > 1 && <Section label="Also open" items={live.slice(1)} />}

        {/* Two numbers, not a widget. On an empty board these read as a new
            season rather than a dead app, which a full-width "nobody has
            played" panel does not. */}
        {(myRank || fanRank || !live.length) && (
          <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
            <RankTile label="Your rank this week" value={myRank ? ordinal(myRank) : '—'} />
            <RankTile label={club?.name || 'Pick a club'}
              value={fanRank ? ordinal(fanRank) : '—'} />
          </div>
        )}

        <div style={{ display: 'flex', alignItems: 'center',
          justifyContent: 'space-between', marginBottom: 10 }}>
          <div style={{ fontSize: 10, letterSpacing: '0.14em', fontWeight: 700,
            color: TT.textMute, fontFamily: 'DM Sans', textTransform: 'uppercase' }}>
            Standings
          </div>
          <button onClick={() => setShowBoard(v => !v)} style={{
            background: 'transparent', border: 'none', cursor: 'pointer',
            color: TT.gold400, fontFamily: 'DM Sans', fontSize: 11, fontWeight: 700 }}>
            {showBoard ? 'Show less' : 'See all ›'}
          </button>
        </div>
        {showBoard && (
          <div style={{ marginBottom: 16 }}>
            <window.MatchdayLeaderboard anonId={anonId} compact={false} />
          </div>
        )}

        {/* Tomorrow, deliberately locked. Anticipation without implying today
            is skippable — clubs shown, no play affordance. */}
        {later.slice(0, 1).map(m => (
          <div key={m.id}>
            <div style={{ fontSize: 10, letterSpacing: '0.14em', fontWeight: 700,
              color: TT.textMute, fontFamily: 'DM Sans', margin: '4px 2px 8px',
              textTransform: 'uppercase' }}>Up next</div>
            <div style={{ background: TT.navy900, border: `1px solid ${TT.border}`,
              borderRadius: 10, padding: '11px 12px', marginBottom: 16,
              display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
              <div style={{ minWidth: 0 }}>
                <div style={{ fontFamily: 'DM Sans', fontSize: 13, color: TT.text,
                  overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {m.fixture?.home?.name} v {m.fixture?.away?.name}
                </div>
                <div style={{ fontFamily: 'DM Sans', fontSize: 11, color: TT.textMute,
                  marginTop: 1 }}>
                  Opens {m.opens_at ? `in ${relTime(new Date(m.opens_at).getTime()).v}` : 'soon'}
                </div>
              </div>
              <span style={{ fontSize: 13, color: TT.textMute, flexShrink: 0,
                marginLeft: 10 }}>🔒</span>
            </div>
          </div>
        ))}

        {!!past.length && (
          <>
            <div style={{ fontSize: 10, letterSpacing: '0.14em', fontWeight: 700,
              color: TT.textMute, fontFamily: 'DM Sans', margin: '4px 2px 4px',
              textTransform: 'uppercase' }}>Your recent results</div>
            <div style={{ borderTop: `1px solid ${TT.border}` }}>
              {past.slice(-6).reverse().map(m => (
                <ResultRow key={m.id} md={m} score={scores[m.id]} onPlay={onOpen} />
              ))}
            </div>
          </>
        )}
        <div style={{ height: 30 }} />

        {showPicker && (
          <window.ClubPicker anonId={anonId} current={club}
            onPicked={setClub} onClose={() => setShowPicker(false)} />
        )}
      </Shell>
    );
  }

  // ── Main screen ────────────────────────────────────────────────────────────
  function MatchdayScreen({ matchdayId, fixtureId, anonId, onExit }) {
    const TT = T();
    // No target given -> show the list rather than guessing at "today", which
    // is empty on any day without a fixture.
    const [openId, setOpenId] = React.useState(matchdayId);
    if (!openId && !fixtureId) {
      return <MatchdayIndex anonId={anonId} onExit={onExit}
        onOpen={(md) => setOpenId(md.id)} />;
    }
    return <MatchdayPlay matchdayId={openId} fixtureId={fixtureId}
      anonId={anonId} onExit={() => setOpenId(null)} />;
  }

  function MatchdayPlay({ matchdayId, fixtureId, anonId, onExit }) {
    const TT = T();
    const [md, setMd] = React.useState(null);
    const [questions, setQuestions] = React.useState(null);
    const [idx, setIdx] = React.useState(0);
    const [found, setFound] = React.useState({});      // qid -> Set(rank)
    const [revealed, setRevealed] = React.useState({}); // qid -> bool
    const [done, setDone] = React.useState(false);
    const [prior, setPrior] = React.useState(null);
    const [err, setErr] = React.useState(null);
    const [hints, setHints] = React.useState({});   // qid -> {initial:Set, era:Set, reveal:Set}
    const [penalty, setPenalty] = React.useState({});   // qid -> points forfeited
    const [everUsedHint, setEverUsedHint] = React.useState(false);
    const [freeLeft, setFreeLeft] = React.useState(1);
    const [balance, setBalance] = React.useState(null);
    const [club, setClub] = React.useState(null);
    const [name, setName] = React.useState(
      () => (window.getSavedName && window.getSavedName()) || '');
    const [askName, setAskName] = React.useState(false);
    const startRef = React.useRef(Date.now());

    React.useEffect(() => {
      let dead = false;
      (async () => {
        try {
          const m = await window.fetchMatchday(matchdayId, fixtureId);
          if (!m) { if (!dead) setErr('No matchday scheduled'); return; }
          const [qs, mine] = await Promise.all([
            window.fetchMatchdayQuestions(m.id),
            window.fetchMyMatchdayScore(m.id, anonId),
          ]);
          if (dead) return;
          setMd(m); setQuestions(qs); setPrior(mine);
          window.fetchTokens?.().then(t => { if (!dead) setBalance(t?.balance ?? 0); });
          window.fetchMyClub?.(anonId).then(c => { if (!dead) setClub(c); }).catch(() => {});
          if (mine) setDone(true);
          startRef.current = Date.now();
        } catch (e) { if (!dead) setErr(e.message); }
      })();
      return () => { dead = true; };
    }, [matchdayId, fixtureId, anonId]);

    const q = questions?.[idx];

    const guess = (text) => {
      const cands = q.payload.answers.filter(a => !(found[q.id] || new Set()).has(a.rank));
      const hit = matchAnswer(text, cands.map(a => ({ name: a.player, rank: a.rank })));
      if (!hit) return false;
      setFound(f => {
        const s = new Set(f[q.id] || []);
        s.add(hit.rank);
        return { ...f, [q.id]: s };
      });
      return true;
    };

    const reveal = () => setRevealed(r => ({ ...r, [q.id]: true }));

    // Hints apply to the lowest-ranked slot the player hasn't solved.
    const targetRank = () => {
      const got = found[q.id] || new Set();
      const a = (q.payload.answers || []).find(x => !got.has(x.rank));
      return a ? a.rank : null;
    };

    const charge = async (tokens, points) => {
      if (freeLeft > 0) { setFreeLeft(0); }
      else {
        try { await window.spendTokens(tokens); }
        catch (e) {
          // Tokens are tied to an account, so signed-out players can't buy any.
          // Say that plainly rather than "not enough tokens", which reads as a
          // balance problem they can't do anything about.
          alert(String(e?.message || '').includes('Not authenticated')
            ? 'Sign in to use more hints — your first one each matchday is free.'
            : 'Not enough tokens');
          return false;
        }
        setBalance(b => (b ?? 0) - tokens);
        setPenalty(pn => ({ ...pn, [q.id]: (pn[q.id] || 0) + points }));
      }
      setEverUsedHint(true);
      return true;
    };

    const buyLetter = async () => {
      const rank = targetRank();
      if (rank == null) return;
      const hq = hints[q.id] || {};
      const bought = Object.values(hq.letters || {}).reduce((a, b) => a + b, 0);
      if (!(await charge(letterCost(bought), LETTER_PENALTY))) return;
      setHints(hs => {
        const cur = hs[q.id] || {};
        const letters = { ...(cur.letters || {}) };
        letters[rank] = (letters[rank] || 0) + 1;
        return { ...hs, [q.id]: { ...cur, letters } };
      });
    };

    // For multiple choice, the useful hint is removing a wrong option, not
    // spelling out a name that's already visible.
    const buyEliminate = async () => {
      const a = (q.payload.answers || [])[0];
      const opts = q.payload.options || [];
      const hq = hints[q.id] || {};
      const gone = hq.eliminated || new Set();
      const candidates = opts.filter(o => o !== a.player && !gone.has(o));
      if (candidates.length <= 1) return;   // never leave fewer than two
      if (!(await charge(NATIONALITY_COST, NATIONALITY_PENALTY))) return;
      const drop = candidates[Math.floor(Math.random() * candidates.length)];
      setHints(hs => {
        const cur = hs[q.id] || {};
        const e = new Set(cur.eliminated || []); e.add(drop);
        return { ...hs, [q.id]: { ...cur, eliminated: e } };
      });
    };

    const buyNationality = async () => {
      const rank = targetRank();
      if (rank == null) return;
      if (!(await charge(NATIONALITY_COST, NATIONALITY_PENALTY))) return;
      setHints(hs => {
        const cur = hs[q.id] || {};
        const nat = new Set(cur.nationality || []); nat.add(rank);
        return { ...hs, [q.id]: { ...cur, nationality: nat } };
      });
    };

    const next = async () => {
      if (idx < questions.length - 1) { setIdx(idx + 1); return; }
      // Ask for a name before the first submit. A leaderboard of "Anonymous"
      // rows is worse than no leaderboard — nobody can find themselves on it.
      if (!name.trim()) { setAskName(true); return; }
      await finish();
    };

    const finish = async () => {
      // Snapshot what the achievement checks need. Derived here rather than by
      // joining back later, because matchdays get rebuilt when fixtures move.
      const hintsUsed = Object.values(hints).reduce((n, h) =>
        n + Object.values(h.letters || {}).reduce((a, b) => a + b, 0)
          + (h.nationality?.size || 0) + (h.eliminated?.size || 0), 0);
      const solvedTypes = questions.filter(qq => {
        const need = requiredCount(qq);
        return (found[qq.id] || new Set()).size >= need;
      }).map(qq => qq.type_key);
      const clubIds = [md.fixture?.home?.id, md.fixture?.away?.id].filter(Boolean);
      // finish
      const totals = questions.reduce((acc, qq) => {
        const need = requiredCount(qq);
        const got = Math.min((found[qq.id] || new Set()).size, need);
        acc.correct += got; acc.total += need;
        acc.points += Math.max(0,
          Math.round((got / need) * qq.max_points) - (penalty[qq.id] || 0));
        return acc;
      }, { correct: 0, total: 0, points: 0 });

      try {
        await window.submitMatchdayScore({
          matchdayId: md.id,
          points: totals.points,
          correctCount: totals.correct,
          totalCount: totals.total,
          durationMs: Date.now() - startRef.current,
          displayName: name.trim() || 'Anonymous',
          anonId,
          clubId: club?.id || null,
          hintsUsed,
          competition: md.fixture?.competition || null,
          featuredClubIds: clubIds,
          solvedTypes,
        });
        // Award anything newly earned. Fire and forget — the score is saved,
        // and a failed check just means the badge lands next time.
        window.checkMatchdayAchievements?.(anonId, club?.id || null);
      } catch (e) { console.warn('score save failed', e.message); }
      setPrior({ points: totals.points, correct_count: totals.correct,
                 total_count: totals.total, duration_ms: Date.now() - startRef.current });
      setDone(true);
    };

    if (err) return <Shell onExit={onExit}><Msg text={err} /></Shell>;
    if (!md || !questions) return <Shell onExit={onExit}><Msg text="Loading…" /></Shell>;
    if (!questions.length) return <Shell onExit={onExit}>
      <Msg text="This matchday has no questions yet." /></Shell>;

    const fx = md.fixture;

    if (done) {
      const pct = prior?.total_count ? Math.round(prior.correct_count / prior.total_count * 100) : 0;
      return (
        <Shell onExit={onExit}>
          <div style={{ textAlign: 'center', padding: '28px 0 20px' }}>
            <div style={{ fontSize: 11, letterSpacing: '0.16em', color: TT.gold400,
              fontFamily: 'DM Sans', fontWeight: 700, marginBottom: 10 }}>MATCHDAY COMPLETE</div>
            <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 52,
              color: TT.text, lineHeight: 1 }}>{prior?.points ?? 0}</div>
            <div style={{ fontSize: 12, color: TT.textSec, fontFamily: 'DM Sans', marginTop: 4 }}>
              {prior?.correct_count}/{prior?.total_count} correct · {pct}%
              {prior?.duration_ms ? ` · ${Math.round(prior.duration_ms / 1000)}s` : ''}
            </div>
            {Object.values(penalty).some(v => v > 0) ? null : (
              <div style={{
                display: 'inline-block', marginTop: 8, padding: '4px 10px',
                borderRadius: 20, background: 'rgba(245,200,66,0.14)',
                border: '1px solid rgba(245,200,66,0.4)',
                fontSize: 10, fontFamily: 'DM Sans', fontWeight: 700,
                letterSpacing: '0.1em', color: TT.gold400,
              }}>NO HINTS</div>
            )}
          </div>
          {questions.map(qq => (
            <div key={qq.id} style={{ marginBottom: 18 }}>
              <QuestionBody q={qq} found={found[qq.id] || new Set()} revealed
                onGuess={() => false} hints={hints[qq.id] || {}} />
            </div>
          ))}
          <div style={{ height: 30 }} />
        </Shell>
      );
    }

    const need = requiredCount(q);
    const got = (found[q.id] || new Set()).size;
    const complete = got >= need || revealed[q.id];

    return (
      <Shell onExit={onExit}>
        {/* fixture header */}
        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          gap: 10, padding: '4px 0 14px',
        }}>
          <div style={{ fontFamily: 'DM Sans', fontSize: 12, color: TT.textSec }}>
            {fx?.home?.name}
          </div>
          <div style={{ fontSize: 10, color: TT.textMute }}>v</div>
          <div style={{ fontFamily: 'DM Sans', fontSize: 12, color: TT.textSec }}>
            {fx?.away?.name}
          </div>
        </div>

        {/* progress */}
        <div style={{ display: 'flex', gap: 4, marginBottom: 18 }}>
          {questions.map((_, i) => (
            <div key={i} style={{
              flex: 1, height: 3, borderRadius: 2,
              background: i < idx ? TT.gold400 : i === idx ? TT.gold600 : TT.navy700,
            }} />
          ))}
        </div>

        <QuestionBody q={q} found={found[q.id] || new Set()}
          revealed={!!revealed[q.id]} onGuess={guess} onResolve={reveal}
          hints={hints[q.id] || {}} />

        {!revealed[q.id] && q.type_key === 'never_played' && !complete && (
          <div style={{ marginTop: 14 }}>
            <button onClick={buyEliminate} style={{
              width: '100%', padding: '10px', borderRadius: 9,
              background: TT.navy800, border: '1px solid rgba(245,200,66,0.35)',
              cursor: 'pointer',
            }}>
              <div style={{ fontSize: 11, fontFamily: 'DM Sans', fontWeight: 700,
                color: TT.text }}>Remove a wrong answer</div>
              <div style={{ fontSize: 9, fontFamily: 'DM Sans', marginTop: 3,
                display: 'flex', alignItems: 'center', justifyContent: 'center',
                gap: 3, color: TT.gold400 }}>
                {freeLeft > 0 ? 'FREE' : <><window.HexIcon size={9} />{NATIONALITY_COST}</>}
              </div>
            </button>
          </div>
        )}

        {!revealed[q.id] && q.type_key !== 'never_played' && (
          <HintBar
            payload={q.payload}
            typeKey={q.type_key}
            balance={balance}
            freeLeft={freeLeft}
            hints={{ ...(hints[q.id] || {}), _solved: found[q.id] || new Set() }}
            maxPossible={Math.max(0, q.max_points - (penalty[q.id] || 0))}
            everUsed={everUsedHint}
            onLetter={buyLetter}
            onNationality={buyNationality}
            disabled={complete}
          />
        )}

        <div style={{ display: 'flex', gap: 8, marginTop: 18 }}>
          {/* No "give up" on multiple choice — all four answers are visible,
              so the button would just be a free pass. */}
          {!complete && q.type_key !== 'never_played' && (
            <button onClick={reveal} style={{
              flex: 1, padding: '13px', borderRadius: 10, cursor: 'pointer',
              background: 'transparent', border: `1px solid ${TT.border}`,
              color: TT.textSec, fontFamily: 'DM Sans', fontSize: 13, fontWeight: 700,
            }}>Give up</button>
          )}
          <button onClick={next} disabled={!complete} style={{
            flex: 2, padding: '13px', borderRadius: 10, border: 'none',
            cursor: complete ? 'pointer' : 'default',
            background: complete ? TT.gold400 : TT.navy800,
            color: complete ? '#1A1200' : TT.textMute,
            fontFamily: 'Bakbak One,sans-serif', fontSize: 14, letterSpacing: '0.05em',
          }}>
            {idx < questions.length - 1 ? 'NEXT' : 'FINISH'}
          </button>
        </div>
        <div style={{ height: 40 }} />

        {askName && (
          <NamePrompt
            initial={name}
            onCancel={() => setAskName(false)}
            onSave={async (n) => {
              setName(n);
              window.saveName && window.saveName(n);
              setAskName(false);
              await finish();
            }}
          />
        )}
      </Shell>
    );
  }

  // ── Multiple choice (never_played) ─────────────────────────────────────────
  // Four names, one of whom never turned out for the club. All four are SHOWN —
  // this was previously routed through RankedQuestion, which hid them behind
  // letter blanks and asked you to type a name you were never told. You get one
  // pick: the whole question is the judgement, so a retry would give it away.
  function ChoiceQuestion({ q, found, revealed, onGuess, onResolve, hints = {} }) {
    const TT = T();
    const a = (q.payload.answers || [])[0] || {};
    const options = q.payload.options || [];
    const [chosen, setChosen] = React.useState(null);
    const settled = chosen != null || found.has(a.rank) || revealed;
    const eliminated = hints.eliminated || new Set();

    const choose = (name) => {
      if (settled) return;
      setChosen(name);
      if (name === a.player) onGuess(name);
      else onResolve && onResolve();   // wrong: mark done so they can move on
    };

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 16 }}>
          <KitSwatch club={q.club} size={44} />
          <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 17,
            color: TT.text, lineHeight: 1.15 }}>{q.payload.prompt}</div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {options.map((name) => {
            const isAnswer = name === a.player;
            const isChosen = chosen === name;
            const gone = eliminated.has(name);
            let bg = TT.navy800, bd = TT.border, fg = TT.text;

            if (settled) {
              if (isAnswer) { bg = 'rgba(45,140,78,0.16)'; bd = 'rgba(55,168,95,0.55)'; }
              else if (isChosen) { bg = 'rgba(232,122,122,0.13)'; bd = 'rgba(232,122,122,0.5)'; fg = TT.red; }
              else { fg = TT.textMute; }
            } else if (gone) {
              bg = TT.navy900; fg = TT.textMute; bd = TT.border;
            }

            return (
              <button key={name} onClick={() => choose(name)} disabled={settled || gone}
                style={{
                  display: 'flex', alignItems: 'center', gap: 10,
                  padding: '13px 14px', borderRadius: 11, textAlign: 'left',
                  background: bg, border: `1px solid ${bd}`, color: fg,
                  cursor: settled || gone ? 'default' : 'pointer',
                  opacity: gone ? 0.4 : 1,
                  textDecoration: gone ? 'line-through' : 'none',
                  fontFamily: 'DM Sans', fontSize: 14.5,
                  fontWeight: settled && isAnswer ? 700 : 500,
                  transition: 'background .15s ease',
                }}>
                <span style={{ flex: 1 }}>{name}</span>
                {settled && isAnswer && <span style={{ color: TT.green, fontSize: 14 }}>✓</span>}
                {settled && isChosen && !isAnswer && <span style={{ color: TT.red, fontSize: 13 }}>✕</span>}
              </button>
            );
          })}
        </div>

        {settled && (
          <div style={{ marginTop: 12, fontSize: 12, fontFamily: 'DM Sans',
            color: chosen === a.player ? TT.green : TT.textSec, lineHeight: 1.5 }}>
            {chosen === a.player
              ? `Correct — ${a.player} never played for ${q.payload.club}.`
              : `${a.player} is the one who never played for ${q.payload.club}.`}
          </div>
        )}
      </div>
    );
  }

  // Asked once, before a first score is saved.
  function NamePrompt({ initial, onSave, onCancel }) {
    const TT = T();
    const [v, setV] = React.useState(initial || '');
    const ok = v.trim().length >= 2;
    return (
      <div onClick={onCancel} style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.7)', zIndex: 1100,
        display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
      }}>
        <div onClick={e => e.stopPropagation()} style={{
          background: TT.navy900, border: `1px solid ${TT.border}`,
          borderRadius: 16, padding: 20, width: '100%', maxWidth: 340,
        }}>
          <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 17,
            color: TT.text, marginBottom: 6 }}>WHAT SHOULD WE CALL YOU?</div>
          <div style={{ fontSize: 12, color: TT.textSec, fontFamily: 'DM Sans',
            marginBottom: 14, lineHeight: 1.5 }}>
            This is the name that shows on the leaderboard.
          </div>
          <input
            value={v} onChange={e => setV(e.target.value.slice(0, 24))}
            onKeyDown={e => { if (e.key === 'Enter' && ok) onSave(v.trim()); }}
            placeholder="Your name" autoFocus maxLength={24}
            style={{
              width: '100%', padding: '12px 13px', borderRadius: 10,
              background: TT.navy950, border: `1px solid ${TT.border}`,
              color: TT.text, fontFamily: 'DM Sans', fontSize: 15,
              outline: 'none', boxSizing: 'border-box', marginBottom: 12,
            }} />
          <button onClick={() => ok && onSave(v.trim())} disabled={!ok} style={{
            width: '100%', padding: '12px', borderRadius: 10, border: 'none',
            background: ok ? TT.gold400 : TT.navy800,
            color: ok ? '#1A1200' : TT.textMute,
            cursor: ok ? 'pointer' : 'default',
            fontFamily: 'Bakbak One,sans-serif', fontSize: 14, letterSpacing: '0.05em',
          }}>SAVE & FINISH</button>
        </div>
      </div>
    );
  }

  // ── Teammates ──────────────────────────────────────────────────────────────
  // One club, one named anchor player, many valid answers. Structurally the
  // same as SharedQuestion but the header carries the anchor rather than a
  // second kit, because the question is "who played alongside this person"
  // rather than "who spans these two clubs".
  function TeammatesQuestion({ q, found, revealed, onGuess, hints = {} }) {
    const TT = T();
    const [val, setVal] = React.useState('');
    const [shake, setShake] = React.useState(false);
    const need = requiredCount(q);
    const foundList = q.payload.answers.filter(a => found.has(a.rank));
    const openSlots = q.payload.answers.filter(a => !found.has(a.rank));
    const anchor = q.payload.anchor || {};

    const submit = (e) => {
      e && e.preventDefault();
      if (!val.trim()) return;
      const ok = onGuess(val);
      if (!ok) { setShake(true); setTimeout(() => setShake(false), 400); }
      setVal('');
    };

    return (
      <div>
        <div style={{ display: 'flex', alignItems: 'center',
          justifyContent: 'center', gap: 14, marginBottom: 12 }}>
          <KitSwatch club={q.payload.club} size={60} />
          <div style={{ textAlign: 'left' }}>
            <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 18,
              color: TT.gold400, lineHeight: 1.1 }}>{anchor.player}</div>
            {anchor.start_year && (
              <div style={{ fontSize: 11, color: TT.textMute, fontFamily: 'DM Sans',
                marginTop: 2 }}>
                {anchor.start_year}–{anchor.end_year || 'now'}
              </div>
            )}
          </div>
        </div>

        <div style={{ textAlign: 'center', fontFamily: 'Bakbak One,sans-serif',
          fontSize: 17, color: TT.text, marginBottom: 4, lineHeight: 1.2 }}>
          {q.payload.prompt}
        </div>
        <div style={{ textAlign: 'center', fontSize: 12, color: TT.textSec,
          fontFamily: 'DM Sans', marginBottom: 14 }}>
          {foundList.length} of {need} found
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 14 }}>
          {Array.from({ length: need }).map((_, i) => {
            const a = foundList[i];
            const target = !a ? openSlots[i - foundList.length] : null;
            return (
              <div key={i} style={{
                padding: '11px 12px', borderRadius: 10,
                background: a ? 'rgba(45,140,78,0.14)' : TT.navy800,
                border: `1px solid ${a ? 'rgba(55,168,95,0.5)' : TT.border}`,
                display: 'flex', alignItems: 'center', gap: 7,
              }}>
                {target && hints.nationality?.has(target.rank) && (
                  <NatFlag code={target.flag} title={target.nationality} />
                )}
                {a ? (
                  <span style={{ fontFamily: 'DM Sans', fontSize: 14, fontWeight: 700,
                    color: TT.text }}>{a.player}</span>
                ) : target ? (
                  <LetterBlanks name={target.player}
                    revealed={revealedLetters(hints, target.rank, q.type_key)} />
                ) : (
                  <span style={{ color: TT.textMute, letterSpacing: '0.18em' }}>— — — —</span>
                )}
              </div>
            );
          })}
        </div>

        {revealed && (
          <div style={{
            padding: '10px 12px', borderRadius: 10, marginBottom: 12,
            background: 'rgba(232,122,122,0.08)',
            border: '1px solid rgba(232,122,122,0.25)',
          }}>
            <div style={{ fontSize: 10, color: TT.red, fontFamily: 'DM Sans',
              fontWeight: 700, letterSpacing: '0.1em', marginBottom: 6 }}>
              OTHERS WHO QUALIFIED
            </div>
            <div style={{ fontSize: 12, color: TT.textSec, fontFamily: 'DM Sans',
              lineHeight: 1.6 }}>
              {q.payload.answers.filter(a => !found.has(a.rank))
                .slice(0, 8).map(a => a.player).join(' · ')}
            </div>
          </div>
        )}

        {!revealed && (
          <GuessInput val={val} setVal={setVal} onSubmit={submit} shake={shake}
            placeholder="Name a teammate…" />
        )}
      </div>
    );
  }

  function QuestionBody(props) {
    if (props.q.type_key === 'shared_players') return <SharedQuestion {...props} />;
    if (props.q.type_key === 'teammates')      return <TeammatesQuestion {...props} />;
    if (props.q.type_key === 'career_path')    return <CareerPathQuestion {...props} />;
    if (props.q.type_key === 'never_played')   return <ChoiceQuestion {...props} />;
    return <RankedQuestion {...props} />;
  }

  function Msg({ text }) {
    return <div style={{ padding: '48px 0', textAlign: 'center',
      color: window.T.textSec, fontFamily: 'DM Sans', fontSize: 13 }}>{text}</div>;
  }

  function Shell({ children, onExit }) {
    const TT = T();
    return (
      <div style={{ display: 'flex', flexDirection: 'column', height: '100%',
        background: TT.navy950 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10,
          padding: '14px 16px 6px' }}>
          <button onClick={onExit} style={{ background: 'transparent', border: 'none',
            color: TT.textSec, fontSize: 20, cursor: 'pointer', padding: 0 }}>‹</button>
          <div style={{ fontFamily: 'Bakbak One,sans-serif', fontSize: 15,
            color: TT.text, letterSpacing: '0.04em' }}>MATCHDAY</div>
        </div>
        <div className="no-scrollbar" style={{ flex: 1, overflowY: 'auto', padding: '0 16px' }}>
          {children}
        </div>
      </div>
    );
  }

  // shake keyframes
  if (!document.getElementById('md-anim')) {
    const st = document.createElement('style');
    st.id = 'md-anim';
    st.textContent = '@keyframes mdShake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}';
    document.head.appendChild(st);
  }

  window.MatchdayScreen = MatchdayScreen;
  window.MatchdayKitSwatch = KitSwatch;
})();
