// Desktop host TV view — Risograph direction.
// Wide-screen (≥1100px, fine pointer) rendition of the host UI. Every screen
// here is wired to the SAME state shape app-host.jsx already maintains for
// the phone host — we just present it big.
//
// Reuses the globals from theme.jsx: R, risoTex, RisoStamp, RisoConfetti.
// No new dependencies — QR uses qrcode-generator (already loaded in host.html).

(() => {
const R = window.R;
const risoTex = window.risoTex;
const RisoStamp = window.RisoStamp;
const RisoConfetti = window.RisoConfetti;

// ── Shell ──────────────────────────────────────────────────
function TvShell({ children, bg = R.paper }) {
  return (
    <div style={{
      width: '100%', minHeight: '100dvh', background: bg, color: R.ink,
      fontFamily: R.font, position: 'relative', overflow: 'hidden',
      display: 'flex', flexDirection: 'column',
      backgroundImage: risoTex('rgba(26,26,26,.05)', 3),
    }}>{children}</div>
  );
}

function TvHeader({ left, mid, right }) {
  return (
    <div style={{
      padding: '20px 36px', display: 'flex', alignItems: 'center',
      justifyContent: 'space-between', gap: 24,
      borderBottom: `2px solid ${R.ink}`,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
        <div style={{
          width: 32, height: 32, background: R.pink, border: `2px solid ${R.ink}`,
          boxShadow: `2px 2px 0 ${R.ink}`, position: 'relative',
        }}>
          <div style={{
            position: 'absolute', inset: 4, background: R.blue,
            mixBlendMode: 'multiply', opacity: .9,
          }} />
        </div>
        <div style={{ fontSize: 14, fontWeight: 900, letterSpacing: '.2em' }}>HITFOTHIT</div>
        {left && <div style={{ fontSize: 12, fontWeight: 700, color: R.muted, letterSpacing: '.16em', paddingLeft: 12, borderLeft: `1px solid ${R.dim}` }}>{left}</div>}
      </div>
      <div style={{ fontSize: 13, fontWeight: 800, letterSpacing: '.22em', color: R.muted }}>{mid}</div>
      <div style={{ fontSize: 13, fontWeight: 800, letterSpacing: '.16em', color: R.ink }}>{right}</div>
    </div>
  );
}

function TvBtn({ children, onClick, color = R.pink, fg = R.paper, size = 'lg', disabled = false }) {
  const big = size === 'lg';
  return (
    <button onClick={disabled ? undefined : onClick} disabled={disabled} style={{
      padding: big ? '20px 36px' : '12px 22px',
      fontSize: big ? 22 : 14, fontWeight: 900, letterSpacing: '.04em',
      background: disabled ? R.paper3 : color, color: disabled ? R.muted : fg,
      border: `2px solid ${R.ink}`, boxShadow: `5px 5px 0 ${R.ink}`,
      fontFamily: 'inherit', cursor: disabled ? 'not-allowed' : 'pointer',
      textTransform: 'uppercase',
      transition: 'transform .08s, box-shadow .08s',
    }}
    onPointerDown={(e) => { if (disabled) return; e.currentTarget.style.transform = 'translate(2px,2px)'; e.currentTarget.style.boxShadow = `3px 3px 0 ${R.ink}`; }}
    onPointerUp={(e) => { if (disabled) return; e.currentTarget.style.transform = 'translate(0,0)'; e.currentTarget.style.boxShadow = `5px 5px 0 ${R.ink}`; }}
    onPointerLeave={(e) => { if (disabled) return; e.currentTarget.style.transform = 'translate(0,0)'; e.currentTarget.style.boxShadow = `5px 5px 0 ${R.ink}`; }}
    >{children}</button>
  );
}

function TvOverprint({ children, size = 200, color1 = R.pink, color2 = R.blue, offset = 8 }) {
  return (
    <div style={{ position: 'relative', lineHeight: .85, letterSpacing: '-.04em', fontWeight: 900, fontSize: size }}>
      <div style={{ position: 'absolute', left: -offset, top: offset, color: color1, mixBlendMode: 'multiply', opacity: .9 }}>{children}</div>
      <div style={{ color: color2, mixBlendMode: 'multiply', opacity: .9 }}>{children}</div>
    </div>
  );
}

// Real QR via qrcode-generator (already loaded in host.html).
function TvQR({ url, size = 220 }) {
  const [src, setSrc] = React.useState(null);
  React.useEffect(() => {
    if (!url || !window.qrcode) return;
    try {
      const qr = window.qrcode(0, 'M');
      qr.addData(url);
      qr.make();
      setSrc(qr.createDataURL(6, 2));
    } catch (e) { /* ignore */ }
  }, [url]);
  return (
    <div style={{
      width: size + 20, height: size + 20, padding: 10,
      background: R.paper, border: `3px solid ${R.ink}`, boxShadow: `5px 5px 0 ${R.ink}`,
    }}>
      {src
        ? <img src={src} alt="Scan to join" style={{ width: '100%', height: '100%', imageRendering: 'pixelated', display: 'block' }} />
        : <div style={{ width: '100%', height: '100%', background: R.paper2 }} />
      }
    </div>
  );
}

// Player chip — pulls from real {id,name,color,score,locked}.
function TvPlayerCard({ p, status, big = false, small = false, score, locked = false, idx = 0 }) {
  const color = p.color || R.blue;
  // Three sizes: big (centerpiece on final screen), default (8-player grids),
  // small (>16 players — compact 4-col grid).
  const pad = big ? 18 : small ? 8 : 14;
  const avSz = big ? 52 : small ? 30 : 44;
  const nameSize = big ? 22 : small ? 13 : 18;
  return (
    <div style={{
      padding: pad,
      background: locked ? color : R.paper2,
      border: `2px solid ${R.ink}`, boxShadow: `4px 4px 0 ${R.ink}`,
      backgroundImage: locked ? risoTex('rgba(26,26,26,.08)', 3) : 'none',
      animation: `tvcard-in .35s ${Math.min(idx, 24) * 0.04}s backwards`,
      display: 'flex', alignItems: 'center', gap: small ? 8 : 14, minWidth: 0,
    }}>
      <div style={{
        width: avSz, height: avSz, flexShrink: 0,
        background: color, border: `2px solid ${R.ink}`,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: big ? 22 : small ? 14 : 18, fontWeight: 900, color: R.ink,
      }}>{(p.name || '?')[0].toUpperCase()}</div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: nameSize, fontWeight: 900, letterSpacing: '-.01em', lineHeight: 1.1, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
          {(p.name || '—').toUpperCase()}
        </div>
        {status && !small && (
          <div style={{ fontSize: 10, fontWeight: 800, letterSpacing: '.18em', marginTop: 2, color: locked ? R.ink : R.muted }}>
            {status}
          </div>
        )}
      </div>
      {score != null && (
        <div style={{
          fontSize: big ? 32 : small ? 16 : 24, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
          color: R.ink, padding: small ? '2px 6px' : '4px 10px', background: R.paper,
          border: `2px solid ${R.ink}`,
        }}>{score}</div>
      )}
    </div>
  );
}

// ── 0. SPLASH (host entry on desktop) ────────────────────────
function TvSplash({ onCreate, onResume, savedRoom, configError, mode, setMode, fullAccess, hostPlays, setHostPlays, hostName, setHostName }) {
  const showSpotify = fullAccess !== false;
  const canCreate = (mode !== 'spotify' || !configError) && (!hostPlays || (hostName || '').trim().length >= 2);
  return (
    <TvShell>
      <RisoStamp size={600} color={R.pink} top={-200} left={-200} duration={32} />
      <RisoStamp size={500} color={R.blue} top={300} left={950} duration={28} delay={1.5} />

      <TvHeader left="Host on this device" mid="" right="" />

      <div style={{ flex: 1, padding: '40px 80px', display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 60, alignItems: 'center', position: 'relative', zIndex: 2 }}>
        <div>
          <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted, marginBottom: 14 }}>HITFOTHIT · DESKTOP HOST</div>
          <TvOverprint size={170}>NAME<br/>THE<br/>YEAR.</TvOverprint>
          <div style={{ marginTop: 24, fontSize: 17, color: R.muted, fontWeight: 600, lineHeight: 1.5, maxWidth: 540 }}>
            Open a room on this laptop. Players join from their phone — code or QR. Music plays from your browser tab; no Spotify device juggling.
          </div>

          {configError && mode === 'spotify' && (
            <div style={{ marginTop: 18, padding: 12, background: R.pink, color: R.paper, fontSize: 12, fontWeight: 700, border: `2px solid ${R.ink}`, maxWidth: 540 }}>
              {configError}
            </div>
          )}

          <div style={{ marginTop: 28, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            <div style={{ display: 'flex', gap: 8 }}>
              <div onClick={() => setMode && setMode('preview')} style={{
                padding: '10px 16px', cursor: 'pointer',
                background: mode === 'preview' ? R.green : R.paper2, color: R.ink,
                border: `2px solid ${R.ink}`,
                boxShadow: mode === 'preview' ? `4px 4px 0 ${R.ink}` : `2px 2px 0 ${R.ink}`,
                fontSize: 12, fontWeight: 900, letterSpacing: '.06em',
              }}>FREE · 30s</div>
              {showSpotify && (
                <div onClick={() => setMode && setMode('spotify')} style={{
                  padding: '10px 16px', cursor: 'pointer',
                  background: mode === 'spotify' ? R.blue : R.paper2, color: mode === 'spotify' ? R.paper : R.ink,
                  border: `2px solid ${R.ink}`,
                  boxShadow: mode === 'spotify' ? `4px 4px 0 ${R.ink}` : `2px 2px 0 ${R.ink}`,
                  fontSize: 12, fontWeight: 900, letterSpacing: '.06em',
                }}>SPOTIFY · FULL</div>
              )}
            </div>
          </div>

          <div style={{ marginTop: 22, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
            <label style={{
              display: 'flex', alignItems: 'center', gap: 10,
              padding: '8px 12px', background: hostPlays ? R.yellow : R.paper2,
              border: `2px solid ${R.ink}`, boxShadow: `2px 2px 0 ${R.ink}`,
              cursor: 'pointer', fontSize: 12, fontWeight: 800, letterSpacing: '.04em',
            }}>
              <span style={{
                width: 16, height: 16, border: `2px solid ${R.ink}`,
                background: hostPlays ? R.ink : 'transparent',
                color: R.paper, display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                fontSize: 10,
              }}>{hostPlays ? '✓' : ''}</span>
              <input type="checkbox" checked={!!hostPlays} onChange={(e) => setHostPlays && setHostPlays(e.target.checked)} style={{ position: 'absolute', opacity: 0, pointerEvents: 'none' }} />
              I'M PLAYING TOO
            </label>
            {hostPlays && (
              <input
                value={hostName || ''}
                onChange={(e) => setHostName && setHostName(e.target.value.slice(0, 12))}
                placeholder="Your name"
                style={{
                  padding: '8px 12px', fontSize: 14, fontWeight: 800, color: R.ink,
                  background: R.paper2, border: `2px solid ${R.ink}`, boxShadow: `2px 2px 0 ${R.ink}`,
                  outline: 'none', fontFamily: 'inherit',
                }}
              />
            )}
          </div>

          <div style={{ marginTop: 32, display: 'flex', gap: 14, flexWrap: 'wrap' }}>
            <TvBtn color={R.pink} disabled={!canCreate} onClick={onCreate}>CREATE A ROOM →</TvBtn>
            {savedRoom && onResume && (
              <TvBtn color={R.paper2} fg={R.ink} size="sm" onClick={onResume}>RESUME · {savedRoom}</TvBtn>
            )}
            <a href="/" style={{ textDecoration: 'none' }}>
              <TvBtn color={R.paper2} fg={R.ink} size="sm">I'M JOINING INSTEAD</TvBtn>
            </a>
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted }}>WHY DESKTOP HOST?</div>
          {[
            ['🖥', 'Big room code', 'Whole room sees it across the table.'],
            ['🔊', 'Browser plays directly', 'No separate Spotify device needed.'],
            ['📺', 'Cast to TV', 'Plug in HDMI or AirPlay — looks great big.'],
            ['📱', 'Phones to guess', 'Players scan the QR — no app install.'],
          ].map(([icon, head, sub], i) => (
            <div key={i} style={{
              padding: '14px 16px', background: R.paper2, border: `2px solid ${R.ink}`,
              boxShadow: `3px 3px 0 ${R.ink}`,
              display: 'flex', alignItems: 'center', gap: 14,
              animation: `tvcard-in .35s ${i * 0.07}s backwards`,
            }}>
              <div style={{ fontSize: 28, lineHeight: 1, width: 36, textAlign: 'center' }}>{icon}</div>
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 15, fontWeight: 900, letterSpacing: '-.01em' }}>{head}</div>
                <div style={{ fontSize: 12, fontWeight: 700, color: R.muted, marginTop: 2 }}>{sub}</div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </TvShell>
  );
}

// ── 1. CONNECT SPOTIFY ───────────────────────────────────────
function TvConnect({ onConnect, busy, error }) {
  return (
    <TvShell>
      <TvHeader left="Setup · 01 of 02" mid="" right="" />
      <div style={{ flex: 1, padding: '60px 80px', display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: 80, alignItems: 'center' }}>
        <div>
          <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted, marginBottom: 16 }}>YOU NEED A SPOTIFY PREMIUM ACCOUNT</div>
          <TvOverprint size={130}>HOOK UP<br/>SPOTIFY.</TvOverprint>
          <div style={{ marginTop: 30, fontSize: 18, color: R.muted, fontWeight: 600, lineHeight: 1.5, maxWidth: 520 }}>
            We stream tracks straight from Spotify. Players guess from their phone. Premium-only — that's a Spotify rule for picking exact tracks.
          </div>
          {error && (
            <div style={{ marginTop: 18, padding: 12, background: R.pink, color: R.paper, fontSize: 13, fontWeight: 700, border: `2px solid ${R.ink}`, maxWidth: 520 }}>
              {error}
            </div>
          )}
          <div style={{ marginTop: 40 }}>
            <TvBtn color={busy ? R.paper3 : R.green} fg={R.ink} disabled={busy} onClick={onConnect}>
              {busy ? 'CONNECTING…' : '🎧  Connect Spotify'}
            </TvBtn>
          </div>
        </div>
        <div style={{ display: 'flex', justifyContent: 'center' }}>
          <div style={{
            width: 320, height: 320,
            background: R.ink, color: R.paper,
            border: `3px solid ${R.ink}`, boxShadow: `8px 8px 0 ${R.pink}`,
            display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center',
            padding: 30, textAlign: 'center', gap: 14,
            backgroundImage: risoTex('rgba(245,239,226,.05)', 4),
          }}>
            <div style={{ fontSize: 88, lineHeight: 1 }}>🎧</div>
            <div style={{ fontSize: 14, fontWeight: 900, letterSpacing: '.22em', color: R.yellow }}>WHY PREMIUM?</div>
            <div style={{ fontSize: 13, fontWeight: 600, color: R.paper, opacity: .85, lineHeight: 1.45 }}>
              Spotify only lets Premium accounts trigger specific tracks via their Web API. Free accounts can't.
            </div>
          </div>
        </div>
      </div>
    </TvShell>
  );
}

// ── 2. PICK PLAYLIST ─────────────────────────────────────────
function TvPlaylist({ onPick, onPickCurated, curatedPresets, busy, error, onReauth }) {
  // URL paste removed by request — curated presets are the canonical path.
  // The unused `onPick` prop kept so app-host's existing wiring still works.
  void onPick;
  return (
    <TvShell>
      <TvHeader left="Setup · 02 of 02" mid="" right={onReauth ? <span style={{ cursor: 'pointer' }} onClick={onReauth}>↻ RECONNECT</span> : ''} />
      <div style={{ flex: 1, padding: '50px 80px', display: 'flex', flexDirection: 'column', gap: 24, overflow: 'auto' }}>
        <div>
          <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted, marginBottom: 14 }}>PICK A CURATED MIX</div>
          <TvOverprint size={100}>PICK A PLAYLIST.</TvOverprint>
        </div>

        {error && (
          <div style={{ padding: 12, background: R.pink, color: R.paper, fontSize: 13, fontWeight: 700, lineHeight: 1.5, border: `2px solid ${R.ink}` }}>{error}</div>
        )}

        {(curatedPresets || []).length > 0 && (
          <>
            <div style={{ fontSize: 11, fontWeight: 900, letterSpacing: '.22em', color: R.muted }}>CURATED · YEARS VERIFIED</div>
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 14 }}>
              {curatedPresets.map((p, i) => {
                const palette = [R.yellow, R.pink, R.blue, R.green];
                const color = palette[i % palette.length];
                return (
                  <div key={p.slug} onClick={() => !busy && onPickCurated(p.slug)} style={{
                    padding: '18px 16px', cursor: busy ? 'wait' : 'pointer',
                    background: color, border: `2px solid ${R.ink}`,
                    boxShadow: `3px 3px 0 ${R.ink}`,
                    transition: 'all .15s',
                    backgroundImage: risoTex('rgba(26,26,26,.06)', 3),
                  }}>
                    <div style={{ fontSize: 18, fontWeight: 900, lineHeight: 1.15, letterSpacing: '-.01em' }}>{p.name}</div>
                    <div style={{ fontSize: 11, fontWeight: 700, marginTop: 6, color: R.muted, fontFamily: 'ui-monospace, monospace' }}>{p.trackCount} TRACKS</div>
                  </div>
                );
              })}
            </div>
          </>
        )}
      </div>
    </TvShell>
  );
}

// ── 3. LOBBY — giant code + QR + player grid ────────────────
function TvLobby({ code, players, joinUrl, onStart, onChangePlaylist, playlistName, mode, scoringRule, onChangeScoringRule, roundTimerSec, onChangeRoundTimer }) {
  // Show up to 8 placeholder "WAITING…" slots so a near-empty lobby still
  // feels populated; beyond that, render only the real player count (no
  // dangling empties). Server enforces no hard cap — sockets scale far
  // higher than the original 8-player design assumed.
  const slots = players.length < 8 ? 8 : players.length;
  const cols = players.length > 16 ? 4 : players.length > 8 ? 3 : 2;
  const cardSize = players.length > 16 ? 'sm' : 'md';
  return (
    <TvShell>
      <RisoStamp size={400} color={R.pink} top={-160} left={-160} duration={32} />
      <RisoStamp size={300} color={R.blue} top={420} left={1050} duration={28} delay={1} />

      <TvHeader
        left="Room open"
        mid={`${players.length} ${players.length === 1 ? 'player' : 'players'} · waiting on host`}
        right={playlistName ? <span style={{ cursor: onChangePlaylist ? 'pointer' : 'default' }} onClick={onChangePlaylist}>{playlistName} · CHANGE →</span> : ''}
      />

      <div style={{ flex: 1, padding: '40px 60px', display: 'grid', gridTemplateColumns: '1.1fr 1.4fr', gap: 60, alignItems: 'start', position: 'relative', zIndex: 2 }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
          <div>
            <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted }}>JOIN AT</div>
            <div style={{ fontSize: 22, fontWeight: 900, fontFamily: 'ui-monospace, monospace', letterSpacing: '-.02em', marginTop: 4, wordBreak: 'break-all' }}>
              {joinUrl ? joinUrl.replace(/^https?:\/\//, '') : '—'}
            </div>
          </div>

          <div>
            <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted, marginBottom: 8 }}>CODE</div>
            <TvOverprint size={200}>{code || '—'}</TvOverprint>
          </div>

          <div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
            <TvQR url={joinUrl} size={160} />
            <div style={{ fontSize: 13, fontWeight: 800, color: R.muted, lineHeight: 1.5, maxWidth: 200 }}>
              Scan to drop in. Or open the URL on any phone and type the code.
            </div>
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
            <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted }}>PLAYERS</div>
            <div style={{ fontSize: 22, fontWeight: 900, fontFamily: 'ui-monospace, monospace' }}>{players.length}</div>
          </div>
          <div style={{
            display: 'grid',
            gridTemplateColumns: `repeat(${cols}, 1fr)`,
            gap: 12,
            maxHeight: 420, overflow: 'auto',
          }}>
            {Array.from({ length: slots }).map((_, i) => {
              const p = players[i];
              return p
                ? <TvPlayerCard key={p.id} p={p} status="● HERE" idx={i} small={cardSize === 'sm'} />
                : <div key={`empty-${i}`} style={{
                    padding: cardSize === 'sm' ? 8 : 14, border: `2px dashed ${R.dim}`,
                    display: 'flex', alignItems: 'center', gap: 10, opacity: .4, minHeight: cardSize === 'sm' ? 48 : 76,
                  }}>
                    <div style={{ width: cardSize === 'sm' ? 28 : 44, height: cardSize === 'sm' ? 28 : 44, border: `2px dashed ${R.dim}`, flexShrink: 0 }} />
                    <div style={{ fontSize: cardSize === 'sm' ? 11 : 14, fontWeight: 800, color: R.muted, letterSpacing: '.14em' }}>WAITING…</div>
                  </div>;
            })}
          </div>

          {/* Compact settings strip */}
          {(onChangeScoringRule || onChangeRoundTimer) && (
            <div style={{ display: 'flex', gap: 12, marginTop: 8, flexWrap: 'wrap' }}>
              {onChangeScoringRule && (
                <div style={{ flex: 1, minWidth: 200, padding: '8px 12px', background: R.paper2, border: `2px solid ${R.ink}` }}>
                  <div style={{ fontSize: 9, fontWeight: 900, letterSpacing: '.2em', color: R.muted }}>SCORING</div>
                  <div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
                    {[['closest','CLOSEST'],['podium','3·2·1'],['banded','BANDED']].map(([rule, label]) => {
                      const on = scoringRule === rule;
                      return (
                        <span key={rule} onClick={() => onChangeScoringRule(rule)} style={{
                          flex: 1, padding: '6px 0', textAlign: 'center', cursor: 'pointer',
                          background: on ? R.ink : R.paper, color: on ? R.paper : R.ink,
                          border: `2px solid ${R.ink}`,
                          fontSize: 11, fontWeight: 900, letterSpacing: '.06em',
                        }}>{label}</span>
                      );
                    })}
                  </div>
                </div>
              )}
              {onChangeRoundTimer && (
                <div style={{ flex: 1, minWidth: 200, padding: '8px 12px', background: R.paper2, border: `2px solid ${R.ink}` }}>
                  <div style={{ fontSize: 9, fontWeight: 900, letterSpacing: '.2em', color: R.muted }}>TIMER</div>
                  <div style={{ display: 'flex', gap: 4, marginTop: 6 }}>
                    {[[null,'OFF'],[15,'15s'],[20,'20s'],[30,'30s']].map(([sec, label]) => {
                      const on = (roundTimerSec || null) === sec;
                      return (
                        <span key={label} onClick={() => onChangeRoundTimer(sec)} style={{
                          flex: 1, padding: '6px 0', textAlign: 'center', cursor: 'pointer',
                          background: on ? R.ink : R.paper, color: on ? R.paper : R.ink,
                          border: `2px solid ${R.ink}`,
                          fontSize: 11, fontWeight: 900, letterSpacing: '.1em',
                        }}>{label}</span>
                      );
                    })}
                  </div>
                </div>
              )}
            </div>
          )}

          <div style={{ marginTop: 14, display: 'flex', justifyContent: 'flex-end' }}>
            <TvBtn color={players.length >= 1 ? R.pink : R.paper3} disabled={players.length < 1} onClick={onStart}>
              {players.length >= 1 ? 'START ROUND 01 →' : 'NEED A PLAYER'}
            </TvBtn>
          </div>
        </div>
      </div>

      <style>{`@keyframes tvcard-in { 0% { opacity: 0; transform: translateX(-12px) } 100% { opacity: 1; transform: translateX(0) } }`}</style>
    </TvShell>
  );
}

// Countdown chip — re-renders every 250ms while the deadline is in the future.
function TvCountdown({ deadline }) {
  const [now, setNow] = React.useState(Date.now());
  React.useEffect(() => {
    if (!deadline) return;
    const id = setInterval(() => setNow(Date.now()), 250);
    return () => clearInterval(id);
  }, [deadline]);
  if (!deadline) return <span style={{ fontSize: 13, fontWeight: 800, letterSpacing: '.2em', color: R.muted }}>NO TIMER</span>;
  const remaining = Math.max(0, Math.ceil((deadline - now) / 1000));
  const warn = remaining <= 5;
  return (
    <span style={{
      fontSize: 32, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
      color: warn ? R.pink : R.ink,
      animation: warn && remaining > 0 ? 'tv-pulse .6s ease-in-out infinite' : 'none',
    }}>0:{String(remaining).padStart(2, '0')}</span>
  );
}

// ── 5. GUESSING — vinyl + lock grid + timer ──────────────────
function TvGuessing({ round, song, players, onReveal, onReplay, roundDeadline, hostPlays }) {
  const lockedCount = players.filter(p => p.locked).length;
  const allIn = players.length > 0 && lockedCount === players.length;
  const cols = players.length > 16 ? 3 : 2;
  const cardSmall = players.length > 16;
  // TV is visible to the whole room. Never put the song title or artist on
  // it — players would Google it. The host can see the full track info on
  // their phone (Spotify Connect controls). If the host is playing too we
  // also call that out so they know to guess from their phone.
  return (
    <TvShell>
      <TvHeader
        left={`Round ${String(round).padStart(2, '0')}`}
        mid={allIn ? 'ALL PLAYERS LOCKED · READY TO REVEAL' : `${lockedCount} / ${players.length} LOCKED IN`}
        right={<TvCountdown deadline={roundDeadline} />}
      />

      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1.2fr 1fr', gap: 40, padding: '36px 40px', minHeight: 0 }}>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
          <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.pink }}>
            ● MYSTERY TRACK{hostPlays ? " · YOU'RE GUESSING TOO" : ''}
          </div>

          <div style={{
            padding: 30, background: R.paper2, border: `2px solid ${R.ink}`,
            boxShadow: `6px 6px 0 ${R.ink}`,
            display: 'flex', alignItems: 'center', gap: 30,
            backgroundImage: risoTex('rgba(26,26,26,.05)', 3),
          }}>
            <div style={{
              width: 180, height: 180, borderRadius: '50%', flexShrink: 0,
              background: `repeating-radial-gradient(${R.ink} 0 4px, #2a2420 4px 7px)`,
              border: `3px solid ${R.ink}`, position: 'relative',
              animation: 'tv-spin 8s linear infinite',
              boxShadow: 'inset 0 0 0 4px rgba(255,255,255,.05), 0 6px 16px rgba(0,0,0,.2)',
            }}>
              <div style={{
                position: 'absolute', top: '50%', left: '50%', width: 56, height: 56,
                marginLeft: -28, marginTop: -28, borderRadius: '50%',
                background: R.pink, border: `4px solid ${R.paper}`, boxShadow: `inset 0 0 0 4px ${R.ink}`,
              }} />
            </div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 14, fontWeight: 900, letterSpacing: '.2em', color: R.muted }}>
                MYSTERY TRACK
              </div>
              <div style={{ fontSize: 44, fontWeight: 900, letterSpacing: '-.02em', lineHeight: 1.05, marginTop: 8 }}>
                ?  ?  ?
              </div>
              <div style={{ fontSize: 14, fontWeight: 700, color: R.muted, marginTop: 6, lineHeight: 1.4 }}>
                {hostPlays
                  ? "You're a player this round — guess from your phone."
                  : 'Players guess on their phones. You can see the track on yours.'}
              </div>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 18 }}>
                {[18, 32, 24, 38, 20, 28, 36].map((h, i) => (
                  <div key={i} style={{
                    width: 6, height: h, background: R.blue,
                    animation: `tv-bar ${0.5 + (i % 3) * 0.2}s ${i * 0.06}s ease-in-out infinite alternate`,
                  }} />
                ))}
              </div>
            </div>
          </div>

          <div style={{ display: 'flex', gap: 12 }}>
            {onReplay && <TvBtn size="sm" color={R.paper2} fg={R.ink} onClick={onReplay}>↻ Replay</TvBtn>}
            <div style={{ flex: 1 }} />
            <TvBtn color={allIn ? R.green : R.pink} fg={allIn ? R.ink : R.paper} onClick={onReveal}>
              {allIn ? 'REVEAL YEAR  →' : 'REVEAL NOW →'}
            </TvBtn>
          </div>
        </div>

        <div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
            <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted }}>WAITING ON</div>
            <div style={{ fontSize: 22, fontWeight: 900, fontFamily: 'ui-monospace, monospace' }}>{lockedCount}/{players.length}</div>
          </div>
          <div style={{
            display: 'grid', gridTemplateColumns: `repeat(${cols}, 1fr)`, gap: 10,
            overflow: 'auto', paddingBottom: 16,
          }}>
            {players.map((p, i) => (
              <TvPlayerCard key={p.id} p={p} status={p.locked ? '● LOCKED' : '○ THINKING…'} locked={!!p.locked} idx={i} small={cardSmall} />
            ))}
          </div>
        </div>
      </div>
      <style>{`
        @keyframes tv-spin { 100% { transform: rotate(360deg) } }
        @keyframes tv-bar { 0% { transform: scaleY(.3) } 100% { transform: scaleY(1) } }
        @keyframes tv-pulse { 0%,100% { transform: scale(1) } 50% { transform: scale(1.1) } }
      `}</style>
    </TvShell>
  );
}

// ── 6. REVEAL — giant year stamp + track ─────────────────────
function TvReveal({ round, song, guesses = [], players = [], myPid, onNext, onEnd }) {
  const [display, setDisplay] = React.useState(song?.year ? Math.max(1955, song.year - 25) : 1955);
  const [step, setStep] = React.useState(0);

  React.useEffect(() => {
    if (!song?.year) return;
    setStep(0);
    setDisplay(Math.max(1955, song.year - 25));
    const t1 = setTimeout(() => setStep(1), 200);
    return () => clearTimeout(t1);
  }, [song?.year]);

  React.useEffect(() => {
    if (step !== 1 || !song?.year) return;
    const start0 = Math.max(1955, song.year - 25);
    const target = song.year;
    const total = 1500;
    const start = performance.now();
    let raf;
    const tick = (now) => {
      const t = Math.min(1, (now - start) / total);
      const eased = 1 - Math.pow(1 - t, 3);
      setDisplay(Math.round(start0 + (target - start0) * eased));
      if (t < 1) raf = requestAnimationFrame(tick);
      else setStep(2);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [step, song?.year]);

  const exact = guesses.filter(g => g.points === 2);
  const sortedGuesses = [...guesses].sort((a, b) => (b.points - a.points) || ((a.dist ?? 99) - (b.dist ?? 99)));
  const sortedPlayers = [...players].sort((a, b) => (b.score || 0) - (a.score || 0));

  return (
    <TvShell>
      {step === 2 && exact.length > 0 && <RisoConfetti count={120} />}
      <TvHeader left={`Round ${String(round).padStart(2, '0')} reveal`} mid="THE YEAR WAS" right="" />

      <div style={{ flex: 1, display: 'grid', gridTemplateColumns: '1.1fr 1fr', gap: 36, padding: '24px 40px 28px', minHeight: 0 }}>
        <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 22, position: 'relative', zIndex: 2 }}>
          <div style={{ position: 'relative', textAlign: 'center' }}>
            <div style={{
              position: 'absolute', left: '50%', top: 14, transform: 'translateX(calc(-50% - 10px))',
              fontSize: 280, fontWeight: 900, color: R.pink, lineHeight: .82,
              letterSpacing: '-.05em', mixBlendMode: 'multiply', opacity: .9,
              fontVariantNumeric: 'tabular-nums', fontFamily: 'ui-monospace, monospace',
            }}>{display}</div>
            <div style={{
              fontSize: 280, fontWeight: 900, color: R.blue, lineHeight: .82,
              letterSpacing: '-.05em', mixBlendMode: 'multiply', opacity: .9,
              fontVariantNumeric: 'tabular-nums', fontFamily: 'ui-monospace, monospace',
              animation: step === 2 ? 'tv-stamp .55s cubic-bezier(.2,1.4,.4,1)' : 'none',
            }}>{display}</div>
          </div>

          {song && (
            <div style={{
              padding: '20px 28px', background: R.yellow, border: `3px solid ${R.ink}`,
              boxShadow: `6px 6px 0 ${R.ink}`,
              display: 'flex', alignItems: 'center', gap: 22, maxWidth: 560,
            }}>
              <div style={{
                width: 64, height: 64, background: R.ink,
                backgroundImage: `repeating-linear-gradient(0deg, ${R.pink} 0 5px, ${R.ink} 5px 11px)`,
                border: `3px solid ${R.ink}`, flexShrink: 0,
              }} />
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 11, fontWeight: 900, letterSpacing: '.22em', color: R.ink }}>THE TRACK</div>
                <div style={{ fontSize: 26, fontWeight: 900, letterSpacing: '-.02em', lineHeight: 1.1, marginTop: 4, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{song.title}</div>
                <div style={{ fontSize: 15, fontWeight: 700, color: R.muted, marginTop: 2, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{song.artist}</div>
              </div>
            </div>
          )}

          <div style={{ display: 'flex', gap: 12, marginTop: 6 }}>
            <TvBtn color={R.paper2} fg={R.ink} size="sm" onClick={onEnd}>END GAME</TvBtn>
            <TvBtn color={R.pink} onClick={onNext}>NEXT ROUND →</TvBtn>
          </div>
        </div>

        {/* Right column: round + total tally */}
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12, minHeight: 0, overflow: 'auto', paddingRight: 4 }}>
          <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted }}>ROUND RESULTS</div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
            {sortedGuesses.map((g, i) => {
              const self = myPid && g.id === myPid;
              return (
                <div key={g.id} style={{
                  padding: '10px 14px',
                  background: g.points === 2 ? R.yellow : g.points === 1 ? R.green : R.paper2,
                  border: `${self ? 3 : 2}px solid ${R.ink}`,
                  boxShadow: `3px 3px 0 ${R.ink}`,
                  display: 'flex', alignItems: 'center', gap: 12,
                  animation: `tv-row .35s ${i * 0.06}s backwards`,
                }}>
                  <div style={{ width: 24, height: 24, background: g.color || R.blue, border: `2px solid ${R.ink}`, flexShrink: 0 }} />
                  <span style={{ flex: 1, fontSize: 15, fontWeight: 800, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
                    {g.name}
                    {self && <span style={{ marginLeft: 6, fontSize: 9, background: R.ink, color: R.paper, padding: '2px 5px', letterSpacing: '.16em' }}>YOU</span>}
                  </span>
                  <span style={{ fontSize: 13, fontWeight: 700, fontFamily: 'ui-monospace, monospace' }}>{g.guess ?? '—'}</span>
                  <span style={{
                    fontSize: 14, fontWeight: 900, fontFamily: 'ui-monospace, monospace',
                    padding: '2px 10px', minWidth: 36, textAlign: 'center',
                    background: g.points === 2 ? R.pink : 'transparent',
                    color: g.points === 2 ? R.paper : R.ink,
                    border: `2px solid ${R.ink}`,
                  }}>+{g.points}</span>
                </div>
              );
            })}
          </div>

          {sortedPlayers.length > 0 && (
            <>
              <div style={{
                height: 4, marginTop: 6, marginBottom: 4,
                backgroundImage: `repeating-linear-gradient(90deg, ${R.ink} 0 10px, transparent 10px 16px)`,
              }} />
              <div style={{ fontSize: 13, fontWeight: 900, letterSpacing: '.3em', color: R.muted }}>TOTAL · AFTER {String(round).padStart(2, '0')}</div>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                {sortedPlayers.map((p, i) => (
                  <div key={p.id} style={{
                    padding: '6px 12px',
                    background: i === 0 ? R.yellow : R.paper2,
                    border: `2px solid ${R.ink}`,
                    display: 'flex', alignItems: 'center', gap: 10,
                  }}>
                    <span style={{ fontSize: 13, fontWeight: 900, width: 18, color: i === 0 ? R.pink : R.ink }}>{i + 1}</span>
                    <div style={{ width: 14, height: 14, background: p.color || R.blue, border: `2px solid ${R.ink}` }} />
                    <span style={{ flex: 1, fontSize: 13, fontWeight: 800, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</span>
                    <span style={{ fontSize: 16, fontWeight: 900, fontFamily: 'ui-monospace, monospace' }}>{p.score || 0}</span>
                  </div>
                ))}
              </div>
            </>
          )}
        </div>
      </div>

      <style>{`
        @keyframes tv-stamp { 0% { transform: scale(2.4) rotate(-8deg); opacity: 0 } 70% { transform: scale(.97) rotate(2deg); opacity: 1 } 100% { transform: scale(1) rotate(0); opacity: 1 } }
        @keyframes tv-row { 0% { opacity: 0; transform: translateX(-12px) } 100% { opacity: 1; transform: translateX(0) } }
      `}</style>
    </TvShell>
  );
}

// ── 9. FINAL ─────────────────────────────────────────────────
function TvFinal({ players, onAgain, onNewGame }) {
  const sorted = [...players].sort((a, b) => (b.score || 0) - (a.score || 0));
  const top = sorted[0];
  const topScore = top?.score || 0;
  const topPlayers = sorted.filter(p => (p.score || 0) === topScore);
  const isTie = topPlayers.length > 1;
  const headlineText = topPlayers.map(p => (p.name || '—').toUpperCase()).join(' · ');
  const headlineSize = topPlayers.length >= 3 ? 100 : topPlayers.length === 2 ? 140 : 200;
  return (
    <TvShell>
      <RisoConfetti count={150} />
      <RisoStamp size={600} color={R.yellow} top={-180} left={-150} duration={26} />
      <RisoStamp size={500} color={R.pink} top={300} left={1000} duration={30} delay={1.5} />

      <TvHeader left="Game over" mid={isTie ? `${topPlayers.length}-WAY TIE` : 'WINNER'} right="" />

      <div style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 24, position: 'relative', zIndex: 5 }}>
        <div style={{ fontSize: 14, fontWeight: 900, letterSpacing: '.32em', color: R.muted }}>FINAL · {sorted.length} {sorted.length === 1 ? 'PLAYER' : 'PLAYERS'}</div>
        <TvOverprint size={headlineSize}>{headlineText || '—'}</TvOverprint>
        <div style={{
          padding: '12px 28px', background: R.ink, color: R.paper,
          border: `3px solid ${R.ink}`, boxShadow: `6px 6px 0 ${R.pink}`,
          display: 'flex', alignItems: 'baseline', gap: 20,
        }}>
          <div style={{ fontSize: 16, fontWeight: 900, letterSpacing: '.18em', color: R.yellow }}>FINAL</div>
          <div style={{ fontSize: 64, fontWeight: 900, fontFamily: 'ui-monospace, monospace', lineHeight: 1 }}>{topScore}</div>
          <div style={{ fontSize: 12, fontWeight: 800, letterSpacing: '.18em', color: 'rgba(245,239,226,.6)' }}>POINTS</div>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 8, width: '70%', marginTop: 10 }}>
          {sorted.map((p, i) => {
            const tied = (p.score || 0) === topScore;
            return (
              <div key={p.id} style={{
                padding: '8px 12px',
                background: tied ? R.yellow : R.paper2,
                border: `2px solid ${R.ink}`,
                display: 'flex', alignItems: 'center', gap: 10,
              }}>
                <span style={{ fontSize: 14, fontWeight: 900, width: 20 }}>{i + 1}</span>
                <div style={{ width: 16, height: 16, background: p.color || R.blue, border: `2px solid ${R.ink}` }} />
                <span style={{ flex: 1, fontSize: 14, fontWeight: 800, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{p.name}</span>
                <span style={{ fontSize: 14, fontWeight: 900, fontFamily: 'ui-monospace, monospace' }}>{p.score || 0}</span>
              </div>
            );
          })}
        </div>

        <div style={{ marginTop: 18, display: 'flex', gap: 14 }}>
          {onAgain && <TvBtn color={R.pink} onClick={onAgain}>PLAY AGAIN →</TvBtn>}
          {onNewGame && <TvBtn color={R.paper2} fg={R.ink} size="sm" onClick={onNewGame}>BACK TO SETUP</TvBtn>}
        </div>
      </div>
    </TvShell>
  );
}

// ── Desktop host orchestrator ────────────────────────────────
// Given the same state slice app-host.jsx already has, pick the right TV
// component and pass its props. This stays presentational — it doesn't talk
// to the socket. All actions are callbacks supplied by App.
function DesktopHost(props) {
  const { view, room, code, joinUrl, players, currentSong, reveal, savedRoom } = props;

  // Setup phases — driven by `view` because pre-room there's no room.phase.
  if (view === 'start') {
    return <TvSplash
      onCreate={props.onCreate}
      onResume={savedRoom ? props.onResume : null}
      savedRoom={savedRoom}
      configError={props.configError}
      mode={props.mode}
      setMode={props.setMode}
      fullAccess={props.fullAccess}
      hostPlays={props.hostPlays}
      setHostPlays={props.setHostPlays}
      hostName={props.hostName}
      setHostName={props.setHostName}
    />;
  }
  if (view === 'spotify') {
    return <TvConnect onConnect={props.onConnect} busy={props.authBusy} error={props.authError || props.configError} />;
  }
  if (view === 'playlist') {
    return <TvPlaylist
      onPick={props.onPick}
      onPickCurated={props.onPickCurated}
      curatedPresets={props.curatedPresets}
      busy={props.playlistBusy}
      error={props.playlistError}
      onReauth={props.onReauth}
    />;
  }
  // Device picker: desktop doesn't need a separate flow — the room IS the device
  // (browser playback). For Spotify-Connect mode, fall back to the phone screen.
  if (view === 'device') {
    // Delegate to the phone HostDevice screen wrapped in a desktop frame so
    // we don't have to duplicate device polling. It's an admin step anyway.
    const H = window.HostScreens;
    return (
      <TvShell>
        <TvHeader left="Pick device" mid="" right="" />
        <div style={{ flex: 1, padding: '20px 60px', maxWidth: 720, margin: '0 auto', width: '100%' }}>
          <H.HostDevice
            devices={props.devices}
            picked={props.pickedDevice}
            onPick={props.onPickDevice}
            onRefresh={props.onRefreshDevices}
            refreshing={props.refreshingDevices}
            onContinue={props.onDeviceContinue}
          />
        </div>
      </TvShell>
    );
  }

  // In-room phases — driven by room.phase.
  const phase = room?.phase || 'lobby';
  if (view === 'room' || phase === 'lobby') {
    return <TvLobby
      code={code}
      players={players}
      joinUrl={joinUrl}
      onStart={props.onStart}
      onChangePlaylist={props.onChangePlaylist}
      playlistName={props.playlistName}
      mode={props.mode}
      scoringRule={props.scoringRule}
      onChangeScoringRule={props.onChangeScoringRule}
      roundTimerSec={props.roundTimerSec}
      onChangeRoundTimer={props.onChangeRoundTimer}
    />;
  }
  if (view === 'playing' || phase === 'playing') {
    return <TvGuessing
      round={room?.round || 1}
      song={currentSong}
      players={players}
      onReveal={props.onReveal}
      onReplay={props.onReplay}
      roundDeadline={room?.roundDeadline}
      hostPlays={props.hostPlays}
    />;
  }
  if (view === 'reveal' || phase === 'revealed') {
    const guesses = reveal?.guesses || [];
    const song = reveal?.song || currentSong;
    return <TvReveal
      round={room?.round || 1}
      song={song}
      guesses={guesses}
      players={players}
      myPid={props.myPid}
      onNext={props.onNext}
      onEnd={props.onEnd}
    />;
  }
  if (view === 'final' || phase === 'ended') {
    return <TvFinal players={players} onAgain={props.onAgain} onNewGame={props.onNewGame} />;
  }

  // Fallback — should never hit.
  return <TvShell><TvHeader left="…" mid="" right="" /></TvShell>;
}

// useIsDesktopHost — wide viewport + fine pointer. Updates on resize so window
// can be dragged onto a different display.
function useIsDesktopHost() {
  const compute = () => {
    if (typeof window === 'undefined') return false;
    if (window.innerWidth < 1100) return false;
    try {
      return window.matchMedia('(pointer: fine)').matches;
    } catch (_) { return true; }
  };
  const [v, setV] = React.useState(compute);
  React.useEffect(() => {
    const onResize = () => setV(compute());
    window.addEventListener('resize', onResize);
    let mq;
    try {
      mq = window.matchMedia('(pointer: fine)');
      if (mq.addEventListener) mq.addEventListener('change', onResize);
      else if (mq.addListener) mq.addListener(onResize);
    } catch (_) {}
    return () => {
      window.removeEventListener('resize', onResize);
      if (mq) {
        if (mq.removeEventListener) mq.removeEventListener('change', onResize);
        else if (mq.removeListener) mq.removeListener(onResize);
      }
    };
  }, []);
  return v;
}

window.TvSplash = TvSplash;
window.TvConnect = TvConnect;
window.TvPlaylist = TvPlaylist;
window.TvLobby = TvLobby;
window.TvGuessing = TvGuessing;
window.TvReveal = TvReveal;
window.TvFinal = TvFinal;
window.DesktopHost = DesktopHost;
window.useIsDesktopHost = useIsDesktopHost;
})();
