// ============================================================
// Memory Maze — 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">Memory Maze</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 }) => {
  return (
    <div className="stack scene-enter" style={{ gap: 12 }}>
      <div className="block" style={{ textAlign: 'center', padding: '22px 18px' }}>
        <div className="display" style={{ fontSize: 46, color: 'var(--violet)', marginBottom: 8, textShadow: '4px 4px 0 var(--ink)', lineHeight: 0.95 }}>
          MEMORY<br />MAZE
        </div>
        <div className="h3" style={{ color: 'var(--ink-soft)', fontFamily: 'var(--font-ui)', fontWeight: 700, letterSpacing: '0.16em', fontSize: 11, textTransform: 'uppercase', marginBottom: 18, marginTop: 12 }}>
          Trace the path from memory.
        </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
// ------------------------------------------------------------
// 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 [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>
  );
};

// ------------------------------------------------------------
// 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>
);

// No Run button — the blob auto-launches as soon as the path reaches the top.
// Undo stays as the single, deliberate way to step the path back.
const MoverExecute = ({ onUndo, 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>
    <p className="kb-hint">Drag or use arrow keys · sketch to the top to launch</p>
  </div>
);

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