/* =============================================================================
   SheetHead — application shell and non-game screens
   Depends on SH.engine, SH.ai, SH.presenter and SH.ui (cards.jsx, brand.jsx, game.jsx).
   Visual language lives in brand.css / brand.jsx: the card room (ink), ivory card stock, felt red, brass, Joker violet.
============================================================================= */
(function(){
const { useState, useEffect, useMemo, useRef, useCallback } = React;
const E = window.SH.engine;
const TN = window.SH.tournament;
const PR = window.SH.presenter;
const UI = window.SH.ui;
const { cx, Icon, Button, Modal, Card, GameScreen, MDiv, Presence, MotionRoot, ScreenFrame,
        Pip, Logo, Room, FaceCard, Rule, Frame, Switch, Tabs, Shuffle } = UI;

// ============== Small utilities ==============
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

// Persistent user settings
const SETTINGS_KEY = 'sheethead:settings';
function loadSettings() {
  try { return { ...defaultSettings(), ...(JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {}) }; }
  catch(e){ return defaultSettings(); }
}
function saveSettings(s) { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(s)); } catch(e){} }
function defaultSettings() {
  return {
    sound: true,
    animations: true,
    reducedMotion: false,
    fx: true,
    deal: true,
    quality: 'auto',
    theme: 'midnight',
    aiSpeedMs: 900
  };
}

// Player statistics live in src/stats.js (per-game records + monthly rollups), not here.

// ============== Screen registry / navigation ==============
const SCREENS = {
  LANDING: 'landing',
  AUTH: 'auth',
  LOBBY: 'lobby',
  MODE: 'mode',
  DIFFICULTY: 'difficulty',
  FRIENDS: 'friends',
  ONLINE: 'online',
  LOADING: 'loading',
  GAME: 'game',
  RULES: 'rules',
  SETTINGS: 'settings',
  PROFILE: 'profile',
  RESULTS: 'results',
  TOURNEY_SETUP: 'tourney-setup',
  TOURNEY_BOARD: 'tourney-board',
  TOURNEY_WIN: 'tourney-win',
  STATS: 'stats'
};

// E2E/design aid: ?screen=results[&won=0] opens the results screen with a synthetic finished match.
function demoResults(place) {
  const players = [{ id: 'me', name: 'You', kind: 'human' }, { id: 'b1', name: 'Nova', kind: 'ai-easy' }, { id: 'b2', name: 'Kairo', kind: 'ai-easy' }, { id: 'b3', name: 'Zephyr', kind: 'ai-easy' }];
  const st = E.newGame(players, 11, { endMode: 'LAST_STANDING' });
  const others = [1, 2, 3], order = others.slice(); order.splice(Math.max(0, Math.min(3, place - 1)), 0, 0);      // the human finishes in `place`
  st.status = 'FINISHED'; st.finishingOrder = order; st.winnerOrder = order.slice(0, 3); st.winner = order[0]; st.firstFinisher = order[0]; st.overallWinnerSeat = order[0];
  st.sheetHeadSeat = order[3]; st.duelWinnerSeat = order[2];
  order.forEach((seat, i) => { const p = st.players[seat]; p.finishedRank = i + 1; if (i < 3) { p.phase = 'OUT'; p.hand = []; p.faceUp = []; p.faceDown = []; } });
  return { finalState: st, humanSeat: 0, place, sheetHead: st.sheetHeadSeat === 0 };
}
// E2E/design aid: ?screen=tboard | tvictory builds a synthetic tournament (target from ?target=).
function demoTournament(kind, target) {
  const players = [{ id: 'me', name: 'Alex', kind: 'human' }, { id: 'b1', name: 'Nova', kind: 'ai-medium' }, { id: 'b2', name: 'Kairo', kind: 'ai-medium' }, { id: 'b3', name: 'Zephyr', kind: 'ai-medium' }];
  let T = TN.createTournament({ players, targetWins: target, seed: 3 });
  const wins = kind === 'tvictory' ? [0, 1, 0, 2, 0, 3, 0].slice(0, 0).concat([1, 0, 2, 0, 1, 0, 3, 0, 0, 0]) : [1, 0, 2, 0, 1];
  let now = 0;
  for (const w of wins) {
    if (T.status === 'FINISHED') break;
    const r = TN.startNextGame(T, now, { force: true });
    T = TN.recordGameResult(r.tournament, Object.assign({}, r.game, { status: 'FINISHED', winner: w }), now);
    now += 6000;
  }
  if (T.status === 'BETWEEN_GAMES') T = Object.assign({}, T, { nextGameAt: Date.now() + TN.INTERMISSION_MS });
  return T;
}
function App() {
  const params = useMemo(() => new URLSearchParams(window.location.search), []);
  const demoT = ['tboard', 'tvictory'].includes(params.get('screen')) ? demoTournament(params.get('screen'), +params.get('target') || 5) : null;
  const demo = params.get('screen') === 'results' && !params.get('gameId') ? demoResults(+params.get('place') || (params.get('won') === '0' ? 4 : 1)) : null;
  // E2E/design aid: ?screen=stats opens the real statistics; &demo=1 (or &demo=<games>) uses a seeded in-memory history, &demo=empty an empty one. Never persisted.
  const statsDemo = params.get('screen') === 'stats' ? params.get('demo') : null;
  const demoNow = useMemo(() => Date.now(), []);
  const demoStore = useMemo(() => {
    if (!statsDemo) return null;
    const S = window.SH.stats, st = S.createStore(S.memoryAdapter());
    if (statsDemo !== 'empty') S.seedDemo(st, demoNow, +statsDemo > 1 ? +statsDemo : 260);
    return st;
  }, []);
  // ---- identity: Supabase Auth via src/auth.js. Nothing is stored or faked here. -----------------------------------------------------
  const cfg = window.SH_CONFIG || null;
  const auth = useMemo(() => window.SH.authLib.createAuth({ config: cfg, sdk: window.supabase }), []);
  const [authState, setAuthState] = useState(auth.state);
  // Friends (Phase 3): a thin data layer over the same authenticated Supabase client auth.js already created -- never a
  // second client instance. See src/friends.js.
  const friends = useMemo(() => window.SH.friendsLib.createFriends({ client: auth.client }), [auth]);
  // Rooms (Phase 4): same pattern -- one instance, reusing auth's client. Also owns the single Realtime room channel
  // (Postgres Changes + Presence); see src/rooms.js.
  const rooms = useMemo(() => window.SH.roomsLib.createRooms({ client: auth.client }), [auth]);
  useEffect(() => { try { localStorage.removeItem('sheethead:user'); } catch (e) { /* ignore */ } }, []);       // the old simulated-login record: gone for good
  useEffect(() => { const off = auth.subscribe(setAuthState); auth.init(); return off; }, []);
  const allowGuest = !!(cfg && cfg.allowGuest);                      // offline practice without an account: off in production
  const guestAid = params.get('guest') === '1' || (() => { try { return localStorage.getItem('sheethead:guest') === '1'; } catch (e) { return false; } })() || ['results', 'tboard', 'tvictory', 'stats'].includes(params.get('screen'));   // design / test aids
  useEffect(() => { if (authState.status === 'signedOut' && allowGuest && guestAid && !authState.notice) auth.continueAsGuest(); }, [authState.status]);
  const user = authState.status === 'signedIn' ? authState.user : null;
  window.SH.stats.setOwner(user && user.provider !== 'guest' ? user.id : 'guest');      // local statistics are per account (a pure setter: safe every render)
  const [settings, setSettings] = useState(loadSettings());
  const [screen, setScreen] = useState(params.get('screen') === 'stats' ? SCREENS.STATS : demoT ? (demoT.status === 'FINISHED' ? SCREENS.TOURNEY_WIN : SCREENS.TOURNEY_BOARD) : demo ? SCREENS.RESULTS : SCREENS.LOBBY);

  // ?screen=results&gameId=<uuid>: show a game exactly as the SERVER recorded it (finishing order, SHEETHEAD, and the Trophy ledger row).
  const gameIdParam = params.get('screen') === 'results' ? params.get('gameId') : null;
  useEffect(() => {
    if (!gameIdParam || authState.status !== 'signedIn' || authState.guest) return;
    auth.gameRecord(gameIdParam).then(rec => { if (rec) { setLastResults(recordToResults(rec, authState.user.id)); setScreen(SCREENS.RESULTS); } });
  }, [authState.status]);
  const [gameSession, setGameSession] = useState(null);
  // TOURNAMENT state lives here, above any single game (the game engine knows nothing about it; see tournament.js)
  const [tournament, setTournament] = useState(demoT);
  const tRef = useRef(demoT);
  const [tourneyVenue, setTourneyVenue] = useState('solo');
  const [tourneyCfg, setTourneyCfg] = useState({ venue: 'solo', difficulty: 'medium', target: 5 });
  const [lastResults, setLastResults] = useState(demo);
  const [toast, setToast] = useState(null);
  const [history, setHistory] = useState([]);
  const [booted, setBooted] = useState(false);
  // Leaving a session clears everything that belonged to it (an unfinished game, a tournament in progress, the last result) and returns to the start.
  const prevAuth = useRef(authState.status);
  useEffect(() => {
    if (prevAuth.current === 'signedIn' && authState.status !== 'signedIn') {
      setScreen(SCREENS.LOBBY); setGameSession(null); setTournament(null); tRef.current = null; setLastResults(null); setHistory([]);
      rooms.unsubscribeFromRoom();      // sign-out must not leave a Realtime room channel open
    }
    prevAuth.current = authState.status;
  }, [authState.status]);

  useEffect(() => { saveSettings(settings); }, [settings]);

  // brand splash: waits for the display font so the first frame is already the real identity
  useEffect(() => {
    let live = true;
    const fonts = document.fonts && document.fonts.load ? Promise.all(['400 40px "Rozha One"', '700 14px "Space Grotesk"'].map(f => document.fonts.load(f).catch(() => null))) : Promise.resolve();
    Promise.race([fonts, sleep(1400)]).then(() => sleep(380)).then(() => { if (live) setBooted(true); });
    return () => { live = false; };
  }, []);

  useEffect(() => {
    // run engine self-tests once on load
    try {
      const results = E.selfTest();
      const failed = results.filter(r => !r.ok);
      if (failed.length) console.warn('SheetHead engine self-tests failed', failed);
      else console.log(`SheetHead engine: ${results.length} self-tests passed`);
    } catch(e) { console.error(e); }
  }, []);

  const nav = useCallback((target) => {
    setHistory(h => [...h, screen]);
    setScreen(target);
  }, [screen]);

  const back = useCallback(() => {
    setHistory(h => {
      if (h.length === 0) { setScreen(user ? SCREENS.LOBBY : SCREENS.LANDING); return []; }
      const prev = h[h.length - 1];
      setScreen(prev);
      return h.slice(0, -1);
    });
  }, [user]);

  const toastTimer = useRef(0);
  const showToast = (msg, ms=2200) => {
    setToast(msg);
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToast(null), ms);
  };

  const startSinglePlayer = (difficulty) => {
    const players = [
      { id: user?.id || 'me', name: user?.name || 'You', kind: 'human', avatar: user?.avatarKey },
      { id: 'bot1', name: 'Nova', kind: 'ai-'+difficulty },
      { id: 'bot2', name: 'Kairo', kind: 'ai-'+difficulty },
      { id: 'bot3', name: 'Zephyr', kind: 'ai-'+difficulty }
    ];
    const seed = Math.floor(Math.random() * 2**31) || 1;
    const state = E.newGame(players, seed, { endMode: 'LAST_STANDING' });     // normal games play on until one player is left: the SheetHead
    setGameSession({ state, players, difficulty, aiSeats: [1,2,3], humanSeat: 0, seed, mode: 'single', recordStats: true });
    setScreen(SCREENS.LOADING);
  };

  // hand-over from the menu into a match: a short shuffle so the table never pops in cold
  useEffect(() => {
    if (screen !== SCREENS.LOADING) return undefined;
    const t = setTimeout(() => setScreen(gameSession ? SCREENS.GAME : SCREENS.LOBBY), settings.reducedMotion ? 350 : 1050);
    return () => clearTimeout(t);
  }, [screen]);

  // ---- tournament orchestration (authority = this host today; the game server online) ----
  const launchGame = (T, cfg, viaLoading) => {
    const r = TN.startNextGame(T, Date.now());
    tRef.current = r.tournament; setTournament(r.tournament);
    setGameSession({ state: r.game, players: r.tournament.players, difficulty: cfg.difficulty, aiSeats: [1, 2, 3], humanSeat: 0, seed: r.game.seed,
                     mode: cfg.venue === 'online' ? 'online' : 'single', tournament: tournamentInfo(r.tournament), key: r.game.gameId, recordStats: true });
    setScreen(viaLoading ? SCREENS.LOADING : SCREENS.GAME);
  };
  const startTournament = ({ venue, difficulty, target }) => {
    const players = [
      { id: user?.id || 'me', name: user?.name || 'You', kind: 'human', avatar: user?.avatar },
      { id: 'bot1', name: 'Nova', kind: 'ai-' + difficulty }, { id: 'bot2', name: 'Kairo', kind: 'ai-' + difficulty }, { id: 'bot3', name: 'Zephyr', kind: 'ai-' + difficulty }
    ];
    const seed = Math.floor(Math.random() * 2 ** 31) || 1;
    const T0 = TN.createTournament({ players, targetWins: target, seed });
    const cfg = { venue, difficulty, target };
    setTourneyCfg(cfg); setLastResults(null);
    launchGame(T0, cfg, true);
  };
  // Called by the scoreboard when its 5 s are over. Idempotent: only a due BETWEEN_GAMES tournament can start a game.
  const onNextGame = () => {
    const T = tRef.current;
    if (!T || T.status !== 'BETWEEN_GAMES' || !TN.isNextGameDue(T, Date.now())) return;
    launchGame(T, tourneyCfg, false);
  };
  const leaveTournament = () => { tRef.current = null; setTournament(null); setGameSession(null); setHistory([]); setScreen(user ? SCREENS.LOBBY : SCREENS.LANDING); };
  // Phase 5E: the Friends counterpart of leaveTournament -- also tears down the ONE persisted remote adapter/
  // channel that has been kept alive across every round of the tournament (never disposed per-round; see
  // FriendsTournamentScreen/makeRoundSubAdapter above).
  const leaveFriendsTournament = () => {
    if (gameSession && gameSession.remoteAdapter) gameSession.remoteAdapter.dispose();
    setGameSession(null); setScreen(user ? SCREENS.LOBBY : SCREENS.LANDING);
  };
  const openTournament = (venue) => { setTourneyVenue(venue); nav(SCREENS.TOURNEY_SETUP); };

  // Phase 5C/5E -- the ONLY way a client ever enters a real Friends game (normal or tournament): real to the
  // trusted remote authority (Phase 5A's game-init), never a locally-invented game. Any of the four players may
  // call this (host or not) -- it is idempotent (game-init recovers/returns the existing game if one is already
  // initialized), so there is no "only the host can start" dependency here (CLAUDE.md Phase 5C §6). Also the
  // refresh/reconnect entry point (§7): called identically whether the room was just started or is being
  // rediscovered after a page reload. Which of the two client-side presentations to use is decided ONLY from the
  // room's own server-trusted game_mode (CLAUDE.md Phase 5E §11 -- "never by guessing from payload shape"), never
  // by sniffing the shape of the safe view the server happens to return.
  const enterFriendsGame = async (room) => {
    const isTournament = room.gameMode === 'friends_tournament';
    const RG = window.SH.remoteGame;
    // RG.enterRoomGame (src/remote-game.js) owns the init -> adapter.start() -> bounded-retry sequence, including
    // the Phase 5C.2 recovery from a single transient first-read hiccup (CLAUDE.md §2.3: that orchestration is
    // remote-game logic, not a UI concern, so it is not inlined here).
    const entry = await RG.enterRoomGame({ client: auth.client, roomId: room.id });
    if (!entry.ok) {
      // init_failed: game-init itself never reached 'initialized'/'already_initialized' (CLAUDE.md Phase 5C §6 --
      // there is no 'exists' result; Phase 5C.1's real E2E caught an earlier version of this code checking for
      // that non-existent value). load_failed: init succeeded but this caller's own view never became readable
      // even after RG.enterRoomGame's retries.
      showToast(entry.reason === 'init_failed' ? 'Could not start the game. Please try again.' : 'Could not load the game. Please try again.');
      return false;
    }
    // humanSeat comes ONLY from the server's own game_player_views row (entry.seat) -- never assumed from the
    // room roster, never seat 0, never a client guess (CLAUDE.md Phase 5C §5).
    setGameSession({ state: entry.view, humanSeat: entry.seat, mode: isTournament ? 'friends_tournament' : 'friends', remoteAdapter: entry.adapter, key: entry.gameId, recordStats: false });
    setScreen(SCREENS.GAME);
    return true;
  };

  // Refresh/reconnect recovery (CLAUDE.md Phase 5C §7): the moment auth is restored, check for an active Friends
  // room/game and resume straight into it -- no room-code re-entry, no host action, no replaying history. Runs
  // once per sign-in, independent of which tab/screen is currently open (a page reload always lands on the
  // default screen, not necessarily the Friends tab).
  useEffect(() => {
    if (!user || authState.guest || gameSession) return undefined;
    let live = true;
    rooms.fetchCurrentRoom().then(r => {
      if (!live || !r.ok || r.result !== 'found' || r.room.status !== 'starting') return;
      enterFriendsGame(r.room);
    });
    return () => { live = false; };
  }, [user && user.id]);

  const onGameEnd = (finalState, humanSeat) => {
    if (gameSession && gameSession.tournament && tRef.current) {
      try {
        const T2 = TN.recordGameResult(tRef.current, finalState, Date.now());       // the authority awards the point (idempotent)
        tRef.current = T2; setTournament(T2);
        if (T2.status === 'FINISHED' && window.SH.stats) {
          try { const me = T2.players.find(p => p.kind === 'human'); window.SH.stats.local().addTournament(window.SH.stats.buildTournamentSummary(T2, me.id, { endedAt: Date.now(), venue: (tourneyCfg && tourneyCfg.venue) || 'solo', difficulty: tourneyCfg && tourneyCfg.difficulty })); }
          catch (err) { console.error('SheetHead: could not record the tournament', err); }
        }
        setScreen(T2.status === 'FINISHED' ? SCREENS.TOURNEY_WIN : SCREENS.TOURNEY_BOARD);
      } catch (e) { console.error('SheetHead: tournament result rejected', e); }
      return;
    }
    // (the permanent statistics record is written by GameScreen the moment the game finishes; this only shows the result)
    const place = Math.max(1, (finalState.finishingOrder || []).indexOf(humanSeat) + 1) || (finalState.winner === humanSeat ? 1 : 4);
    const sheetHead = finalState.sheetHeadSeat === humanSeat;
    setLastResults({ finalState, humanSeat, place, sheetHead });
    // Stale-room fix: a finished Friends room never transitions out of 'starting' on its own (Phase 4 never built a
    // rematch/re-lobby flow -- confirmed invariant, unchanged here), which otherwise permanently blocks this profile
    // from ever creating/joining a new room (create_room/join_room's "already in a room" check only excludes
    // 'closed' rooms). The game just finished, so leaving it now is unconditionally safe -- best-effort, fire and
    // forget: a failure here must never block showing the result, and leave_room() is naturally idempotent if
    // another of the four clients already released it first.
    if (gameSession && gameSession.mode === 'friends') rooms.leaveRoom();   // never rejects (see rooms.js callRpc) -- a safe fire-and-forget
    setScreen(SCREENS.RESULTS);
  };

  // Render
  const ctx = { nav, back, setScreen, showToast, user, auth, friends, rooms, authState, allowGuest, settings, setSettings,
                gameSession, setGameSession, lastResults, setLastResults, startSinglePlayer, onGameEnd, enterFriendsGame,
                tournament, tourneyVenue, tourneyCfg, startTournament, onNextGame, leaveTournament, openTournament, leaveFriendsTournament,
                humanSeat: (gameSession && gameSession.humanSeat) || 0 };

  return (
    <MotionRoot reduced={!!settings.reducedMotion}>
    <div className="app">
      <Room settings={settings} paused={screen === SCREENS.GAME}/>
      {!booted || authState.status === 'loading' ? (
        <div className="boot" data-testid="boot"><Logo variant="stacked" animated size={112} tagline={false}/><BootHint/></div>
      ) : authState.status !== 'signedIn' ? (
        <Presence mode="wait" initial={false}>
          <ScreenFrame key={authState.status}>
            {authState.status === 'unconfigured' && <UI.UnconfiguredScreen allowGuest={allowGuest} onGuest={() => auth.continueAsGuest()}/>}
            {authState.status === 'signedOut' && <UI.AuthScreen auth={auth} authState={authState} allowGuest={allowGuest} onGuest={() => auth.continueAsGuest()}/>}
            {authState.status === 'recovery' && <UI.RecoveryScreen auth={auth}/>}
            {authState.status === 'error' && <UI.AuthErrorScreen auth={auth} authState={authState}/>}
          </ScreenFrame>
        </Presence>
      ) : (
      <Presence mode="wait" initial={false}>
        <ScreenFrame key={screen} still={screen === SCREENS.GAME}>
          {screen === SCREENS.LANDING && <WelcomeScreen {...ctx}/>}
          {screen === SCREENS.LOBBY && <HomeScreen {...ctx}/>}
          {screen === SCREENS.MODE && <ModeScreen {...ctx}/>}
          {screen === SCREENS.DIFFICULTY && <DifficultyScreen {...ctx}/>}
          {screen === SCREENS.FRIENDS && <FriendsScreen {...ctx}/>}
          {screen === SCREENS.ONLINE && <OnlineScreen {...ctx}/>}
          {screen === SCREENS.LOADING && <div className="center-fill"><Shuffle label={gameSession ? 'Shuffling · ' + ({easy:'Easy',medium:'Medium',hard:'Hard',expert:'Expert'}[gameSession.difficulty] || '') + ' table' : 'Shuffling'}/></div>}
          {screen === SCREENS.GAME && gameSession && gameSession.mode === 'friends_tournament' && <FriendsTournamentScreen key={gameSession.key} {...ctx}/>}
          {screen === SCREENS.GAME && gameSession && gameSession.mode !== 'friends_tournament' && <GameScreen key={gameSession.key || 'game'} {...ctx}/>}
          {screen === SCREENS.TOURNEY_SETUP && <TournamentSetupScreen {...ctx}/>}
          {screen === SCREENS.TOURNEY_BOARD && tournament && <TournamentBoardScreen {...ctx}/>}
          {screen === SCREENS.TOURNEY_WIN && tournament && <TournamentVictoryScreen {...ctx}/>}
          {screen === SCREENS.RULES && <UI.RulesScreen {...ctx}/>}
          {screen === SCREENS.SETTINGS && <SettingsScreen {...ctx}/>}
          {screen === SCREENS.PROFILE && <UI.ProfileScreen {...ctx}/>}
          {screen === SCREENS.RESULTS && lastResults && <ResultsScreen {...ctx}/>}
          {screen === SCREENS.STATS && <UI.StatisticsScreen {...ctx} statsStore={demoStore || undefined} statsNow={demoStore ? demoNow : undefined} persist={!demoStore}/>}
        </ScreenFrame>
      </Presence>
      )}
      {toast && <div className="toast" role="status">{toast}</div>}
    </div>
    </MotionRoot>
  );
}

// ============== Shared pieces ==============
// After a few seconds on the splash, say what is happening instead of leaving the player staring at a logo.
function BootHint() {
  const [late, setLate] = useState(false);
  useEffect(() => { const t = setTimeout(() => setLate(true), 3500); return () => clearTimeout(t); }, []);
  return late ? <p className="boot-msg" role="status" data-testid="boot-hint">Connecting to SheetHead…</p> : null;
}

function NavBar({ nav, user, current, settings }) {
  const av = PR.avatarByKey(user?.avatarKey);
  return (
    <header className="nav">
      <button type="button" className="nav-brand" onClick={() => nav(user ? SCREENS.LOBBY : SCREENS.LANDING)} aria-label="SheetHead home"><Logo variant="lockup" size={40}/></button>
      <nav className="nav-links" aria-label="Main">
        <button type="button" aria-label="Rules" className={cx('nav-link', current === 'rules' && 'on')} onClick={() => nav(SCREENS.RULES)}><Icon name="book" size={16}/><span>Rules</span></button>
        <button type="button" aria-label="Settings" className={cx('nav-link', current === 'settings' && 'on')} onClick={() => nav(SCREENS.SETTINGS)}><Icon name="settings" size={16}/><span>Settings</span></button>
        {user && (
          <button type="button" className="player-tag" onClick={() => nav(SCREENS.PROFILE)} aria-label={'Profile: ' + user.name}>
            <span className="pt-av" style={{ '--hue': av.hue }} aria-hidden="true">{av.glyph}</span>
            <span className="pt-name">{user.name}</span>
            <UI.TrophyChip user={user} settings={settings}/>
          </button>
        )}
      </nav>
    </header>
  );
}

// The four ways into a game, as a hand of printed cards. Used by the home screen and the mode picker.
function ModeFan({ nav, className, withStats }) {
  return (
    <div className={cx('fan', withStats && 'fan-5', className)} role="group" aria-label="Game modes">
      <FaceCard index={0} rank="A" suit="S" tilt={withStats ? -12 : -9} kicker="Single player" title="Solo" desc="Three bots at your table. Choose how sharp they are." cta="Deal in" onClick={() => nav(SCREENS.DIFFICULTY)}/>
      <FaceCard index={1} rank="K" suit="H" tilt={-3} kicker="Quick match" title="Online" desc="Find three players and take a seat." cta="Find a table" onClick={() => nav(SCREENS.ONLINE)}/>
      <FaceCard index={2} rank="Q" suit="D" tilt={withStats ? 0 : 3} kicker="Private table" title="Friends" desc="Host a room, or join one with a code." cta="Open a room" onClick={() => nav(SCREENS.FRIENDS)}/>
      <FaceCard index={3} rank="J" suit="C" tilt={withStats ? 6 : 9} kicker="How to play" title="Learn" desc="The rules of the table, chapter by chapter." cta="Read" onClick={() => nav(SCREENS.RULES)}/>
      {withStats && <FaceCard index={4} rank="10" suit="H" tilt={12} kicker="Your record" title="Statistics" desc="Wins, streaks, burns and every SheetHead moment." cta="Open" className="fc-stats" onClick={() => nav(SCREENS.STATS)}/>}
    </div>
  );
}

// ============== Welcome (signed out) ==============
function WelcomeScreen({ nav, user }) {
  const hand = [
    { id: 'w1', rank: '8', suit: 'C' }, { id: 'w2', rank: 'K', suit: 'H' }, { id: 'w3', rank: 'JOKER', suit: null },
    { id: 'w4', rank: 'A', suit: 'S' }, { id: 'w5', rank: 'Q', suit: 'D' }
  ];
  const M = UI.M;
  return (
    <div className="welcome">
      <div className="welcome-top">
        <Logo variant="stacked" size={112} animated/>
        <p className="welcome-lede">Shed every card before anyone else does. Burn the pile, block the table, and hold your Joker until it counts.</p>
        <div className="cta-row">
          <Button variant="primary" onClick={() => nav(user ? SCREENS.LOBBY : SCREENS.AUTH)} icon={<Icon name="play" size={16}/>}>{user ? 'Continue' : 'Sign in to play'}</Button>
          <Button variant="ghost" onClick={() => nav(SCREENS.RULES)}>How to play</Button>
        </div>
        <div className="welcome-facts"><span>4 players</span><i/><span>52 cards + 2 Jokers</span><i/><span>Solo &middot; Online &middot; Friends</span></div>
        {!user && <UI.LegalLinks/>}
      </div>
      <div className="welcome-hand" aria-hidden="true">
        {hand.map((c, i) => {
          const o = i - 2, rot = o * 11, x = o * 70, y = Math.abs(o) * 14 + 6;
          const inner = <Card card={c} size={132}/>;
          return M ? (
            <MDiv key={c.id} className="wh-card" style={{ zIndex: i }} initial={{ opacity: 0, y: 220, rotate: 0, x: 0 }} animate={{ opacity: 1, y, x, rotate: rot }} transition={{ type: 'spring', stiffness: 120, damping: 17, delay: 0.5 + i * 0.09 }}>{inner}</MDiv>
          ) : <div key={c.id} className="wh-card" style={{ zIndex: i, transform: `translate(${x}px,${y}px) rotate(${rot}deg)` }}>{inner}</div>;
        })}
      </div>
    </div>
  );
}

// ============== Home (signed in) ==============
function HomeScreen({ nav, user, settings }) {
  return (
    <div className="home">
      <NavBar nav={nav} user={user} settings={settings}/>
      <div className="home-stage">
        <section className="home-hero" aria-label="SheetHead">
          <Logo variant="stacked" size={128} animated tagline={false}/>
          <p className="hero-tag" data-testid="tagline">Empty your hand.<br/><em>Dodge the poop hat.</em></p>
        </section>
        <ModeFan nav={nav} withStats/>
      </div>
    </div>
  );
}

// ============== Mode selection ==============
function ModeScreen({ nav, back }) {
  return (
    <Frame title="Pick your table" kicker="Play" onBack={back} wide bodyClass="center">
      <ModeFan nav={nav} className="fan-page"/>
    </Frame>
  );
}

// ============== AI Difficulty ==============
function LevelCard({ level, index, onPick }) {
  const layouts = { 1: [[50, 50]], 2: [[50, 24], [50, 76]], 3: [[50, 20], [50, 50], [50, 80]], 4: [[30, 26], [70, 26], [30, 74], [70, 74]] };
  const red = level.suit === 'H' || level.suit === 'D';
  return (
    <MDiv className="fc-slot lv-slot" initial={{ opacity: 0, y: 60 }} animate={{ opacity: 1, y: 0 }} transition={{ type: 'spring', stiffness: 170, damping: 20, delay: 0.1 + index * 0.08 }}>
      <button type="button" className={cx('fcard lvcard', red ? 'red' : 'black', level.foil && 'foil')} style={{ '--tilt': level.tilt + 'deg' }} onClick={() => onPick(level.id)}>
        <span className="fc-idx tl"><b>{level.rank}</b><Pip suit={level.suit} size={16}/></span>
        <span className="fc-idx br"><b>{level.rank}</b><Pip suit={level.suit} size={16}/></span>
        <span className="pips" aria-hidden="true">
          {layouts[level.pips].map(([x, y], i) => <Pip key={i} suit={level.suit} size={level.pips === 1 ? 64 : 34} style={{ left: x + '%', top: y + '%', transform: 'translate(-50%,-50%)' + (y > 50 && level.pips > 1 ? ' rotate(180deg)' : '') }}/>)}
        </span>
        <span className="fc-body">
          <span className="fc-kicker">{level.kicker}</span>
          <strong className="fc-title">{level.name}</strong>
          <span className="fc-desc">{level.desc}</span>
        </span>
        <span className="fc-cta">Sit down<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" strokeWidth="2.600" strokeLinecap="round" strokeLinejoin="round"/></svg></span>
      </button>
    </MDiv>
  );
}
function TourneyCta({ venue, openTournament }) {
  return (
    <button type="button" className="tourney-cta" onClick={() => openTournament(venue)} data-testid="tournament-cta">
      <span className="tc-seal" aria-hidden="true"><Icon name="trophy" size={22}/></span>
      <span className="tc-text"><b>Tournament</b><i>First to 3, 5, 7 or 10 game wins. The winner of each game starts the next one with a better setup.</i></span>
      <span className="tc-go">Set up<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" fill="none" stroke="currentColor" strokeWidth="2.600" strokeLinecap="round" strokeLinejoin="round"/></svg></span>
    </button>
  );
}
function DifficultyScreen({ back, startSinglePlayer, openTournament }) {
  const levels = [
    { id:'easy',   name:'Easy',    rank:'A', suit:'C', pips:1, tilt:-6, kicker:'Learning the ropes', desc:'Basic legal play. Good for learning.' },
    { id:'medium', name:'Medium',  rank:'2', suit:'S', pips:2, tilt:-2, kicker:'A fair game',        desc:'Smarter card choice and special-card awareness.' },
    { id:'hard',   name:'Hard',    rank:'3', suit:'D', pips:3, tilt:2,  kicker:'Plays the long game', desc:'Stronger strategy and long-term thinking.' },
    { id:'expert', name:'Expert',  rank:'4', suit:'H', pips:4, tilt:6,  kicker:'Counts every card',   desc:'Card memory, planning and opponent modelling.', foil:true }
  ];
  return (
    <Frame title="Choose your opponents" kicker="Solo" onBack={back} wide bodyClass="center">
      <p className="frame-lede">Three bots share your table. The deal is random; the bots are not.</p>
      <div className="fan fan-levels" role="group" aria-label="Bot difficulty">
        {levels.map((lv, i) => <LevelCard key={lv.id} level={lv} index={i} onPick={startSinglePlayer}/>)}
      </div>
      <TourneyCta venue="solo" openTournament={openTournament}/>
    </Frame>
  );
}

// ============== Seat map (top-down table with four seats) ==============
function SeatMap({ seats }) {
  const pos = ['bottom', 'left', 'top', 'right'];
  return (
    <div className="seatmap" role="list" aria-label="Seats at the table">
      <div className="sm-table" aria-hidden="true"><span>SheetHead</span></div>
      {seats.map((s, i) => (
        <div key={i} role="listitem" className={cx('sm-seat', 'p-' + pos[i], 'st-' + s.state)}>
          <span className="sm-plate">{s.state === 'you' || s.state === 'joined' || s.state === 'bot' ? (s.name || '?')[0] : <Pip suit="S" size={16}/>}</span>
          <span className="sm-name">{s.name}</span>
          <span className="sm-state">{s.label}</span>
        </div>
      ))}
    </div>
  );
}

// ============== Online (stub) ==============
function OnlineScreen({ back, startSinglePlayer, showToast, user, openTournament }) {
  const [searching, setSearching] = useState(false);
  const [elapsed, setElapsed] = useState(0);
  useEffect(() => {
    if (!searching) return undefined;
    const t = setInterval(() => setElapsed(e => e+1), 1000);
    return () => clearInterval(t);
  }, [searching]);
  useEffect(() => { if (!searching) setElapsed(0); }, [searching]);
  const me = user?.name || 'You';
  const seats = [
    { name: me, state: 'you', label: 'You' },
    { name: 'Open seat', state: searching ? 'seeking' : 'open', label: searching ? 'Searching' : 'Open' },
    { name: 'Open seat', state: searching ? 'seeking' : 'open', label: searching ? 'Searching' : 'Open' },
    { name: 'Open seat', state: searching ? 'seeking' : 'open', label: searching ? 'Searching' : 'Open' }
  ];
  return (
    <Frame title="Quick match" kicker="Online" onBack={back} wide bodyClass="center">
      <div className="split">
        <SeatMap seats={seats}/>
        <section className="ticket">
          {!searching ? (
            <>
              <span className="kicker dark">Four seats, one table</span>
              <h2 className="ticket-title sm">Find a game</h2>
              <p className="ticket-sub">We seat you with three other players. In this demo build the multiplayer backend is scaffolded, so empty seats are filled by bots.</p>
              <div className="cta-col">
                <Button variant="primary" onClick={() => setSearching(true)} icon={<Icon name="play" size={16}/>}>Find match</Button>
                <Button variant="ghost-dark" onClick={() => { showToast('Starting offline game…'); startSinglePlayer('medium'); }}>Play offline instead</Button>
              </div>
            </>
          ) : (
            <>
              <span className="kicker dark">Looking for players</span>
              <h2 className="ticket-title sm">Searching&hellip;</h2>
              <p className="ticket-sub">Waiting <b className="tnum">{elapsed}s</b>. You can jump in with bots any time.</p>
              <div className="cta-col">
                <Button variant="primary" onClick={() => { setSearching(false); startSinglePlayer('hard'); }}>Start with bots</Button>
                <Button variant="ghost-dark" onClick={() => setSearching(false)}>Cancel</Button>
              </div>
            </>
          )}
        </section>
      </div>
      <TourneyCta venue="online" openTournament={openTournament}/>
    </Frame>
  );
}

// ============== Friends ==============
function CodeTiles({ value, count = 6, active }) {
  return (
    <div className="code-tiles" aria-hidden="true">
      {Array.from({ length: count }).map((_, i) => <span key={i} className={cx('ct', value[i] && 'filled', active && i === value.length && 'caret')}>{value[i] || ''}</span>)}
    </div>
  );
}
// ---- Friends: My Friends / Requests / Add Friend sections (Phase 3, real Supabase data) --------------------------
function FriendAvatar({ avatarKey, size = 40 }) {
  const a = PR.avatarByKey(avatarKey);
  return <span className="friend-avatar" style={{ '--hue': a.hue, width: size, height: size, fontSize: Math.round(size * 0.52) }} aria-hidden="true">{a.glyph}</span>;
}
function FriendEmpty({ children }) { return <p className="friend-empty">{children}</p>; }

function MyFriendsPanel({ friends, showToast, onChanged }) {
  const [state, setState] = useState({ loading: true, items: [], error: null });
  const [confirm, setConfirm] = useState(null);
  const load = useCallback(async () => {
    setState(s => ({ ...s, loading: true }));
    const r = await friends.listFriends();
    setState({ loading: false, items: r.ok ? r.data : [], error: r.ok ? null : r.error });
  }, [friends]);
  useEffect(() => { load(); }, [load]);
  useEffect(() => { onChanged && onChanged.current && (onChanged.current.friends = load); }, [load]);
  const remove = async (f) => {
    setConfirm(null);
    const r = await friends.removeFriend(f.id);
    if (!r.ok) { showToast(r.error); return; }
    showToast(r.result === 'removed' ? 'Removed ' + f.display_name + '.' : 'Already not friends.');
    load();
  };
  return (
    <>
      <div className="friend-panel-head">
        <span className="kicker dark">My Friends</span>
        <button type="button" className="linkish sm" onClick={load} aria-label="Refresh friends list">Refresh</button>
      </div>
      {state.loading ? <FriendEmpty>Loading your friends…</FriendEmpty>
        : state.error ? <FriendEmpty>{state.error}</FriendEmpty>
        : state.items.length === 0 ? <FriendEmpty>No friends yet. Add one from the "Add Friend" tab.</FriendEmpty>
        : (
          <ul className="friend-list">
            {state.items.map(f => (
              <li className="friend-row" key={f.id}>
                <FriendAvatar avatarKey={f.avatar_key}/>
                <span className="friend-info"><span className="friend-name">{f.display_name}</span><span className="friend-user">@{f.username}</span></span>
                <span className="friend-actions">
                  <Button variant="ghost-dark" onClick={() => setConfirm(f)}>Remove</Button>
                </span>
              </li>
            ))}
          </ul>
        )}
      {confirm && (
        <Modal label="Remove friend" onClose={() => setConfirm(null)}>
          <div className="font-display text-xl font-bold mb-2">Remove {confirm.display_name}?</div>
          <div className="text-white/60 text-sm mb-4">You can send another friend request later.</div>
          <div className="flex gap-2 justify-center">
            <Button variant="coral" onClick={() => remove(confirm)}>Remove friend</Button>
            <Button variant="ghost" onClick={() => setConfirm(null)}>Cancel</Button>
          </div>
        </Modal>
      )}
    </>
  );
}

function FriendRequestsPanel({ friends, showToast, onChanged }) {
  const [incoming, setIncoming] = useState({ loading: true, items: [], error: null });
  const [outgoing, setOutgoing] = useState({ loading: true, items: [], error: null });
  const load = useCallback(async () => {
    setIncoming(s => ({ ...s, loading: true })); setOutgoing(s => ({ ...s, loading: true }));
    const [inc, out] = await Promise.all([friends.listIncomingRequests(), friends.listOutgoingRequests()]);
    setIncoming({ loading: false, items: inc.ok ? inc.data : [], error: inc.ok ? null : inc.error });
    setOutgoing({ loading: false, items: out.ok ? out.data : [], error: out.ok ? null : out.error });
  }, [friends]);
  useEffect(() => { load(); }, [load]);
  useEffect(() => { onChanged && onChanged.current && (onChanged.current.requests = load); }, [load]);
  const accept = async (r) => {
    const res = await friends.acceptFriendRequest(r.request_id);
    if (!res.ok) { showToast(res.error); return; }
    showToast(res.result === 'friends' ? 'You and ' + r.display_name + ' are now friends!' : 'That request is gone already.');
    load(); onChanged && onChanged.current && onChanged.current.friends && onChanged.current.friends();
  };
  const decline = async (r) => { const res = await friends.declineFriendRequest(r.request_id); if (!res.ok) { showToast(res.error); return; } showToast('Request declined.'); load(); };
  const cancel = async (r) => { const res = await friends.cancelFriendRequest(r.request_id); if (!res.ok) { showToast(res.error); return; } showToast('Request cancelled.'); load(); };
  return (
    <>
      <div className="friend-panel-head">
        <span className="kicker dark">Friend Requests</span>
        <button type="button" className="linkish sm" onClick={load} aria-label="Refresh requests">Refresh</button>
      </div>
      <span className="friend-subhead">Incoming</span>
      {incoming.loading ? <FriendEmpty>Loading…</FriendEmpty>
        : incoming.error ? <FriendEmpty>{incoming.error}</FriendEmpty>
        : incoming.items.length === 0 ? <FriendEmpty>No incoming requests.</FriendEmpty>
        : (
          <ul className="friend-list">
            {incoming.items.map(r => (
              <li className="friend-row" key={r.request_id}>
                <FriendAvatar avatarKey={r.avatar_key}/>
                <span className="friend-info"><span className="friend-name">{r.display_name}</span><span className="friend-user">@{r.username}</span></span>
                <span className="friend-actions">
                  <Button variant="primary" onClick={() => accept(r)}>Accept</Button>
                  <Button variant="ghost-dark" onClick={() => decline(r)}>Decline</Button>
                </span>
              </li>
            ))}
          </ul>
        )}
      <span className="friend-subhead">Sent</span>
      {outgoing.loading ? <FriendEmpty>Loading…</FriendEmpty>
        : outgoing.error ? <FriendEmpty>{outgoing.error}</FriendEmpty>
        : outgoing.items.length === 0 ? <FriendEmpty>No outgoing requests.</FriendEmpty>
        : (
          <ul className="friend-list">
            {outgoing.items.map(r => (
              <li className="friend-row" key={r.request_id}>
                <FriendAvatar avatarKey={r.avatar_key}/>
                <span className="friend-info"><span className="friend-name">{r.display_name}</span><span className="friend-user">@{r.username}</span></span>
                <span className="friend-actions"><Button variant="ghost-dark" onClick={() => cancel(r)}>Cancel</Button></span>
              </li>
            ))}
          </ul>
        )}
    </>
  );
}

function AddFriendPanel({ friends, showToast, onChanged }) {
  const [query, setQuery] = useState('');
  const [state, setState] = useState({ loading: false, items: [], searched: false, error: null });
  const timer = useRef(0);
  const [sent, setSent] = useState(() => new Set());
  useEffect(() => {
    clearTimeout(timer.current);
    const q = query.trim();
    if (q.length < 2) { setState({ loading: false, items: [], searched: false, error: null }); return undefined; }
    timer.current = setTimeout(async () => {
      setState(s => ({ ...s, loading: true }));
      const r = await friends.searchProfiles(q);
      setState({ loading: false, items: r.ok ? r.data : [], searched: true, error: r.ok ? null : r.error });
    }, 350);
    return () => clearTimeout(timer.current);
  }, [query, friends]);
  const send = async (p) => {
    setSent(s => new Set(s).add(p.id));
    const r = await friends.sendFriendRequest(p.id);
    if (!r.ok) { showToast(r.error); setSent(s => { const n = new Set(s); n.delete(p.id); return n; }); return; }
    if (r.result === 'auto_accepted_friends') { showToast('You and ' + p.display_name + ' are now friends!'); onChanged && onChanged.current && onChanged.current.friends && onChanged.current.friends(); }
    else showToast('Friend request sent to ' + p.display_name + '.');
    onChanged && onChanged.current && onChanged.current.requests && onChanged.current.requests();
  };
  return (
    <>
      <span className="kicker dark">Add Friend</span>
      <div className="field">
        <label htmlFor="friend-search">Search by username</label>
        <div className="field-box">
          <input id="friend-search" value={query} onChange={e => setQuery(e.target.value)} placeholder="At least 2 characters" autoComplete="off" spellCheck="false" autoCapitalize="none"/>
        </div>
      </div>
      {query.trim().length > 0 && query.trim().length < 2 && <FriendEmpty>Type at least 2 characters.</FriendEmpty>}
      {state.loading && <FriendEmpty>Searching…</FriendEmpty>}
      {!state.loading && state.error && <FriendEmpty>{state.error}</FriendEmpty>}
      {!state.loading && !state.error && state.searched && state.items.length === 0 && <FriendEmpty>No matching players.</FriendEmpty>}
      {!state.loading && state.items.length > 0 && (
        <ul className="friend-list">
          {state.items.map(p => (
            <li className="friend-row" key={p.id}>
              <FriendAvatar avatarKey={p.avatar_key}/>
              <span className="friend-info"><span className="friend-name">{p.display_name}</span><span className="friend-user">@{p.username}</span></span>
              <span className="friend-actions"><Button variant="primary" disabled={sent.has(p.id)} onClick={() => send(p)}>{sent.has(p.id) ? 'Sent' : 'Add Friend'}</Button></span>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

// The Host/Join-by-code area: REAL multiplayer rooms (Phase 4). A successful Start hands off to the trusted
// remote authority (Phase 5A/5C) via enterFriendsGame -- see RoomLobby's 'starting' branch below.
function LobbyMemberRow({ member, onKick }) {
  return (
    <li className="friend-row" key={member.profileId}>
      <FriendAvatar avatarKey={member.avatarKey}/>
      <span className="friend-info"><span className="friend-name">{member.displayName}</span><span className="friend-user">@{member.username}</span></span>
      <span className="friend-actions"><Button variant="ghost-dark" onClick={() => onKick(member.profileId)}>Kick</Button></span>
    </li>
  );
}
function RoomLobby({ room, myId, online, entering, busy, onLeave, onClose, onKick, onSetReady, onUpdateSettings, onStart, onRetryEnter }) {
  const isHost = room.hostId === myId;
  const me = room.members.find(m => m.profileId === myId);
  const allReady = room.members.length === 4 && room.members.every(m => m.ready);
  const seats = [0, 1, 2, 3].map(i => room.members.find(m => m.seat === i) || null).map(m => !m ? { name: 'Open seat', state: 'open', label: 'Waiting' } : {
    name: m.displayName, state: m.profileId === myId ? 'you' : 'joined',
    label: (m.profileId === room.hostId ? 'Host · ' : '') + (online.has(m.profileId) ? 'Online' : 'Offline') + ' · ' + (m.ready ? 'Ready' : 'Not ready')
  });
  const copy = () => { try { navigator.clipboard.writeText(room.roomCode); } catch (e) { /* ignore */ } };

  if (room.status === 'starting') {
    return (
      <div className="split">
        <SeatMap seats={seats}/>
        <section className="ticket">
          <span className="kicker dark">Room ready</span>
          <h2 className="ticket-title sm">Everyone&rsquo;s in!</h2>
          <p className="ticket-sub">{entering ? 'Joining the table…' : "Couldn't load the game."}</p>
          {!entering && (
            <div className="cta-col">
              <Button variant="primary" onClick={onRetryEnter}>Try again</Button>
              <Button variant="ghost-dark" onClick={onLeave}>Back to Friends</Button>
            </div>
          )}
        </section>
      </div>
    );
  }
  return (
    <div className="split">
      <SeatMap seats={seats}/>
      <section className="ticket">
        <span className="kicker dark">{room.gameMode === 'friends_tournament' ? 'Friends Tournament · first to ' + room.tournamentTarget : 'Normal Friends game'}</span>
        <button type="button" className="linkish" onClick={copy}>Code: {room.roomCode} (tap to copy)</button>
        {isHost && (
          <div className="lobby-settings">
            <Tabs label="Game mode" options={[['friends_private', 'Normal'], ['friends_tournament', 'Tournament']]} value={room.gameMode}
                  onChange={(v) => onUpdateSettings(v, v === 'friends_tournament' ? (room.tournamentTarget || 5) : null)}/>
            {room.gameMode === 'friends_tournament' && (
              <Tabs label="Target" options={[3, 5, 7, 10].map(v => [String(v), String(v)])} value={String(room.tournamentTarget || 5)}
                    onChange={(v) => onUpdateSettings('friends_tournament', +v)}/>
            )}
          </div>
        )}
        {isHost && room.members.length > 1 && (
          <ul className="friend-list">{room.members.filter(m => m.profileId !== myId).map(m => <LobbyMemberRow key={m.profileId} member={m} onKick={onKick}/>)}</ul>
        )}
        <div className="cta-col">
          <Button variant={me && me.ready ? 'ghost-dark' : 'primary'} disabled={busy} onClick={() => onSetReady(!(me && me.ready))}>{me && me.ready ? 'Not ready' : "I'm ready"}</Button>
          {isHost ? (
            <>
              <Button variant="primary" disabled={!allReady || busy} onClick={onStart} icon={<Icon name="play" size={16}/>}>{allReady ? 'Start room' : 'Waiting for 4 ready players'}</Button>
              <Button variant="ghost-dark" disabled={busy} onClick={onClose}>Close room</Button>
            </>
          ) : (
            <Button variant="ghost-dark" disabled={busy} onClick={onLeave}>Leave room</Button>
          )}
        </div>
      </section>
    </div>
  );
}
function PlayWithFriendsPanel({ user, rooms, showToast, enterFriendsGame }) {
  const [view, setView] = useState('loading'); // 'loading' | 'choose' | 'create' | 'join' | 'lobby'
  const [room, setRoom] = useState(null);
  const [online, setOnline] = useState(() => new Set());
  const [busy, setBusy] = useState(false);
  const [entering, setEntering] = useState(false);
  const [mode, setMode] = useState('friends_private');
  const [target, setTarget] = useState(5);
  const [code, setCode] = useState('');
  const [focus, setFocus] = useState(false);

  const refresh = useCallback(async () => {
    const r = await rooms.fetchCurrentRoom();
    if (r.ok && r.result === 'found') setRoom(r.room);
    else { setRoom(null); setView(v => v === 'lobby' ? 'choose' : v); }
  }, [rooms]);

  useEffect(() => {
    let live = true;
    rooms.fetchCurrentRoom().then(r => {
      if (!live) return;
      if (r.ok && r.result === 'found') { setRoom(r.room); setView('lobby'); } else setView('choose');
    });
    return () => { live = false; };
  }, []);

  useEffect(() => {
    if (view !== 'lobby' || !room || !user) return undefined;
    return rooms.subscribeToRoom(room.id, user.id, { onChange: refresh, onPresence: setOnline });
  }, [view, room && room.id, user && user.id]);

  // Once the room transitions to 'starting' -- observed by ANY of the four players, not only whoever clicked Start
  // -- enter the real trusted-authority game (CLAUDE.md Phase 5C §6). game-init is idempotent, so this also IS the
  // refresh/reconnect recovery path when this effect fires because the mount-time fetchCurrentRoom() above found an
  // already-starting room, not just for the player who just clicked Start.
  useEffect(() => {
    if (!room || room.status !== 'starting' || entering) return undefined;
    let live = true;
    setEntering(true);
    enterFriendsGame(room).then((ok) => { if (live && !ok) setEntering(false); });
    return () => { live = false; };
  }, [room && room.status]);

  const doCreate = async () => {
    setBusy(true); const r = await rooms.createRoom(mode, mode === 'friends_tournament' ? target : null); setBusy(false);
    if (!r.ok) { showToast(r.error); return; }
    if (r.result === 'already_in_a_room') { showToast("You're already in a room."); await refresh(); setView('lobby'); return; }
    setRoom(r.room); setView('lobby');
  };
  const doJoin = async () => {
    if (code.length !== 6) { showToast('Enter a 6-character code'); return; }
    setBusy(true); const r = await rooms.joinRoomByCode(code); setBusy(false);
    if (!r.ok) { showToast(r.error); return; }
    const messages = { invalid_code: 'That code does not match any room.', closed: 'That room is closed.', full: 'That room is full.', already_in_another_room: "You're already in another room." };
    if (messages[r.result]) { showToast(messages[r.result]); return; }
    setRoom(r.room); setView('lobby');
  };
  const doLeave = async () => {
    setBusy(true); const r = await rooms.leaveRoom(); setBusy(false);
    if (!r.ok) { showToast(r.error); return; }
    // game_active: leave_room() deliberately refused -- a genuinely active game still depends on this room for
    // every client's reconnect-after-reload path (CLAUDE.md Phase 5C §7), so it must never be torn down just
    // because THIS client's own entry attempt failed. Stay on the lobby/dead-end screen (refreshed) rather than
    // pretending we left -- "Try again" (reconnect) is the correct action here, not "Back to Friends".
    if (r.result === 'game_active') { showToast('Your game is still in progress -- try reconnecting instead.'); await refresh(); return; }
    rooms.unsubscribeFromRoom(); setRoom(null); setEntering(false); setCode(''); setView('choose');
  };
  // Non-destructive recovery from a failed entry (CLAUDE.md Phase 5C.2): re-run the exact same idempotent
  // enterFriendsGame() the effect above used, without leaving/closing the room. "Back to Friends" is still offered
  // alongside this as an explicit opt-in fallback, but it must never be the ONLY option -- leaving a 'starting'
  // room closes it outright if this player is the host (leave_room()), which would strand the other three
  // players mid-game over what is typically just a transient read hiccup.
  const retryEnter = () => {
    if (!room || entering) return;
    setEntering(true);
    enterFriendsGame(room).then((ok) => { if (!ok) setEntering(false); });
  };
  const doClose = async () => {
    setBusy(true); const r = await rooms.closeRoom(); setBusy(false);
    if (!r.ok) { showToast(r.error); return; }
    rooms.unsubscribeFromRoom(); setRoom(null); setView('choose');
  };
  const doKick = async (targetId) => { const r = await rooms.kickMember(targetId); if (!r.ok) { showToast(r.error); return; } if (r.room) setRoom(r.room); };
  const doSetReady = async (ready) => { const r = await rooms.setReady(ready); if (!r.ok) { showToast(r.error); return; } if (r.room) setRoom(r.room); };
  const doUpdateSettings = async (m, t) => { const r = await rooms.updateRoomSettings(m, t); if (!r.ok) { showToast(r.error); return; } if (r.room) setRoom(r.room); };
  const doStart = async () => {
    setBusy(true); const r = await rooms.startRoom(); setBusy(false);
    if (!r.ok) { showToast(r.error); return; }
    if (r.result === 'not_ready') { showToast('Not everyone is ready yet.'); return; }
    // 'started' and 'already_started' both just mean the room is (now) 'starting' -- a fresh fetch picks that up and
    // the effect above takes it from there, exactly the same path a reconnecting/late client uses.
    await refresh();
  };

  if (view === 'loading') return <FriendEmpty>Loading&hellip;</FriendEmpty>;
  if (view === 'lobby' && room) {
    return <RoomLobby room={room} myId={user && user.id} online={online} entering={entering} busy={busy}
                       onLeave={doLeave} onClose={doClose} onKick={doKick} onSetReady={doSetReady} onUpdateSettings={doUpdateSettings} onStart={doStart} onRetryEnter={retryEnter}/>;
  }
  if (view === 'create') return (
    <section className="ticket ticket-narrow">
      <span className="kicker dark">Choose the game</span>
      <div className="lobby-settings">
        <Tabs label="Game mode" options={[['friends_private', 'Normal'], ['friends_tournament', 'Tournament']]} value={mode} onChange={setMode}/>
        {mode === 'friends_tournament' && <Tabs label="Target" options={[3, 5, 7, 10].map(v => [String(v), 'First to ' + v])} value={String(target)} onChange={v => setTarget(+v)}/>}
      </div>
      <div className="cta-col">
        <Button variant="primary" onClick={doCreate} disabled={busy} icon={<Icon name="play" size={16}/>}>Create room</Button>
        <Button variant="ghost-dark" onClick={() => setView('choose')}>Cancel</Button>
      </div>
    </section>
  );
  if (view === 'join') return (
    <section className="ticket ticket-narrow">
      <span className="kicker dark">Six letters or numbers</span>
      <h2 className="ticket-title sm">Enter the room code</h2>
      <div className="code-field">
        <CodeTiles value={code} active={focus}/>
        <input aria-label="Room code" value={code} autoFocus autoCapitalize="characters" autoComplete="off" spellCheck="false" maxLength={6}
               onFocus={() => setFocus(true)} onBlur={() => setFocus(false)}
               onChange={e => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6))}
               onKeyDown={e => { if (e.key === 'Enter') doJoin(); }}/>
      </div>
      <div className="cta-col">
        <Button variant="primary" onClick={doJoin} disabled={busy}>Join table</Button>
        <Button variant="ghost-dark" onClick={() => setView('choose')}>Cancel</Button>
      </div>
    </section>
  );
  return (
    <div className="fan fan-pair" role="group" aria-label="Room options">
      <FaceCard index={0} rank="K" suit="D" tilt={-4} kicker="You are the host" title="Host" desc="Create a real room and share the six-letter code." cta="Create room" onClick={() => setView('create')}/>
      <FaceCard index={1} rank="J" suit="C" tilt={4} kicker="Got a code?" title="Join" desc="Enter your friend's code and take a seat." cta="Enter code" onClick={() => setView('join')}/>
    </div>
  );
}
function FriendsScreen({ back, showToast, user, friends, rooms, enterFriendsGame }) {
  const [tab, setTab] = useState('friends'); // 'friends' | 'requests' | 'add' | 'play'
  const guest = !user || user.provider === 'guest';
  // lets sibling panels ask each other to refresh (e.g. accepting a request should refresh the friends list too)
  const bridge = useRef({});
  const tabs = [['friends', 'My Friends'], ['requests', 'Requests'], ['add', 'Add Friend'], ['play', 'Play with Friends']];
  return (
    <Frame title="Friends" kicker="Social" onBack={back} wide bodyClass="center">
      <div className="friend-panel">
        <div className="friend-tabs-wrap"><Tabs className="tabs-dark" label="Friends sections" options={tabs} value={tab} onChange={setTab}/></div>
        {tab === 'play' ? (
          guest ? <section className="ticket ticket-narrow"><p className="ticket-sub" style={{ textAlign: 'center' }}>Sign in to play with friends.</p></section>
                : <PlayWithFriendsPanel user={user} rooms={rooms} showToast={showToast} enterFriendsGame={enterFriendsGame}/>
        ) : guest ? (
          <section className="ticket ticket-narrow"><p className="ticket-sub" style={{ textAlign: 'center' }}>Sign in to add friends and see friend requests.</p></section>
        ) : (
          <section className="ticket">
            {tab === 'friends' && <MyFriendsPanel friends={friends} showToast={showToast} onChanged={bridge}/>}
            {tab === 'requests' && <FriendRequestsPanel friends={friends} showToast={showToast} onChanged={bridge}/>}
            {tab === 'add' && <AddFriendPanel friends={friends} showToast={showToast} onChanged={bridge}/>}
          </section>
        )}
      </div>
    </Frame>
  );
}

// ============== Settings ==============
function SettingsScreen({ back, settings, setSettings }) {
  const set = (patch) => setSettings({ ...settings, ...patch });
  const speed = settings.aiSpeedMs;
  return (
    <Frame title="Settings" kicker="House rules" onBack={back}>
      <div className="settings">
        <section className="sheet" aria-labelledby="s-table">
          <h2 id="s-table" className="sheet-h"><Pip suit="S" size={14}/>Table</h2>
          <Tabs label="Graphics quality" options={[['auto', 'Auto'], ['low', 'Low'], ['medium', 'Medium'], ['high', 'High']]} value={settings.quality || 'auto'} onChange={v => set({ quality: v })}/>
          <p className="sheet-note">Lower quality uses fewer particles and lights for smoother play on older phones.</p>
          <Switch id="sw-fx" label="Table effects" hint="WebGL lighting, card flights and burns" value={settings.fx !== false} onChange={v => set({ fx: v })}/>
          <Switch id="sw-deal" label="Deal animation" hint="Cards are dealt out at the start of a match" value={settings.deal !== false} onChange={v => set({ deal: v })}/>
          <Switch id="sw-anim" label="Animations" hint="Menu and table motion" value={settings.animations} onChange={v => set({ animations: v })}/>
          <Switch id="sw-rm" label="Reduced motion" hint="No movement or flashing; the game stays fully readable" value={settings.reducedMotion} onChange={v => set({ reducedMotion: v })}/>
        </section>
        <section className="sheet" aria-labelledby="s-bots">
          <h2 id="s-bots" className="sheet-h"><Pip suit="D" size={14}/>Bots</h2>
          <label className="slider" htmlFor="ai-speed">
            <span className="sw-label">Bot tempo</span>
            <span className="sl-val tnum">{speed} ms per move</span>
            <input id="ai-speed" type="range" min="200" max="1800" step="100" value={speed} onChange={e => set({ aiSpeedMs: +e.target.value })} style={{ '--p': ((speed - 200) / 1600 * 100) + '%' }}/>
            <span className="sl-ends"><span>Fast</span><span>Slow</span></span>
          </label>
        </section>
        <section className="sheet" aria-labelledby="s-snd">
          <h2 id="s-snd" className="sheet-h"><Pip suit="H" size={14}/>Sound</h2>
          <Switch id="sw-snd" label="Sound effects" value={settings.sound} onChange={v => set({ sound: v })}/>
        </section>
        <section className="sheet" aria-labelledby="s-legal">
          <h2 id="s-legal" className="sheet-h"><Pip suit="C" size={14}/>Legal</h2>
          <UI.LegalLinks paper testId="legal-links-settings"/>
        </section>
      </div>
    </Frame>
  );
}

// ============== Results ==============
// An AUTHORITATIVE game record (game_results + game_result_players from the database) shaped like the engine's final state, so the
// same results screen can show it. Everything here is read from the server's record; nothing is recomputed.
function recordToResults(rec, myId) {
  const bySeat = {}; rec.players.forEach(p => { bySeat[p.seat] = p; });
  const prof = Object.fromEntries(rec.profiles.map(x => [x.id, x]));
  const players = [0, 1, 2, 3].map(seat => { const p = bySeat[seat] || {}, pr = p.player_id ? prof[p.player_id] : null;
    return { seat, id: p.player_id || 'bot' + seat, name: pr ? (pr.display_name || pr.username) : 'Bot', kind: p.player_id ? 'human' : 'ai-' + (p.bot_level || 'medium'), avatar: pr ? pr.avatar_key : null, hand: [], faceUp: [], faceDown: [], phase: 'OUT' }; });
  const order = rec.players.filter(p => p.place != null).sort((a, b) => a.place - b.place).map(p => p.seat);
  const humanSeat = Math.max(0, rec.players.findIndex(p => p.player_id === myId));
  const fs = { players, status: 'FINISHED', finishingOrder: order, winner: order[0], firstFinisher: order[0], overallWinnerSeat: order[0],
               sheetHeadSeat: order.length === 4 ? order[3] : null, duelWinnerSeat: order.length === 4 ? order[2] : null };
  return { finalState: fs, humanSeat, place: order.indexOf(humanSeat) + 1, sheetHead: fs.sheetHeadSeat === humanSeat, authoritativeGameId: rec.game.game_id, ranked: rec.game.ranked };
}

function ResultsScreen({ lastResults, setScreen, startSinglePlayer, gameSession, settings, auth, user }) {
  const { finalState: fs, humanSeat } = lastResults;
  const signedIn = !!(user && user.provider !== 'guest');
  const tr = UI.useTrophyResult(auth, lastResults.authoritativeGameId, signedIn && !!lastResults.authoritativeGameId);
  const remaining = (p) => p.hand.length + p.faceUp.length + p.faceDown.length;
  // authoritative finishing order from the engine (1st out ... last = SheetHead); a legacy first-out game falls back to "winner, then fewest cards"
  const order = fs.sheetHeadSeat != null && fs.finishingOrder && fs.finishingOrder.length === 4
    ? fs.finishingOrder
    : fs.players.slice().sort((a, b) => (a.seat === fs.winner ? -1 : b.seat === fs.winner ? 1 : remaining(a) - remaining(b))).map(p => p.seat);
  const sheetHead = fs.sheetHeadSeat, myPlace = order.indexOf(humanSeat) + 1, iAmSheetHead = sheetHead === humanSeat;
  const champ = fs.overallWinnerSeat != null ? fs.overallWinnerSeat : order[0];       // first player out, always
  const nm = (seat) => fs.players[seat].name;
  const figures = sheetHead != null
    ? [{ seat: champ, name: nm(champ), isYou: champ === humanSeat, role: 'winner', tag: 'Winner' },
       { seat: sheetHead, name: nm(sheetHead), isYou: iAmSheetHead, role: 'loser', tag: 'SheetHead' }]
    : [{ seat: fs.winner, name: nm(fs.winner), isYou: fs.winner === humanSeat, role: 'winner', tag: 'Out first' }];
  const title = iAmSheetHead ? 'You are the SheetHead' : myPlace === 1 ? 'Victory' : `You finished ${PR.ordinal(myPlace)}`;
  const sub = iAmSheetHead ? `${nm(champ)} won by going out first. You were left holding the cards.`
    : sheetHead != null ? `${nm(sheetHead)} is left holding the cards: the SheetHead.` : `${nm(fs.winner)} got rid of every card first.`;
  const Finale = UI.Finale;
  return (
    <div className="results">
      <section className={cx('ticket results-ticket', myPlace === 1 && 'won')} aria-labelledby="res-h" data-testid="results">
        <span className="kicker dark">{sheetHead != null ? 'Game over' : 'Match over'}</span>
        <h1 id="res-h" className="ticket-title" data-place={myPlace} data-sheethead={iAmSheetHead ? '1' : '0'}>{title}</h1>
        <p className="ticket-sub">{sub}</p>
        {Finale && <Finale figures={figures} settings={settings} kicker={sheetHead != null ? 'Winner and SheetHead' : 'Winner'} />}
        {tr.status === 'found' && <UI.TrophyResult row={tr.row} settings={settings}/>}
        {tr.status === 'pending' && <p className="practice-note" role="status" data-testid="trophy-pending">Settling your Trophies…</p>}
        {tr.status === 'none' && (
          <p className="practice-note" data-testid="practice-note">
            {!signedIn ? 'Sign in to earn Trophies in ranked games.' : lastResults.authoritativeGameId ? 'No Trophies were at stake in this game.' : 'Practice game: no Trophies at stake. Ranked online games count.'}
          </p>
        )}
        <ol className="standings" aria-label="Final standings" data-testid="finishing-order">
          {order.map((seat, i) => {
            const p = fs.players[seat], last = seat === sheetHead;
            return (
              <li key={seat} data-seat={seat} className={cx(seat === humanSeat && 'me', i === 0 && 'first', last && 'sheet')}>
                <span className="st-pos">{i + 1}</span>
                <span className="st-name">{seat === humanSeat ? `${p.name} (you)` : p.name}</span>
                <span className="st-left">{last ? 'SheetHead' : i === 0 && sheetHead != null ? 'Out first' : sheetHead != null ? 'Out ' + PR.ordinal(i + 1) : i === 0 ? 'Out first' : `${remaining(p)} cards left`}</span>
              </li>
            );
          })}
        </ol>
        <div className="cta-row">
          <Button variant="primary" onClick={() => startSinglePlayer(gameSession?.difficulty || 'medium')} icon={<Icon name="play" size={16}/>}>Rematch</Button>
          <Button variant="ghost-dark" onClick={() => setScreen(SCREENS.LOBBY)}>Back to lobby</Button>
        </div>
      </section>
    </div>
  );
}
// ============== Tournament ==============
function TournamentSetupScreen({ back, tourneyVenue, startTournament }) {
  const [target, setTarget] = useState(5);
  const [diff, setDiff] = useState('medium');
  const solo = tourneyVenue !== 'online';
  return (
    <Frame title="Tournament" kicker={solo ? 'Solo' : 'Online'} onBack={back} bodyClass="center">
      <div className="tsetup">
        <section className="ticket">
          <span className="kicker dark">A series of games</span>
          <h2 className="ticket-title sm">Set up your tournament</h2>
          <p className="t-lede">Every game win is one point. The first player to reach the target wins the whole tournament.</p>
          <span className="t-label" id="tt-label">Points to win</span>
          <div className="target-tiles" role="radiogroup" aria-labelledby="tt-label">
            {TN.TARGETS.map(n => (
              <button key={n} type="button" role="radio" aria-checked={target === n} className="target-tile" data-target={n} onClick={() => setTarget(n)}>
                <b>{n}</b><span>First to {n}</span>
              </button>
            ))}
          </div>
          {solo && (<><span className="t-label">Bot difficulty</span>
            <Tabs label="Bot difficulty" options={[['easy', 'Easy'], ['medium', 'Medium'], ['hard', 'Hard'], ['expert', 'Expert']]} value={diff} onChange={setDiff}/></>)}
          <ul className="t-rules">
            <li><i>1</i><span>Game 1 is a normal SheetHead game with a random starting player.</span></li>
            <li><i>2</i><span>From game 2 the <b>winner of the previous game starts</b>.</span></li>
            <li><i>3</i><span>That winner also gets an <b>advantage</b>: 6 private cards, and they choose 3 as their FACE UP (their 40-second clock is already running).</span></li>
            <li><i>4</i><span>Between games the standings show for 5 seconds, then the next game starts on its own.</span></li>
          </ul>
          <div className="t-actions cta-col">
            <Button variant="primary" onClick={() => startTournament({ venue: tourneyVenue, difficulty: solo ? diff : 'hard', target })} icon={<Icon name="play" size={16}/>}>Start tournament · First to {target}</Button>
          </div>
        </section>
      </div>
    </Frame>
  );
}

// Pure -- also reused by FriendsTournamentScreen below, fed the server's own sanitized TN.viewFor(T) rather than a
// locally-owned T (both shapes carry the same fields this function reads: id/targetWins/gameNumber/players/
// scoresByPlayer/previousWinnerId -- see src/tournament.js's viewFor).
function tournamentInfo(T) {
  return {
    id: T.id, targetWins: T.targetWins, gameNumber: T.gameNumber, names: T.players.map(p => p.name), scores: T.players.map(p => T.scoresByPlayer[p.id]),
    previousWinnerSeat: T.gameNumber > 1 ? TN.seatOf(T, T.previousWinnerId) : null
  };
}

// Phase 5E -- Friends Tournament multiplayer. ONE persisted RemoteGameAdapter/gameId for the WHOLE tournament
// (never one per round, per CLAUDE.md Phase 5E). The server projects tournament state INSIDE the safe view as
// {tournament, game} (server/game-authority-server.mjs's computeSafeViewsForEnvelope); this shim republishes only
// the `game` half through the exact adapter surface GameScreen/wrapRemoteAuthority already expect, so GameScreen is
// reused completely unchanged for an in-progress round. `dispose` is deliberately a no-op here: GameScreen calls it
// every time a round ends and FriendsTournamentScreen below switches away to the between-rounds screen, but the
// underlying connection/channel must survive the whole tournament -- only leaveFriendsTournament tears down the
// real adapter.
function makeRoundSubAdapter(adapter) {
  const project = (full) => (full && full.view && full.view.game)
    ? { seat: full.seat, actionSeq: full.actionSeq, view: full.view.game, lastEvent: full.lastEvent, connectionState: full.connectionState }
    : null;
  return {
    subscribe: (listener) => adapter.subscribe((full) => { const p = project(full); if (p) listener(p); }),
    getSnapshot: () => project(adapter.getSnapshot()) || adapter.getSnapshot(),
    submitIntent: (move) => adapter.submitIntent(move),
    retryLastIntent: () => adapter.retryLastIntent(),
    onConnectionChange: (l) => adapter.onConnectionChange(l),
    dispose: () => {}
  };
}

// Routes ONE persisted Friends Tournament game between its three presentations -- an active round (GameScreen,
// fully reused), the authoritative between-rounds intermission, and the final result -- purely by watching the SAME
// adapter's ongoing view.tournament.status. Never starts a round itself: onNextGame is a deliberate no-op (the
// between-round countdown shown is presentation-only; the server alone decides when the next round begins).
function FriendsTournamentScreen(ctx) {
  const { gameSession, settings, leaveFriendsTournament, rooms } = ctx;
  const { remoteAdapter } = gameSession;
  const [envelope, setEnvelope] = useState(() => gameSession.state);
  useEffect(() => remoteAdapter.subscribe((full) => { if (full.view) setEnvelope(full.view); }), [remoteAdapter]);
  const subAdapter = useMemo(() => makeRoundSubAdapter(remoteAdapter), [remoteAdapter]);
  const T = envelope.tournament;
  // Stale-room fix (same reasoning as onGameEnd's normal-Friends branch above): every client reactively observes
  // T.status becoming FINISHED here, which is exactly as unambiguous a "this room is over" signal as a normal
  // game's FINISHED state -- so release the room the same way, once, best-effort.
  const releasedRef = useRef(false);
  useEffect(() => {
    if (T.status === 'FINISHED' && !releasedRef.current) { releasedRef.current = true; rooms.leaveRoom(); }
  }, [T.status]);
  if (T.status === 'IN_GAME' && envelope.game) {
    const roundSession = {
      state: envelope.game, humanSeat: gameSession.humanSeat, mode: 'friends', remoteAdapter: subAdapter,
      tournament: tournamentInfo(T), key: 'tourney-round-' + T.gameNumber, recordStats: false
    };
    // onGameEnd is a no-op here on purpose: a round finishing is entirely reflected by the NEXT envelope push
    // (tournament.status leaving IN_GAME), which this component reacts to above -- never by client-decided scoring.
    return <GameScreen {...ctx} gameSession={roundSession} onGameEnd={() => {}} key={roundSession.key}/>;
  }
  if (T.status === 'FINISHED') {
    return <TournamentVictoryScreen tournament={T} tourneyVenue="online" setScreen={ctx.setScreen} leaveTournament={leaveFriendsTournament} settings={settings} friendsMode humanSeat={gameSession.humanSeat}/>;
  }
  return <TournamentBoardScreen tournament={T} onNextGame={() => {}} leaveTournament={leaveFriendsTournament} humanSeat={gameSession.humanSeat}/>;
}

// `humanSeat` (not `kind === 'human'`) decides the "(you)" label: SOLO has exactly one human seat (always 0, see
// startTournament) so the two were equivalent there, but a Friends tournament has FOUR real human players --
// `kind` alone can never say which one is the viewer (CLAUDE.md Phase 5E finding, caught by the real 4-browser E2E:
// every seat's standings row showed "(you)" simultaneously before this fix).
function StandingRows({ T, highlightId, newPoint, humanSeat }) {
  const rows = TN.standings(T);
  return (
    <ol className="tb-rows" aria-label="Tournament standings">
      {rows.map((r, i) => {
        const av = PR.avatarFor({ seat: r.seat });
        const won = highlightId === r.id;
        return (
          <li key={r.id} className={cx('tb-row', won && 'winner')} data-player={r.id} data-points={r.points}>
            <span className="tb-pos">{i + 1}</span>
            <span className="tb-av" style={{ '--hue': av.hue }} aria-hidden="true">{av.glyph}</span>
            <span className="tb-name">{r.name}{r.seat === humanSeat ? ' (you)' : ''}
              {won && <small>{newPoint ? 'Won game ' + T.gameNumber : 'Champion'}</small>}
              <span className="tb-pips" aria-hidden="true">{Array.from({ length: T.targetWins }).map((_, k) => <i key={k} className={cx(k < r.points && 'on', won && newPoint && k === r.points - 1 && 'new')}/>)}</span>
            </span>
            <span className="tb-pts" aria-label={r.points + ' points'}>{r.points}{won && newPoint && <em className="tb-plus" aria-hidden="true">+1</em>}</span>
          </li>
        );
      })}
    </ol>
  );
}

function TournamentBoardScreen({ tournament: T, onNextGame, leaveTournament, humanSeat = 0 }) {
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    const h = setInterval(() => { const n = Date.now(); setNow(n); if (TN.isNextGameDue(T, n)) onNextGame(); }, 150);
    return () => clearInterval(h);
  }, [T]);
  const secs = TN.secondsUntilNext(T, now);
  const frac = Math.max(0, Math.min(1, (T.nextGameAt - now) / TN.INTERMISSION_MS));
  const winner = T.players.find(p => p.id === T.previousWinnerId);
  return (
    <div className="tboard">
      <section className="ticket tboard-ticket" aria-labelledby="tb-h">
        <div className="tb-head">
          <span className="kicker dark">Game {T.gameNumber} complete</span>
          <h1 id="tb-h" className="tb-title">Tournament · First to {T.targetWins}</h1>
          <div className="tb-sub">{winner ? winner.name + ' wins game ' + T.gameNumber : ''}</div>
        </div>
        <StandingRows T={T} highlightId={T.previousWinnerId} newPoint humanSeat={humanSeat}/>
        <div className="tb-count" data-testid="tournament-countdown">
          <div className="cd-label">Next game starts in</div>
          <span className="cd-num tick" key={secs} data-seconds={secs} aria-hidden="true">{secs}</span>
          <div className="tb-bar" aria-hidden="true"><i style={{ width: (frac * 100) + '%' }}/></div>
          <span className="sr-only" role="status" aria-live="polite">Next game starts in {secs} seconds</span>
        </div>
        <p className="tb-hint"><b>{winner ? winner.name : ''}</b> starts game {T.gameNumber + 1} and chooses 3 of 6 private cards as their FACE UP.</p>
        <div className="tb-leave"><Button variant="ghost-dark" onClick={leaveTournament}>Leave tournament</Button></div>
      </section>
    </div>
  );
}

function TournamentVictoryScreen({ tournament: T, tourneyVenue, setScreen, leaveTournament, settings, friendsMode, humanSeat = 0 }) {
  const host = useRef(null);
  useEffect(() => {
    const F = window.SH.fx;
    if (!F || !F.Celebration) return undefined;
    F.configure({ quality: settings.quality || 'auto', reducedMotion: !!settings.reducedMotion || settings.animations === false, enabled: settings.fx !== false });
    const c = F.resolve();
    if (!c.enabled || c.reducedMotion || !F.available()) return undefined;
    let cel = null, dead = false;
    F.Celebration.create(host.current).then(x => { if (dead) x.destroy(); else cel = x; }).catch(() => { /* the ticket stands on its own */ });
    return () => { dead = true; if (cel) cel.destroy(); };
  }, []);
  const rows = TN.standings(T);
  const champ = rows[0];
  return (
    <div className="tvic">
      <div className="tvic-gl" ref={host} aria-hidden="true"/>
      <section className="ticket tvic-ticket" aria-labelledby="tv-h" data-testid="tournament-victory">
        <div className="tvic-kicker">Tournament champion</div>
        <h1 id="tv-h" className="tvic-name" data-winner={champ.id}>{champ.name}{champ.seat === humanSeat ? ' (you)' : ''}</h1>
        {UI.Finale && <UI.Finale settings={settings} kicker="Champion"
          figures={[{ seat: champ.seat, name: champ.name, isYou: champ.seat === humanSeat, role: 'winner', tag: 'Champion' }]
            .concat(rows.slice(1).map(r => ({ seat: r.seat, name: r.name, isYou: r.seat === humanSeat, role: 'loser', tag: 'SheetHead' })))} />}
        <p className="tvic-line">{champ.seat === humanSeat ? 'You reached' : 'Reached'} {T.targetWins} game wins first.</p>
        <div className="tvic-stats">
          <div><b>{T.targetWins}</b><span>Target score</span></div>
          <div><b>{T.history.length}</b><span>Games played</span></div>
          <div><b>{champ.wins}</b><span>{champ.seat === humanSeat ? 'Your wins' : 'Winner\'s wins'}</span></div>
        </div>
        <StandingRows T={T} highlightId={champ.id} humanSeat={humanSeat}/>
        <div className="cta-row t-actions">
          {/* Phase 5E: no automatic/offered rematch for a Friends tournament in this phase -- show the result and stop. */}
          {!friendsMode && <Button variant="primary" onClick={() => setScreen(SCREENS.TOURNEY_SETUP)} icon={<Icon name="play" size={16}/>}>Play another tournament</Button>}
          <Button variant="ghost-dark" onClick={leaveTournament}>Return to main menu</Button>
        </div>
      </section>
    </div>
  );
}

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