/* Party screens — lobby through winner.
 *
 * A direct port of party.js. The markup and class names are kept as they were
 * and styled by the generated party.css, because these screens are read at
 * arm's length across a room and were tuned for that; restyling them in this
 * app's ink-on-paper idiom would be a redesign wearing a port's clothes.
 *
 * What did change is who owns the update. The original rebuilt a screen from
 * an HTML string on every server message and hung one delegated click handler
 * off the root. Here each screen is a component and handlers are props, so a
 * re-render cannot leak a listener and the arena canvas survives — which the
 * old code had to arrange by hand (see arena-ui.js).
 */
(function () {

const esc = s => String(s ?? '');

/* Countdown bars move between server messages, so they tick themselves rather
 * than dragging the whole screen through a re-render four times a second. */
function Timer({ endsAt, total, calm, nowServer }) {
  const ref = React.useRef(null);
  React.useEffect(() => {
    let raf = 0;
    const tick = () => {
      raf = requestAnimationFrame(tick);
      if (!ref.current) return;
      const left = Math.max(0, (endsAt - nowServer()) / 1000);
      ref.current.style.width = total
        ? Math.max(0, Math.min(100, (left / total) * 100)) + '%' : '0%';
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [endsAt, total, nowServer]);
  return <div className={`timer${calm ? ' calm' : ''}`}><i ref={ref} /></div>;
}
window.PartyTimer = Timer;

function HostStrip({ v, on }) {
  if (!v.isOwner) return null;
  const skip = ['vote', 'intro', 'scores'].includes(v.phase);
  return (
    <div className="row" style={{ marginBottom: 14 }}>
      <span className="pill"><span className="dot" />Host · {v.code}</span>
      <div className="grow" />
      {skip ? <button className="btn ghost sm" style={{ width: 'auto' }} onClick={() => on({ t: 'skip' })}>Skip ▸</button> : null}
      <button className="btn ghost sm" style={{ width: 'auto' }} onClick={() => on({ t: 'end' })}>End party</button>
    </div>
  );
}

function Board({ v, me, title = 'Standings' }) {
  const rows = v.standings || [];
  if (!rows.length) return null;
  return (
    <div className="card"><h2>{title}</h2><div className="plist">
      {rows.map(r => (
        <div key={r.pid} className={`prow r${r.rank}${r.pid === me.pid ? ' me' : ''}${r.connected === false ? ' off' : ''}`}>
          <span className="rk">{r.rank}</span>
          <span className="em">{r.emoji}</span>
          <div className="grow"><div className="nm">{esc(r.name)}</div></div>
          <span className="sc">{r.score.toLocaleString()}</span>
        </div>
      ))}
    </div></div>
  );
}

function PlayerList({ v, me, on }) {
  const slots = Math.max(0, 4 - v.players.length);
  return (
    <div className="card">
      <h2>{v.players.length} of {v.maxPlayers} in</h2>
      <div className="plist">
        {v.players.map(p => (
          <div key={p.pid} className={`prow${p.pid === me.pid ? ' me' : ''}${p.connected ? '' : ' off'}`}>
            <span className="em">{p.emoji}</span>
            <div className="grow">
              <div className="nm">{esc(p.name)}{p.pid === me.pid ? ' (you)' : ''}</div>
              <div className="st">{p.connected ? 'ready' : 'reconnecting…'}</div>
            </div>
            {v.isOwner && p.pid !== me.pid ? (
              <button className="btn ghost sm" style={{ width: 'auto', minHeight: 32, padding: '6px 10px' }}
                      onClick={() => on({ t: 'kick', pid: p.pid })}>✕</button>
            ) : null}
          </div>
        ))}
        {Array.from({ length: slots }, (_, i) => (
          <div className="slot" key={`s${i}`}>waiting for a player…</div>
        ))}
      </div>
    </div>
  );
}

/* ---------------------------------------------------------------- lobby -- */

function Lobby({ v, me, on, joinBase }) {
  const joinUrl = `${joinBase}/j/${v.code}`;
  /* Same origin the QR encodes, so whatever host works for a guest's phone
     works for the screen in the room. */
  const tvUrl = `${joinBase}/tv/${v.code}`;
  const [big, setBig] = React.useState(false);
  /* Body class rather than inline style, so the rule that hides this app's nav
     can live next to the arena's — same job, same place. */
  React.useEffect(() => {
    document.body.classList.toggle('mm-qr-big', !!big);
    return () => document.body.classList.remove('mm-qr-big');
  }, [big]);
  const qr = `${window.PH365_API_BASE || ''}/api/qr.svg?code=${v.code}`;

  if (!v.isOwner) {
    return (
      <>
        <div className="stage">
          <div className="huge">{me.emoji || '🐭'}</div>
          <div className="head">You're in</div>
          <div className="body">Hang tight — the host kicks it off.</div>
          <div className="pill ok"><span className="dot" />{v.players.length} here</div>
        </div>
        <PlayerList v={v} me={me} on={on} />
        <button className="btn ghost" style={{ marginTop: 12, width: '100%' }}
                onClick={() => on({ t: 'leave' })}>Leave the party</button>
      </>
    );
  }

  if (big) {
    return (
      <div className="stage" style={{ minHeight: '80vh' }} onClick={() => setBig(false)}>
        <div className="qr-code" style={{ width: '78vw', maxWidth: 520 }}>
          <img src={qr} alt="Join QR code" style={{ width: '100%' }} />
        </div>
        <div className="code-big" style={{ fontSize: '18vw' }}>{v.code}</div>
        <div className="muted">tap to close</div>
      </div>
    );
  }

  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="ticket">
        {/* No QR here any more. The code goes up on the ears the moment the
            party opens, which is the whole point of putting it there — a host
            holding a phone out for people to scan is the thing that replaced.
            What stays is what the host actually needs off their own screen:
            the code to read aloud, and the address for anyone whose camera is
            not co-operating. */}
        <div className="qr-wrap">
          <div className="muted" style={{ marginBottom: 2 }}>Scan the code on the ears</div>
          <div className="code-big">{v.code}</div>
          <div className="muted">or go to<br /><b>{joinUrl}</b></div>
          <button className="btn ghost" style={{ marginTop: 14 }}
                  onClick={() => window.open(tvUrl, '_blank', 'noopener')}>
            📺 Also on a big screen
          </button>
          <div className="muted" style={{ marginTop: 8, fontSize: 12 }}>
            Put <b>{tvUrl}</b> on any screen in the room.
          </div>
        </div>
      </div>

      {v.launcher && v.launcher.length ? (
        <div className="card">
          <h2>Start a round</h2>
          <div className="stack">
            {v.launcher.map(g => (
              <button key={g.key} className="vote-opt" onClick={() => on({ t: 'launch', key: g.key })}>
                <span className="em">{g.emoji}</span>
                <div className="grow"><div className="nm">{esc(g.name)}</div><div className="bl">{esc(g.blurb || '')}</div></div>
                <span className="ct">▶</span>
              </button>
            ))}
          </div>
        </div>
      ) : (
        <div className="card">
          <h2>What are we playing?</h2>
          <div className="btns2">
            <button className={`btn ${v.mode === 'party' ? 'primary' : 'ghost'}`} onClick={() => on({ t: 'mode', mode: 'party' })}>🎉 Party Pack</button>
            <button className={`btn ${v.mode === 'quiz' ? 'primary' : 'ghost'}`} onClick={() => on({ t: 'mode', mode: 'quiz' })}>❓ Quiz Night</button>
          </div>
        </div>
      )}

      <PlayerList v={v} me={me} on={on} />

      <div className="sticky-cta">
        <button className="btn green" disabled={v.players.length <= 1} onClick={() => on({ t: 'start' })}>
          {v.players.length > 1
            ? `Start ${v.mode === 'quiz' ? 'the quiz' : 'the party'} · ${v.players.length} playing`
            : 'Waiting for someone to scan in…'}
        </button>
      </div>
    </>
  );
}

/* ----------------------------------------------------------------- pick -- */

function PickView({ v, me, on, profile, nowServer }) {
  const P = v.pick;
  const level = (v.you && v.you.level) || profile?.level || 1;
  const mine = P.mine;
  const BY_ID = window.PartyEngine?.BY_ID || {};

  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="center" style={{ marginBottom: 4 }}>
        <div style={{ fontSize: 23, fontWeight: 800 }}>Pick your critter</div>
        <div className="muted">{P.ready} of {P.of} ready · level {level}</div>
      </div>
      <Timer endsAt={v.endsAt} total={30} calm nowServer={nowServer} />

      <div className="pickgrid">
        {P.roster.map(c => {
          const locked = c.tier > level;
          const owner = P.taken[c.id];
          const takenByOther = owner && owner !== me.pid;
          const isMine = mine === c.id;
          const cls = ['picktile'];
          if (isMine) cls.push('mine');
          if (locked) cls.push('locked');
          if (takenByOther) cls.push('taken');
          return (
            <button key={c.id} className={cls.join(' ')} disabled={locked || takenByOther}
                    onClick={() => on({ t: 'char', id: c.id })}>
              <window.CritterTile charId={c.id} locked={locked} />
              <div className="pn">{esc(c.name)}</div>
              {locked ? <div className="plock">🔒 Lv {c.tier}</div>
                : takenByOther ? <div className="ptaken">taken</div>
                : <div className="pb">{esc(c.blurb)}</div>}
            </button>
          );
        })}
      </div>

      {profile ? (
        <div className="card" style={{ marginTop: 14 }}>
          <div className="row">
            <div className="grow">
              <div style={{ fontWeight: 800 }}>Level {profile.level}</div>
              <div className="muted">{profile.max ? 'Everything unlocked'
                : `${profile.into} / ${profile.need} XP to level ${profile.level + 1}`}</div>
            </div>
          </div>
          <div className="xpbar" style={{ marginTop: 8 }}><i style={{ width: `${profile.pct}%` }} /></div>
          <div className="muted" style={{ marginTop: 8 }}>Play parties to unlock the rest of the cast.</div>
        </div>
      ) : null}

      <div className="sticky-cta">
        <button className={`btn ${mine ? 'green' : 'primary'}`} disabled={!mine} onClick={() => on({ t: 'ready' })}>
          {mine ? `Ready as ${BY_ID[mine]?.name || ''}` : 'Pick someone first'}
        </button>
      </div>
    </>
  );
}

/* ----------------------------------------------------------------- vote -- */

function VoteView({ v, on, nowServer }) {
  const V = v.vote;
  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="center" style={{ marginBottom: 6 }}>
        <div style={{ fontSize: 22, fontWeight: 800 }}>{esc(V.prompt)}</div>
        <div className="muted">{V.cast} of {V.of} voted</div>
      </div>
      <Timer endsAt={v.endsAt} total={20} calm nowServer={nowServer} />
      <div className="stack">
        {V.options.map(o => (
          <button key={o.key} className={`vote-opt${V.mine === o.key ? ' picked' : ''}`}
                  onClick={() => on({ t: 'vote', key: o.key })}>
            <span className="em">{o.emoji}</span>
            <div className="grow"><div className="nm">{esc(o.name)}</div><div className="bl">{esc(o.blurb)}</div></div>
            {V.mine === o.key ? <span className="ct">✓</span> : null}
          </button>
        ))}
      </div>
      {V.mine ? <div className="muted center" style={{ marginTop: 14 }}>Locked in. Waiting for the rest…</div> : null}
    </>
  );
}

/* ---------------------------------------------------------------- intro -- */

function IntroView({ v, on, nowServer }) {
  const I = v.intro || {};
  const howto = I.howto ? (Array.isArray(I.howto) ? I.howto : [I.howto]) : null;
  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="stage">
        {I.finale ? <div className="finale-flag">FINALE · DOUBLE POINTS</div>
                  : <div className="muted">Game {I.index} of {I.of}</div>}
        <div className="huge intro-pop">{I.emoji || '🎮'}</div>
        <div className="head">{esc(I.name || '')}</div>
        <div className="body">{esc(I.blurb || '')}</div>
        {howto ? (
          <div className="howto">
            {howto.map((h, i) => (
              <div className="howto-row" key={i} style={{ animationDelay: `${0.15 + i * 0.12}s` }}>
                <span className="n">{i + 1}</span>{esc(h)}
              </div>
            ))}
          </div>
        ) : null}
        {I.controller ? <div className="pill warn"><span className="dot" />Your phone is the controller</div> : null}
        <Timer endsAt={v.endsAt} total={6} nowServer={nowServer} />
        <div className="muted">get ready…</div>
      </div>
    </>
  );
}

/* ------------------------------------------------------- scores / final -- */

function ScoresView({ v, me, on, nowServer }) {
  const sc = v.scores || {};
  const gained = sc.points || {};
  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="stage" style={{ minHeight: '24vh' }}>
        <div className="huge">{sc.emoji || '🏁'}</div>
        <div className="head">{esc(sc.name || 'Round over')}</div>
        {sc.summary ? <div className="body">{esc(sc.summary)}</div> : null}
      </div>
      <div className="card"><h2>After {v.gameIdx + 1} of {v.gameCount}</h2><div className="plist">
        {(v.standings || []).map(r => (
          <div key={r.pid} className={`prow r${r.rank}${r.pid === me.pid ? ' me' : ''}`}>
            <span className="rk">{r.rank}</span><span className="em">{r.emoji}</span>
            <div className="grow"><div className="nm">{esc(r.name)}</div>
              {gained[r.pid] ? <div className="st gain">+{Math.round(gained[r.pid])} this game</div> : null}</div>
            <span className="sc">{r.score.toLocaleString()}</span>
          </div>
        ))}
      </div></div>
      <Timer endsAt={v.endsAt} total={12} calm nowServer={nowServer} />
      <div className="muted center">next up…</div>
    </>
  );
}

function XpCard({ xp }) {
  if (!xp) return null;
  const a = xp.after;
  return (
    <div className="card xp-card">
      <h2>{xp.leveledUp ? 'Level up!' : 'Progress'}</h2>
      <div className="row">
        <div className="grow">
          <div style={{ fontSize: 20, fontWeight: 800 }}>
            Level {a.level}{xp.leveledUp ? <span className="gain"> ▲ from {xp.before.level}</span> : null}
          </div>
          <div className="muted">+{xp.gained} XP this party</div>
        </div>
      </div>
      <div className="xpbar" style={{ marginTop: 10 }}><i style={{ width: `${a.pct}%` }} /></div>
      <div className="muted" style={{ marginTop: 6 }}>
        {a.max ? 'Maxed out — the whole cast is yours.' : `${a.into} / ${a.need} XP to level ${a.level + 1}`}
      </div>
      {xp.unlocked?.length ? (
        <>
          <hr className="sep" />
          <div style={{ fontWeight: 800, marginBottom: 10 }}>🎉 Unlocked</div>
          <div className="unlocks">
            {xp.unlocked.map(c => (
              <div className="unlock-tile" key={c.id} style={{ '--a': c.accent }}>
                <window.CritterTile charId={c.id} size={72} />
                <div className="pn">{esc(c.name)}</div>
                <div className="pb">{esc(c.blurb)}</div>
              </div>
            ))}
          </div>
        </>
      ) : null}
    </div>
  );
}

function FinalView({ v, me, on, xp }) {
  const w = v.winner;
  const tied = (v.standings || []).filter(r => r.rank === 1);
  return (
    <>
      <div className="stage" style={{ minHeight: '34vh' }}>
        <div className="crown">👑</div>
        {w ? (
          <>
            <div className="winner-name">{tied.length > 1 ? tied.map(t => t.name).join(' & ') : esc(w.name)}</div>
            <div className="body">{w.score.toLocaleString()} points{tied.length > 1 ? ' — a dead heat' : ''}</div>
          </>
        ) : <div className="head">Party over</div>}
      </div>
      <Board v={v} me={me} title="Final" />
      <XpCard xp={xp} />
      {v.played?.length ? <div className="card"><h2>Played</h2><div className="muted">{v.played.join(' · ')}</div></div> : null}
      {v.isOwner ? (
        <div className="card">
          <button className="btn primary" onClick={() => on({ t: 'start' })}>Another round</button>
          <button className="btn ghost" style={{ marginTop: 10 }} onClick={() => on({ t: 'close' })}>Close the party</button>
        </div>
      ) : (
        <div className="card"><div className="muted center">Thanks for playing! Your score is saved.</div></div>
      )}
    </>
  );
}

/* --------------------------------------------------------------- lander -- */

/* What a scanned code gets: one field and one button.
 *
 * Connecting straight from the URL put people in the room as whatever name
 * happened to be in storage — "Player" for anyone who had never used the app,
 * which is everyone a QR is for. A roster of Players is no roster at all.
 *
 * Shown even when a name is remembered, prefilled: it is one tap to accept,
 * and the phone that scanned the code is not always owned by the person whose
 * name is on it. */
function JoinPrompt({ code, onJoin, onBack, err }) {
  const [name, setName] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem('mm.name') || '""'); } catch { return ''; }
  });
  const go = () => {
    const n = name.trim();
    if (!n) return;
    try { localStorage.setItem('mm.name', JSON.stringify(n)); } catch { /* private mode */ }
    onJoin(n);
  };
  return (
    <>
      <div className="stage" style={{ minHeight: '20vh' }}>
        <div className="huge">🎉</div>
        <div className="head">Joining {code}</div>
        <div className="body">One thing first.</div>
      </div>
      <div className="card">
        <h2>What should we call you?</h2>
        <input className="field" value={name} maxLength={14} autoFocus
               placeholder="your name"
               onChange={e => setName(e.target.value)}
               onKeyDown={e => { if (e.key === 'Enter') go(); }} />
        <button className="btn primary" style={{ marginTop: 10, width: '100%' }}
                disabled={!name.trim()} onClick={go}>
          Join the party
        </button>
        {err ? <div className="muted" style={{ marginTop: 10 }}>{err}</div> : null}
        <button className="btn ghost" style={{ marginTop: 10, width: '100%' }} onClick={onBack}>
          ← Back
        </button>
      </div>
    </>
  );
}

function Lander({ onHost, onJoin, err, busy }) {
  const [code, setCode] = React.useState('');
  const [name, setName] = React.useState(() => {
    try { return JSON.parse(localStorage.getItem('mm.name') || '""'); } catch { return ''; }
  });
  const save = () => { try { localStorage.setItem('mm.name', JSON.stringify(name.trim())); } catch { /* private */ } };

  return (
    <>
      <div className="stage" style={{ minHeight: '22vh' }}>
        <div className="huge">🎉</div>
        <div className="head">Party</div>
        <div className="body">Everyone's phone is a gamepad. Up to 8 play; the host plays too.</div>
      </div>

      <div className="card">
        <h2>Your name</h2>
        <input className="field" value={name} maxLength={14} placeholder="what should we call you?"
               onChange={e => setName(e.target.value)} onBlur={save} />
      </div>

      <div className="card">
        <h2>Join a party</h2>
        <input className="field" value={code} maxLength={4} placeholder="4-letter code"
               style={{ textTransform: 'uppercase', letterSpacing: 4, fontSize: 22, textAlign: 'center' }}
               onChange={e => setCode(e.target.value.toUpperCase().replace(/[^A-Z0-9]/g, ''))} />
        <button className="btn primary" style={{ marginTop: 10 }} disabled={code.length < 4}
                onClick={() => { save(); onJoin(code); }}>Join</button>
      </div>

      <div className="card">
        <h2>Host one</h2>
        <div className="btns2">
          <button className="btn green" disabled={busy} onClick={() => { save(); onHost('party'); }}>🎉 Party Pack</button>
          <button className="btn ghost" disabled={busy} onClick={() => { save(); onHost('quiz'); }}>❓ Quiz Night</button>
        </div>
        <div className="muted" style={{ marginTop: 10 }}>
          Hosting needs the host PIN, saved on the Ears page.
        </div>
      </div>

      {err ? <div className="card"><div className="muted" style={{ color: 'var(--red2)' }}>{err}</div></div> : null}
    </>
  );
}

/* ------------------------------------------------------- tournament round -- */

/* A round in progress — the game runs INSIDE the app, proxied onto the API
 * origin, so nobody leaves and the table is one tap away. This page lives on
 * a different origin than the proxy, so the frame's address is absolute. */
function RoundView({ v, me, on }) {
  const r = v.round || {};
  const [table, setTable] = React.useState(false);
  const src = r.key
    ? `${window.PH365_API_BASE || ''}/g/${encodeURIComponent(r.key)}${r.path || '/'}`
    : '';
  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="round-head">
        <span className="round-em">{r.emoji || '🎮'}</span>
        <div className="grow">
          <div className="round-name">{esc(r.name || 'Round')}</div>
          <div className="round-sub">Round {r.no || v.roundNo} · {esc(r.howto || r.blurb || '')}</div>
        </div>
        <button className="btn ghost sm" style={{ width: 'auto' }} onClick={() => setTable(t => !t)}>Table</button>
      </div>

      {src ? (
        <>
          <div className="gameframe">
            <iframe src={src} title={r.name || 'Game'}
                    allow="autoplay; clipboard-write; fullscreen"
                    referrerPolicy="no-referrer" />
          </div>
          <div className="row" style={{ marginTop: 8 }}>
            <a className="btn ghost sm" style={{ width: 'auto' }} href={src} target="_blank" rel="noopener noreferrer">Open in a tab</a>
            <div className="grow" />
            {r.license ? <span className="muted">{esc(r.name)} · {esc(r.license)}</span> : null}
          </div>
        </>
      ) : (
        <div className="card"><div className="muted">This round has no address configured yet.</div></div>
      )}

      {table ? <Board v={v} me={me} title="Table so far" /> : null}

      {v.isOwner && r.hostNote ? (
        <div className="card" style={{ marginTop: 12 }}>
          <h2>Host</h2><div className="muted">{esc(r.hostNote)}</div>
        </div>
      ) : null}

      {v.isOwner ? (
        <div className="sticky-cta">
          <button className="btn green" onClick={() => on({ t: 'scoring' })}>Round over — record the result</button>
        </div>
      ) : (
        <button className="btn ghost" style={{ marginTop: 12, width: '100%' }}
                onClick={() => on({ t: 'leave' })}>Leave the party</button>
      )}
    </>
  );
}

/* The host taps players in finishing order; anyone skipped still scores for
 * turning up. */
function ScoringView({ v, me, on }) {
  const [placing, setPlacing] = React.useState([]);
  if (!v.isOwner) {
    return (
      <>
        <div className="stage">
          <div className="huge">📝</div>
          <div className="head">Scoring</div>
          <div className="body">The host is recording who came where.</div>
        </div>
        <Board v={v} me={me} title="Table so far" />
        <button className="btn ghost" style={{ marginTop: 12, width: '100%' }}
                onClick={() => on({ t: 'leave' })}>Leave the party</button>
      </>
    );
  }
  const rest = v.players.filter(p => !placing.includes(p.pid));
  return (
    <>
      <HostStrip v={v} on={on} />
      <div className="center" style={{ marginBottom: 10 }}>
        <div style={{ fontSize: 21, fontWeight: 800 }}>Who came where?</div>
        <div className="muted">Tap them in finishing order. Anyone you skip still scores for turning up.</div>
      </div>

      <div className="card"><h2>Finishing order</h2>
        {placing.length ? (
          <div className="plist">
            {placing.map((pid, i) => {
              const p = v.players.find(x => x.pid === pid) || {};
              return (
                <div className={`prow r${i + 1}`} key={pid}>
                  <span className="rk">{i + 1}</span><span className="em">{p.emoji || ''}</span>
                  <div className="grow"><div className="nm">{esc(p.name || '')}</div></div>
                  <button className="btn ghost sm" style={{ width: 'auto', minHeight: 32, padding: '6px 10px' }}
                          onClick={() => setPlacing(pl => pl.filter(x => x !== pid))}>✕</button>
                </div>
              );
            })}
          </div>
        ) : <div className="muted">Nobody placed yet.</div>}
      </div>

      {rest.length ? (
        <div className="card"><h2>Tap to add</h2><div className="plist">
          {rest.map(p => (
            <button className="prow" key={p.pid} style={{ width: '100%', textAlign: 'left' }}
                    onClick={() => setPlacing(pl => [...pl, p.pid])}>
              <span className="em">{p.emoji}</span>
              <div className="grow"><div className="nm">{esc(p.name)}</div></div>
              <span className="muted">tap</span>
            </button>
          ))}
        </div></div>
      ) : null}

      <div className="sticky-cta">
        <button className="btn green" onClick={() => on({ t: 'result', order: placing })}>
          {placing.length ? 'Save the result' : 'Nobody placed — save anyway'}
        </button>
      </div>
    </>
  );
}

/* ------------------------------------------------------------ the screen -- */

window.PartyScreen = function PartyScreen() {
  const P = window.useParty();
  const { room: v, conn, err, me, profile, xp } = P;
  const [busy, setBusy] = React.useState(false);
  const [hostErr, setHostErr] = React.useState('');

  /* A scanned QR lands on /j/CODE; a shared link may use ?party=CODE. Both
   * mean the same thing: join this room as a player — but ask who they are
   * first rather than connecting as whatever was in storage. */
  const [scanned, setScanned] = React.useState(() => {
    try {
      const path = location.pathname.match(/^\/j\/([A-Za-z0-9]{4})/);
      const q = new URLSearchParams(location.search).get('party');
      return (path?.[1] || q || '').toUpperCase();
    } catch { return ''; }
  });

  const on = React.useCallback((msg) => {
    if (msg.t === 'close') { P.send({ t: 'end' }); P.disconnect(); return; }
    /* A guest walking out. Handled here, not on the server: their seat
     * survives (they can rejoin), but this phone is done with the party —
     * including the scanned code, or a reload would march them back in. */
    if (msg.t === 'leave') {
      P.disconnect();
      setScanned('');
      setScanRole('none');
      try { history.replaceState(null, '', '/'); } catch { /* file:// */ }
      return;
    }
    P.send(msg);
  }, [P]);

  /* A scanned code might be OUR OWN party (the ears' QR): check before
   * offering the guest prompt, and walk the owner straight in as host. */
  const [scanRole, setScanRole] = React.useState(scanned ? 'checking' : 'none');
  React.useEffect(() => {
    if (!scanned) return undefined;
    let dead = false;
    (async () => {
      const role = P.joinScanned ? await P.joinScanned(scanned) : 'player';
      if (!dead) setScanRole(role);
    })();
    return () => { dead = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [scanned]);

  const host = async (mode) => {
    setBusy(true); setHostErr('');
    try { await P.host(mode); } catch (e) { setHostErr(e.message); }
    setBusy(false);
  };

  const inner = (() => {
    if (!v) {
      if (conn === 'connecting') {
        return <div className="stage"><div className="spinner" /><div className="muted">connecting…</div></div>;
      }
      if (scanned && scanRole === 'checking') {
        return <div className="stage"><div className="spinner" /><div className="muted">checking whose party this is…</div></div>;
      }
      if (scanned && scanRole !== 'owner') {
        return <JoinPrompt code={scanned} err={err} onJoin={(nm) => {
          /* Name first so the join carries it, rather than connecting and
             renaming a moment later in front of everyone. */
          P.saveMe?.(nm);
          P.connect(scanned, 'player');
        }} onBack={() => {
          /* A scanned code is not a commitment. Clear the URL too, or a
             reload walks them straight back into this prompt. */
          setScanned('');
          setScanRole('none');
          try { history.replaceState(null, '', '/'); } catch { /* file:// */ }
        }} />;
      }
      return <Lander onHost={host} onJoin={c => P.connect(c, 'player')} err={hostErr || err} busy={busy} />;
    }

    switch (v.phase) {
      case 'lobby': return <Lobby v={v} me={me} on={on} joinBase={location.origin} />;
      case 'pick': return <PickView v={v} me={me} on={on} profile={profile} nowServer={P.nowServer} />;
      case 'vote': return <VoteView v={v} on={on} nowServer={P.nowServer} />;
      case 'intro': return <IntroView v={v} on={on} nowServer={P.nowServer} />;
      case 'scores': return <ScoresView v={v} me={me} on={on} nowServer={P.nowServer} />;
      case 'final': return <FinalView v={v} me={me} on={on} xp={xp} />;
      case 'standings': return (
        <>
          <HostStrip v={v} on={on} />
          <Board v={v} me={me} title="Table so far" />
          {v.isOwner ? (
            /* A reloaded host landed here with only the board and End
             * party — no way onward. The server has always allowed
             * standings -> lobby; give it its button. */
            <button className="btn" onClick={() => on({ t: 'lobby' })}>
              Pick another game
            </button>
          ) : (
            <>
              <div className="muted" style={{ textAlign: 'center', marginTop: 10 }}>
                waiting for the host to pick the next game…
              </div>
              <button className="btn ghost" style={{ marginTop: 12, width: '100%' }}
                      onClick={() => on({ t: 'leave' })}>Leave the party</button>
            </>
          )}
        </>
      );
      case 'round': return <RoundView v={v} me={me} on={on} />;
      case 'score': return <ScoringView v={v} me={me} on={on} />;
      case 'play': {
        const g = v.game;
        if (!g) return <div className="stage"><div className="spinner" /></div>;
        /* The arena is keyed on the game so a new game gets a new canvas and a
         * phase change inside one does not. */
        if (g.controller) {
          return <window.PartyArena key={g.key} v={v} me={me} send={P.send} isOwner={v.isOwner} skew={P.nowServer() - Date.now()} />;
        }
        return <window.PartyGameView v={v} me={me} on={on} nowServer={P.nowServer} HostStrip={HostStrip} Timer={Timer} />;
      }
      default:
        return <div className="stage"><div className="muted">{v.phase}…</div></div>;
    }
  })();

  const bare = v && v.phase === 'play' && v.game?.controller;

  /* A controller game is full-bleed: the stage is fixed to the viewport and
   * the thumbstick wants the whole bottom of the screen. The app's own nav and
   * top bar sit above it and land right on the JUMP button, so they are hidden
   * for the duration — this is the one screen that is a game rather than a
   * page. Body class rather than inline style so the rule can live in CSS with
   * the rest of the chrome. */
  React.useEffect(() => {
    document.body.classList.toggle('mm-arena-active', !!bare);
    return () => document.body.classList.remove('mm-arena-active');
  }, [bare]);

  return (
    <div className="mm-party">
      <div className={bare ? '' : 'screen'} style={bare ? null : { padding: 16, paddingBottom: 150 }}>
        {conn === 'lost' ? <div className="pill bad" style={{ marginBottom: 10 }}><span className="dot" />Reconnecting…</div> : null}
        {inner}
      </div>
    </div>
  );
};

})();
