// ============================================================
// Andre — Screen components (Splash, Menu, Game, Results)
// ============================================================

const { useState, useEffect, useRef, useCallback } = React;

// ------------------------------------------------------------
// Icons
// ------------------------------------------------------------
const Icon = {
  sound: () => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" width="20" height="20">
      <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
      <path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
      <path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
    </svg>
  ),
  mute: () => (
    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" width="20" height="20">
      <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5" />
      <line x1="23" y1="9" x2="17" y2="15" />
      <line x1="17" y1="9" x2="23" y2="15" />
    </svg>
  ),
  star:  () => (
    <svg className="star-glyph" viewBox="0 0 24 24">
      {/* Sims-style plumbob — vertical diamond with a band + facet highlight */}
      <path d="M12 2 L20 12 L12 22 L4 12 Z"
            fill="currentColor" stroke="#2A2438" strokeWidth="2" strokeLinejoin="round"/>
      <path d="M12 2 L8 12 L12 9 Z" fill="rgba(255,255,255,0.55)"/>
      <path d="M5 12 L19 12" stroke="#2A2438" strokeWidth="1.4" strokeLinecap="round" opacity="0.5"/>
    </svg>
  ),
  wall: () => (
    <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
      <rect x="3" y="3" width="8" height="8" rx="1.5"/><rect x="13" y="3" width="8" height="8" rx="1.5"/>
      <rect x="3" y="13" width="8" height="8" rx="1.5"/><rect x="13" y="13" width="8" height="8" rx="1.5"/>
    </svg>
  ),
};

// ------------------------------------------------------------
// Brand / TopBar
// ------------------------------------------------------------
const TopBar = ({ muted, onToggleMute, onHome }) => (
  <div className="topbar">
    <div className="brand" onClick={onHome}>
      <div className="brand-mark">
        <Icon.wall />
      </div>
      <div className="brand-name">Andre</div>
    </div>
    <button className={`icon-btn ${muted ? 'muted' : ''}`} onClick={onToggleMute} aria-label="Toggle sound">
      {muted ? <Icon.mute /> : <Icon.sound />}
    </button>
  </div>
);

// ------------------------------------------------------------
// Splash screen — slim hero + looping gameplay demo
// ------------------------------------------------------------
const Splash = ({ onStart }) => {
  // Rotating quip — first one is random so a returning player doesn't always
  // see the same line on cold load. Cycles forward every ~5.5s while they
  // linger on the splash. ANDRE_QUOTES is defined further down; hoisting
  // wouldn't change behavior since this component runs at render time.
  const [quoteIdx, setQuoteIdx] = useState(
    () => Math.floor(Math.random() * ANDRE_QUOTES.length)
  );
  useEffect(() => {
    const id = setInterval(() => {
      setQuoteIdx(i => (i + 1) % ANDRE_QUOTES.length);
    }, 5500);
    return () => clearInterval(id);
  }, []);

  return (
    <div className="stack scene-enter" style={{ gap: 12 }}>
      <div className="block" style={{ textAlign: 'center', padding: '22px 18px' }}>
        <div className="display" style={{ fontSize: 56, color: 'var(--violet)', marginBottom: 4, textShadow: '4px 4px 0 var(--ink)', lineHeight: 1 }}>
          ANDRE
        </div>
        <div className="h3" style={{ color: 'var(--ink-soft)', fontFamily: 'var(--font-ui)', fontWeight: 700, letterSpacing: '0.16em', fontSize: 11, textTransform: 'uppercase', marginBottom: 16, marginTop: 8 }}>
          Trace a path. Save Andre.
        </div>
        <div style={{ display: 'flex', justifyContent: 'center', margin: '4px 0 18px' }}>
          <AndreChatter mood="cheer" size={64} message={ANDRE_QUOTES[quoteIdx]} />
        </div>
        <button className="btn btn-primary btn-lg btn-block" onClick={onStart}>Let's play</button>
      </div>

      <DemoTour />
    </div>
  );
};

// ------------------------------------------------------------
// DemoTour — a mini looping animation of the gameplay loop. Replaces
// the four static rule cards. Three phases on a single ~10s cycle:
//   1. memorize — walls + diamonds are visible
//   2. plan     — walls hide, the path draws bottom-to-top
//   3. execute  — walls reveal, runner dot traces the path,
//                 collects diamonds, and breaches the top row
// Reuses the in-game .cell / .wall / .star / .ghost / .start-mark
// classes so the demo reads as a faithful preview of the real game.
// ------------------------------------------------------------
const DEMO_BOARD = [
  [0,0,0,0,0],
  [0,0,0,3,0], // diamond on row 1
  [1,0,0,0,1], // walls flank row 2
  [0,0,3,0,0], // diamond mid-board
  [0,0,1,0,0], // wall blocks middle column
  [1,0,0,0,0], // wall in start row neighbourhood
  [0,0,0,0,0],
];
const DEMO_PATH = [
  {x:1,y:6},{x:1,y:5},{x:1,y:4},{x:1,y:3},{x:2,y:3},
  {x:2,y:2},{x:2,y:1},{x:3,y:1},{x:3,y:0},
];
// `dur` is the total time the phase occupies in the loop; `hold` is the
// trailing window where the within-phase animation has already finished and
// the frame is held still before transitioning. localT below saturates at 1
// during that hold, so consumers (planRevealCount, runnerIdx) naturally rest
// at their end-state without each needing its own pause logic.
const DEMO_PHASES = [
  { key: 'memorize', dur: 4000, hold: 1, caption: 'Memorize the red bricks — They disappear' },
  { key: 'plan',     dur: 4000, hold: 1000, caption: 'The maze fades to empty — plot a path up, dodge bricks, grab diamonds' },
  { key: 'execute',  dur: 4000, hold: 1000, caption: 'Run your path — Don\'t crash!' },
  { key: 'won',      dur: 4000, hold: 200, caption: 'You made it — here\'s how it scored' },
];
const DEMO_TOTAL = DEMO_PHASES.reduce((s, p) => s + p.dur, 0);

// Sample run points for the won-phase breakdown. Mirrors the live scoring
// rules in app.jsx (`executeRun` → +50 breach, +25/star, −3/move, −20/back),
// so changing the demo path or board recomputes the totals automatically.
const DEMO_MOVES = DEMO_PATH.length - 1;
const DEMO_STARS = DEMO_PATH.filter(p => DEMO_BOARD[p.y][p.x] === 3).length;
const DEMO_BREACH_PTS = 50;
const DEMO_STAR_PTS = DEMO_STARS * 25;
const DEMO_MOVE_PEN = DEMO_MOVES * 3;
const DEMO_TOTAL_PTS = Math.max(0, DEMO_BREACH_PTS + DEMO_STAR_PTS - DEMO_MOVE_PEN);

const DemoTour = () => {
  const reduced = typeof window !== 'undefined' &&
    window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
  const [elapsed, setElapsed] = useState(0);
  // pin.idx == null → auto-loop; otherwise we hold on that phase. `key` bumps
  // every click so re-clicking the same pip replays the phase from t=0.
  const [pin, setPin] = useState({ idx: null, key: 0 });
  const stateRef = useRef({ autoStart: 0, pinned: null, pinStart: 0 });

  // Mirror pin → ref so the long-lived rAF loop sees the latest pin without
  // having to be torn down and rebuilt on every click.
  useEffect(() => {
    stateRef.current.pinned = pin.idx;
    stateRef.current.pinStart = performance.now();
  }, [pin.key]);

  // rAF loop drives the whole demo from a single elapsed counter so phase
  // transitions and intra-phase progress (path drawing, runner steps) stay
  // in lockstep without overlapping timeouts. When pinned, `elapsed` is
  // clamped to the pinned phase so the within-phase animation plays once
  // and rests at its final state.
  useEffect(() => {
    if (reduced) return;
    stateRef.current.autoStart = performance.now();
    let raf;
    const tick = (now) => {
      const s = stateRef.current;
      let e;
      if (s.pinned != null) {
        let phaseStart = 0;
        for (let i = 0; i < s.pinned; i++) phaseStart += DEMO_PHASES[i].dur;
        // Clamp to dur-1 (not dur) so we stay strictly within the pinned
        // phase. The phase resolver uses `elapsed < phaseStart + dur`, so
        // hitting `dur` exactly would tip us into the next phase at t=0.
        const local = Math.min(now - s.pinStart, DEMO_PHASES[s.pinned].dur - 1);
        e = phaseStart + local;
      } else {
        e = (now - s.autoStart) % DEMO_TOTAL;
      }
      setElapsed(e);
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [reduced]);

  let phaseIdx = 0, phaseStart = 0;
  for (let i = 0; i < DEMO_PHASES.length; i++) {
    if (elapsed < phaseStart + DEMO_PHASES[i].dur) { phaseIdx = i; break; }
    phaseStart += DEMO_PHASES[i].dur;
  }
  const phase = DEMO_PHASES[phaseIdx];
  const animDur = Math.max(1, phase.dur - (phase.hold || 0));
  const localT = Math.min(1, (elapsed - phaseStart) / animDur);

  const showWalls = phase.key === 'memorize' || phase.key === 'execute' || phase.key === 'won';
  // Pad the divisor so the last cell finishes ~85% through the plan phase
  // and rests for a beat before walls reveal.
  const planRevealCount = phase.key === 'plan'
    ? Math.min(DEMO_PATH.length, Math.floor(localT * (DEMO_PATH.length + 1)))
    : (phase.key === 'execute' || phase.key === 'won' ? DEMO_PATH.length : 0);
  const runnerIdx = phase.key === 'execute'
    ? Math.min(DEMO_PATH.length - 1, Math.floor(localT * DEMO_PATH.length))
    : phase.key === 'won' ? DEMO_PATH.length - 1 : -1;
  // Trail catch-up: cells light up the moment the runner-dot's CSS transition
  // completes (i.e. the dot has visually landed). animDur/DEMO_PATH.length is
  // the per-cell window; 280ms matches the .demo-runner CSS transition. Same
  // model as the real game's `setRunStep(i + 1)` post-arrival.
  const cellProgress = phase.key === 'execute' ? localT * DEMO_PATH.length : 0;
  const transitionRatio = 280 / (animDur / DEMO_PATH.length);
  const ghostUpTo = phase.key === 'execute'
    ? Math.floor(cellProgress - transitionRatio)
    : -1;

  const collectedStars = new Set();
  if (runnerIdx >= 0) {
    for (let i = 0; i <= runnerIdx; i++) {
      const c = DEMO_PATH[i];
      if (DEMO_BOARD[c.y][c.x] === 3) collectedStars.add(`${c.x},${c.y}`);
    }
  }
  const lastCell = DEMO_PATH[DEMO_PATH.length - 1];
  const won = phase.key === 'won' ||
              (phase.key === 'execute' && runnerIdx === DEMO_PATH.length - 1);
  const runnerCell = runnerIdx >= 0 ? DEMO_PATH[runnerIdx] : null;
  // Each of the 4 phases now has its own pip — direct mapping, no collapse.
  const pipIdx = phaseIdx;

  // Tapping the board acts as a "next" link — advance one phase from
  // wherever we are (whether auto-playing or already pinned). Wraps from
  // the last phase back to memorize so the user can re-walk the loop manually.
  const advance = () => {
    const next = (phaseIdx + 1) % DEMO_PHASES.length;
    setPin(p => ({ idx: next, key: p.key + 1 }));
  };

  return (
    <div className="block tight">
      <div className="label" style={{ marginBottom: 10, textAlign: 'center', fontSize: 24 }}>
        How to play
      </div>
      {phase.key === 'won' ? (
        // Won phase replaces the board entirely with a sample score tally.
        // Same advance() click handler so tapping the card still cycles to
        // the next phase (memorize) like the board does on other phases.
        <div className="demo-score" key={pin.key}
             role="button"
             aria-label="Advance demo to next phase"
             onClick={advance}>
          <div className="demo-score-row pos">
            <span>+{DEMO_BREACH_PTS}</span><span>breach</span>
          </div>
          <div className="demo-score-row pos">
            <span>+{DEMO_STAR_PTS}</span><span>diamonds ×{DEMO_STARS}</span>
          </div>
          <div className="demo-score-row neg">
            <span>−{DEMO_MOVE_PEN}</span><span>moves ×{DEMO_MOVES}</span>
          </div>
          <div className="demo-score-total">
            <span>+{DEMO_TOTAL_PTS}</span><span>total</span>
          </div>
        </div>
      ) : (
        <div className="demo-board"
             role="button"
             aria-label="Advance demo to next phase"
             onClick={advance}>
          {Array.from({length: 7}).map((_, y) =>
            Array.from({length: 5}).map((_, x) => {
              const val = DEMO_BOARD[y][x];
              const cls = ['cell'];
              if (y === 0) cls.push('row-top');
              if (y === 6) cls.push('row-bottom');
              if (val === 1 && showWalls) cls.push('wall');

              const pathIdx = DEMO_PATH.findIndex(p => p.x === x && p.y === y);
              if (pathIdx >= 0) {
                if (phase.key === 'plan' && pathIdx < planRevealCount) {
                  if (pathIdx === 0) cls.push('start-mark');
                  else if (pathIdx === planRevealCount - 1) cls.push('ghost-end');
                  else cls.push('ghost');
                } else if (phase.key === 'execute') {
                  // Trail catches up behind the runner — cells go ghost only
                  // after the dot has visually landed on them. Matches the
                  // real game's "draw your path as you run" feel instead of
                  // pre-painting the whole route. Start cell stays unmarked
                  // since the dot already occupies it.
                  if (pathIdx > 0 && pathIdx <= ghostUpTo) cls.push('ghost');
                }
              }

              const showStar = val === 3 && !collectedStars.has(`${x},${y}`);
              if (showStar) cls.push('star');

              return (
                <div key={`${x}-${y}`} className={cls.join(' ')}>
                  {showStar && <Icon.star />}
                </div>
              );
            })
          )}
          {runnerCell && (
            <div className="demo-runner"
                 style={{ '--rx': runnerCell.x, '--ry': runnerCell.y }} />
          )}
        </div>
      )}
      <p className="demo-caption">{phase.caption}</p>
      <div className="demo-pips" role="tablist">
        {DEMO_PHASES.map((_, i) => (
          <button key={i}
            type="button"
            role="tab"
            aria-selected={i === pipIdx}
            aria-label={DEMO_PHASES[i].caption}
            className={`demo-pip ${i === pipIdx ? 'active' : ''}`}
            onClick={() => setPin(p => ({ idx: i, key: p.key + 1 }))} />
        ))}
      </div>
    </div>
  );
};

// ------------------------------------------------------------
// Main Menu
// ------------------------------------------------------------
const ANDRE_QUOTES = [
  // Andre personality — establishes him as chill, lucky, self-aware
  "Andre likes to chill. The maze does not.",
  "Andre's been lucky so far. Don't ruin it.",
  "Andre's smart. Andre's also wrong sometimes.",
  "Andre's secret: stay calm, click less.",
  "Andre believes in you. Mostly.",
  "Andre's chill until the timer hits five.",
  "Andre would skip the diamond. Then regret it.",
  "Andre's hat hides a planning spreadsheet.",
  "Andre vibes. You optimize. Both work.",
  "Andre's only rule: don't crash. The rest is style.",
  "Andre's smart enough to know when to back off. Are you?",
  "Andre's seen worse mazes. Once. Maybe twice.",
  "Andre took the long way once. Got there. Less stylish.",
  "Andre rates this maze: aggressive.",
  "Andre prefers the scenic route. He'll never admit it.",

  // Maze wisdom — generic aphorisms, still good
  "Memorize twice. Move once.",
  "Walls don't move. Unfortunately, neither does your memory.",
  "A shortest path walked slowly is still a shortest path.",
  "If in doubt, go up. It's where the exit lives.",
  "The maze forgets you the second you look away. Harsh, but fair.",
  "Speedrunning the maze is just confident guessing.",
  "Diamonds are optional. Dignity is negotiable.",
  "Dead ends are just paths with commitment issues.",
  "If you can't see the wall, it can still see you.",
  "Plan like a chess master. Run like you forgot the plan.",
  "Wrong turn? Call it a scenic route.",
  "The bottom row is friendly. The top row is earned.",
  "You miss 100% of the diamonds you don't step on.",
  "A good run is quiet. A great run is quieter.",
  "Blind mode is just trust-falling into geometry.",
  "Hesitation costs points. So does confidence, sometimes.",
  "There's no prize for the longest path. I checked.",
  "The maze is a suggestion. Your queue is a commitment.",
];

// Randomize generates a fresh 6-digit seed for one-off gauntlet runs.
// FRIENDS_SEED comes from window (game-logic.js) and matches the canonical
// permanent-leaderboard seed so the seed input defaults to it.
const randomGauntletSeed = () =>
  String(Math.floor(Math.random() * 900000) + 100000);

// Friendly "3d 4h" / "2h 14m" / "8m" countdown for the weekly board.
// Drops to "ending soon" under a minute. Always accurate to the minute since
// the board only re-renders that often.
const formatCountdown = (msLeft) => {
  if (msLeft <= 0) return 'rotating now';
  const mins = Math.floor(msLeft / 60000);
  if (mins < 1) return 'ending soon';
  if (mins < 60) return `${mins}m`;
  const hours = Math.floor(mins / 60);
  if (hours < 24) {
    const m = mins % 60;
    return m > 0 ? `${hours}h ${m}m` : `${hours}h`;
  }
  const days = Math.floor(hours / 24);
  const h = hours % 24;
  return h > 0 ? `${days}d ${h}h` : `${days}d`;
};

// Single pinned-board card. Renders top 5 from `entries` (sorted desc by
// points), highlights the player's own row, and shows a full-width
// "Play this seed" CTA at the bottom. If the player isn't in the top 5
// their row is appended below as a "you" footer.
const PinnedBoard = ({ title, seed, entries, alias, onPlay, meta }) => {
  const list = Array.isArray(entries) ? entries : [];
  const top = list.slice(0, 5);
  const userIdx = alias ? list.findIndex(e => e.name === alias) : -1;
  const userInTop = userIdx >= 0 && userIdx < 5;
  const userBelow = userIdx >= 5 ? list[userIdx] : null;

  return (
    <div className="pinned-board scene-enter">
      <div className="pb-header">
        <span className="pb-title">{title}</span>
        <span className="pb-seed-block">
          <span className="pb-seed">#{seed}</span>
          {meta && <span className="pb-meta">{meta}</span>}
        </span>
      </div>
      <div className="pb-body">
        {top.length === 0 ? (
          <div className="pb-empty">No runs yet — set the pace.</div>
        ) : top.map((e, i) => (
          <div key={`${e.name}-${i}`}
               className={`pb-row${i === 0 ? ' gold' : ''}${e.name === alias ? ' you' : ''}`}>
            <span className="pb-rank">{i + 1}</span>
            <span className="pb-name">{e.name}</span>
            <span className="pb-stats">L{e.levels} · {e.points}</span>
          </div>
        ))}
        {userBelow && (
          <div className="pb-row you">
            <span className="pb-rank">{userIdx + 1}</span>
            <span className="pb-name">{userBelow.name}</span>
            <span className="pb-stats">L{userBelow.levels} · {userBelow.points}</span>
          </div>
        )}
      </div>
      <button className="btn btn-primary btn-block pb-play-cta" onClick={() => onPlay(seed)}>
        Play this seed
      </button>
    </div>
  );
};

// Practice leaderboard — wins-by-name, rendered inside the Practice tab.
const PracticeLeaderboard = ({ entries }) => (
  <div className="block tight scene-enter">
    <div className="label" style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
      <span>Practice leaders</span>
      <span style={{ fontSize: 10, fontWeight: 600, color: 'var(--ink-faint)' }}>TOP 5 · WINS</span>
    </div>
    {(entries || []).length === 0 ? (
      <p className="body-sm" style={{ textAlign: 'center', margin: 4 }}>No practice wins yet — be the first</p>
    ) : (
      entries.slice(0, 5).map((r, i) => (
        <div key={i} className={`lb-row ${i === 0 ? 'gold' : ''}`}>
          <span className="lb-rank">{String(i + 1).padStart(2, '0')}</span>
          <span className="lb-name">{r.name}</span>
          <span className="lb-wins">{r.wins} {r.wins === 1 ? 'win' : 'wins'}</span>
        </div>
      ))
    )}
  </div>
);

// Match (head-to-head) history — rendered inside both Host and Join tabs since
// either route lands you in the same competitive flow.
const MatchLeaderboard = ({ entries }) => (
  <div className="block tight scene-enter">
    <div className="label" style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between' }}>
      <span>Recent matches</span>
      <span style={{ fontSize: 10, fontWeight: 600, color: 'var(--ink-faint)' }}>LAST 5</span>
    </div>
    {(entries || []).length === 0 ? (
      <p className="body-sm" style={{ textAlign: 'center', margin: 4 }}>No matches yet — host one</p>
    ) : (
      entries.slice(0, 5).map((r, i) => {
        const tied = r.winner === 'Draw';
        const p1Won = !tied && r.winner === r.p1;
        return (
          <div key={i} className="lb-row match-row">
            <span className={`match-side ${p1Won ? 'won' : ''}`}>{r.p1} {r.p1Score}</span>
            <span className="match-vs">{tied ? '=' : 'vs'}</span>
            <span className={`match-side right ${!tied && !p1Won ? 'won' : ''}`}>{r.p2Score} {r.p2}</span>
          </div>
        );
      })
    )}
  </div>
);

const MainMenu = ({ alias, setAlias, onSolo, onGauntlet, onHost, onJoin,
                    lbSolo, lbFriends, lbWeekly, weeklyInfo, lbMatch }) => {
  const [tab, setTab] = useState('practice');
  const [quote] = useState(() => ANDRE_QUOTES[Math.floor(Math.random() * ANDRE_QUOTES.length)]);
  const [difficulty, setDifficulty] = useState('easy');
  const [firewalls, setFirewalls] = useState(12);
  const [mode, setMode] = useState('casual');
  const [joinCode, setJoinCode] = useState('');
  const [seed, setSeed] = useState(window.FRIENDS_SEED || '588008');

  // Tick once per minute so the weekly countdown stays current without
  // burning rAF on a value that only changes minute-to-minute.
  const [, setNowTick] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setNowTick(t => t + 1), 60000);
    return () => clearInterval(id);
  }, []);
  const weeklyMsLeft = weeklyInfo ? Math.max(0, weeklyInfo.weekEndMs - Date.now()) : 0;

  return (
    <div className="stack scene-enter" style={{ gap: 14 }}>
      <div className="block tight">
        <div className="label" style={{ marginBottom: 8 }}>Your alias</div>
        <input className="input" placeholder="e.g. Nova" value={alias} maxLength={10}
          onChange={(e) => setAlias(e.target.value)} />
      </div>

      <div className="tabs t4">
        {[
          { k: 'practice', title: 'Practice', sub: 'random' },
          { k: 'gaunt',    title: 'Gauntlet', sub: '40 levels' },
          { k: 'host',     title: 'Host',     sub: 'live' },
          { k: 'join',     title: 'Join',     sub: 'code' },
        ].map(t => (
          <button key={t.k} className={`tab ${tab === t.k ? 'active' : ''}`} onClick={() => setTab(t.k)}>
            <span>{t.title}</span>
            <span className="tab-sub">{t.sub}</span>
          </button>
        ))}
      </div>

      {tab === 'practice' && (
        <>
          <div className="block tight scene-enter">
            <div className="label" style={{ marginBottom: 10 }}>Difficulty</div>
            <div className="tabs t3" style={{ marginBottom: 14 }}>
              {Object.values(DIFFICULTY_PROFILES).map(p => (
                <button key={p.key} className={`tab ${difficulty === p.key ? 'active' : ''}`} onClick={() => setDifficulty(p.key)}>
                  <span>{p.label}</span><span className="tab-sub">{p.sub}</span>
                </button>
              ))}
            </div>
            <button className="btn btn-primary btn-lg btn-block" onClick={() => onSolo(difficulty)}>
              New random board
            </button>
          </div>
          <PracticeLeaderboard entries={lbSolo} />
        </>
      )}

      {tab === 'gaunt' && (
        <div className="block tight scene-enter">
          <div className="label" style={{ marginBottom: 6 }}>Gauntlet seed</div>
          <p className="body-sm" style={{ margin: '0 0 10px' }}>The default seed is the shared "friends" board — same 40 levels for everyone. Randomize for a one-off run.</p>
          <div className="join-row" style={{ marginBottom: 12 }}>
            <input className="input code" value={seed}
              onChange={e => setSeed(e.target.value.replace(/\D/g, '').slice(0, 6))} />
            <button className="btn btn-sm" onClick={() => {
              setSeed(randomGauntletSeed());
              toast('New seed rolled', 'success');
            }}>Randomize</button>
          </div>
          <button className="btn btn-primary btn-lg btn-block"
            disabled={seed.length < 4}
            onClick={() => onGauntlet(seed)}>
            Fight the leaderboard
          </button>
        </div>
      )}

      {tab === 'host' && (
        <>
          <div className="block tight scene-enter">
            <div className="label" style={{ marginBottom: 8 }}>Memorize time</div>
            <div className="tabs t2" style={{ marginBottom: 14 }}>
              <button className={`tab ${mode === 'casual' ? 'active' : ''}`} onClick={() => setMode('casual')}>
                <span>Casual</span><span className="tab-sub">60s memorize</span>
              </button>
              <button className={`tab ${mode === 'comp' ? 'active' : ''}`} onClick={() => setMode('comp')}>
                <span>Pro</span><span className="tab-sub">10s memorize</span>
              </button>
            </div>
            <div className="label" style={{ marginBottom: 8 }}>Wall budget</div>
            <div className="tabs t4" style={{ marginBottom: 14 }}>
              {[8, 12, 16, 28].map(v => (
                <button key={v} className={`tab ${firewalls === v ? 'active' : ''}`} onClick={() => setFirewalls(v)}>
                  <span>{v}</span>
                </button>
              ))}
            </div>
            <button className="btn btn-primary btn-lg btn-block" onClick={() => onHost({ mode, firewalls })}>
              Host a match
            </button>
          </div>
          <MatchLeaderboard entries={lbMatch} />
        </>
      )}

      {tab === 'join' && (
        <>
          <div className="block tight scene-enter">
            <div className="label" style={{ marginBottom: 8 }}>Port code</div>
            <div className="join-row">
              <input className="input code" placeholder="XXXX" maxLength={4}
                value={joinCode}
                onChange={e => setJoinCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g,'').slice(0,4))}
                onKeyDown={e => { if (e.key === 'Enter' && joinCode.length === 4) onJoin(joinCode); }} />
              <button className="btn btn-accent" disabled={joinCode.length !== 4} onClick={() => onJoin(joinCode)}>Connect</button>
            </div>
            <p className="body-sm" style={{ margin: '10px 0 0', textAlign: 'center' }}>
              Get the code from whoever's hosting.
            </p>
          </div>
          <MatchLeaderboard entries={lbMatch} />
        </>
      )}

      {tab === 'gaunt' && (
        <>
          <PinnedBoard
            title="Friends Board"
            seed={window.FRIENDS_SEED || '588008'}
            entries={lbFriends}
            alias={alias}
            onPlay={onGauntlet}
          />
          {weeklyInfo && (
            <PinnedBoard
              title="This Week"
              seed={weeklyInfo.seed}
              entries={lbWeekly}
              alias={alias}
              onPlay={onGauntlet}
              meta={`resets in ${formatCountdown(weeklyMsLeft)}`}
            />
          )}
        </>
      )}

      <div className="block tight">
        <div className="label" style={{ marginBottom: 8 }}>How points are scored</div>
        <p className="body-sm" style={{ margin: '0 0 12px' }}>
          Shortest path to the top yields the highest score. Red walls crash you. Diamonds are bonus.
        </p>
        <div className="score-grid">
          <div className="score-row pos"><span>Breach the top</span><span>+50</span></div>
          <div className="score-row pos"><span>Each diamond</span><span>+25</span></div>
          <div className="score-row neg score-row-wide"><span>Per move</span><span>−3</span></div>
        </div>
      </div>

      <div style={{ display: 'flex', justifyContent: 'center', marginTop: 6 }}>
        <AndreChatter mood="wink" size={56} message={quote} />
      </div>
    </div>
  );
};

// ------------------------------------------------------------
// Game Header (scorecards + port)
// ------------------------------------------------------------
const GameHeader = ({ p1, p2, port, round, mode, activeIsP1 }) => (
  <div className="scorecards">
    <div className={`scorecard ${activeIsP1 ? '' : 'active'}`}>
      <span className="sc-role">{activeIsP1 ? 'MAKER' : 'MOVER'}</span>
      <span className="sc-name">{p1.name}</span>
      <span className="sc-score">{p1.score}</span>
    </div>
    <div className="port-tag">
      <span className="pt-label">Port</span>
      <span className="pt-code">{port || '––––'}</span>
      {round && <span className="pt-meta">R{round} · {(mode || 'casual').toUpperCase()}</span>}
    </div>
    <div className={`scorecard right ${activeIsP1 ? 'active' : ''}`}>
      <span className="sc-role">{activeIsP1 ? 'MOVER' : 'MAKER'}</span>
      <span className="sc-name">{p2.name}</span>
      <span className="sc-score">{p2.score}</span>
    </div>
  </div>
);

// ------------------------------------------------------------
// Board
// ------------------------------------------------------------
const Board = React.forwardRef(({ board, overlay, onCellTap, onCellDrag, boardRef, runnerDot }, _ref) => {
  const gridRef = useRef(null);
  const isDragging = useRef(false);

  useEffect(() => { if (boardRef) boardRef.current = gridRef.current; }, [boardRef]);

  const handlePointerDown = (x, y) => (e) => {
    // Touch pointers are implicitly captured to the down-target, which
    // suppresses pointerEnter on sibling cells. Release so the drag can
    // trace across the grid on phones.
    if (e.pointerType === 'touch') {
      try { e.target.releasePointerCapture?.(e.pointerId); } catch {}
    }
    isDragging.current = true;
    onCellTap?.(x, y);
    e.preventDefault();
  };
  const handlePointerEnter = (x, y) => (e) => {
    if (!isDragging.current) return;
    if (e.buttons === 0 && e.pointerType !== 'touch') { isDragging.current = false; return; }
    onCellDrag?.(x, y);
  };
  useEffect(() => {
    const up = () => { isDragging.current = false; };
    window.addEventListener('pointerup', up);
    window.addEventListener('pointercancel', up);
    return () => {
      window.removeEventListener('pointerup', up);
      window.removeEventListener('pointercancel', up);
    };
  }, []);

  return (
    <div className="board-wrap">
      <div className="board" ref={gridRef}>
        {/* Thin checkered strip at top = goal indicator (replaces old GOAL pill). */}
        <div className="board-strip goal" aria-label="Goal — top row" />
        {Array.from({ length: ROWS }).map((_, y) =>
          Array.from({ length: COLS }).map((_, x) => {
            const val = board[y][x];
            const ov = overlay?.[y]?.[x];
            const cls = ['cell'];
            if (y === 0) cls.push('row-top');
            if (y === ROWS - 1) cls.push('row-bottom');
            const blindMask = ov === 'hide' || ov === 'ghost' || ov === 'ghost-end' ||
                              ov === 'start' || ov === 'start-mark' || ov === 'win';
            if (val === 1 && !blindMask) cls.push('wall');
            if (ov === 'start') cls.push('start-zone');
            if (ov === 'start-mark') cls.push('start-mark');
            if (val === 3 && ov !== 'hide') cls.push('star');
            if (ov === 'ghost') cls.push('ghost');
            if (ov === 'ghost-danger') cls.push('ghost-danger');
            if (ov === 'ghost-end') cls.push('ghost-end');
            if (ov === 'win') cls.push('win');
            if (ov === 'invalid') cls.push('invalid');
            if (ov === 'pop') cls.push('pop');
            if (ov === 'fidget-pop') cls.push('fidget-pop');
            return (
              <div key={`${x}-${y}`} className={cls.join(' ')}
                   onPointerDown={handlePointerDown(x, y)}
                   onPointerEnter={handlePointerEnter(x, y)}>
                {val === 3 && ov !== 'hide' && <Icon.star />}
              </div>
            );
          })
        )}
        {/* Thin violet strip at bottom = start indicator (replaces old START pill). */}
        <div className="board-strip start" aria-label="Start — bottom row" />
        {runnerDot && <div className={`runner-dot show${runnerDot.hit ? ' hit' : ''}`} style={runnerDot.style} />}
      </div>
    </div>
  );
});

// ------------------------------------------------------------
// Maker controls
// ------------------------------------------------------------
const MakerControls = ({ timeLeft, wallsLeft, maxWalls, onDeploy, deployDisabled }) => {
  const urgent = timeLeft <= 5;
  return (
    <div className="stack">
      <div className={`timer-pill ${urgent ? 'urgent' : ''}`}>
        <span className="label" style={{ color: urgent ? 'var(--chalk)' : undefined }}>Build phase</span>
        <span className="timer-value">{timeLeft}</span>
      </div>
      <div className="stat-chip">
        <span className="label">Walls remaining</span>
        <span className="stat-count">{wallsLeft}/{maxWalls}</span>
      </div>
      <button className="btn btn-primary btn-block btn-lg" onClick={onDeploy} disabled={deployDisabled}>
        Deploy maze
      </button>
    </div>
  );
};

// ------------------------------------------------------------
// Mover controls — memorize + execute
// ------------------------------------------------------------
// Memorize phase: just the ready button. Countdown lives in the top status
// pill (which goes urgent in the last 5s) so we don't double up timers.
const MoverMemorize = ({ onReady, competitive }) => (
  <button className="btn btn-primary btn-block btn-lg" onClick={onReady}>
    {competitive ? 'Lock it in — go blind' : 'Ready — hide the board'}
  </button>
);

// Stacked rather than side-by-side so each button is full-width / thumb-sized
// on phones. Run lands closest to the bottom of the screen for one-handed use.
// "arrows" + "Enter" in the hint refer to keyboard input — the on-screen
// controls are drag-on-board + the buttons below.
const MoverExecute = ({ onUndo, onExecute, executeEnabled, canUndo }) => (
  <div className="stack" style={{ gap: 8 }}>
    <button className="btn btn-block" style={{ padding: '10px 16px', fontSize: 13 }}
      onClick={onUndo} disabled={!canUndo}>
      Undo last move
    </button>
    <button className="btn btn-primary btn-lg btn-block" onClick={onExecute} disabled={!executeEnabled}>
      Run
    </button>
    <p className="kb-hint">Drag or use arrow keys · tap the path tip to retract · Enter to run</p>
  </div>
);

Object.assign(window, {
  Icon, TopBar, Splash, MainMenu, GameHeader, Board, MakerControls, MoverMemorize, MoverExecute,
});
