/* eslint-disable */
// Blackjack: the seat card (name + Solo / Online / Play with friends, on every visit) and the online table.
//
// The online table is drawn from the table's snapshots: the server's (bj-online.js), or, when the server can't be
// reached, a table this page runs itself on the same rules (bj-table-model.js) with CPU players only. Either way
// the page never decides a hand; it renders the snapshot and sends the player's moves.
//
// Money: the casino bankroll lives in this browser. Each snapshot says what this seat has staked this round; the
// page takes the difference out of the bankroll (a bet, a double, a split, insurance) or puts it back (a bet taken
// back), and remembers it in a ticket (localStorage bjTableHand). When the table reports the round's result, the
// page pays in what came back and clears the ticket. A tab closed mid-hand banks the result on the next visit,
// because the table keeps playing the hand (the stand-in) and keeps the result.

const BJ_MODE_KEY = 'bjMode';
// set when a player actually takes a seat: from then on a visit sits them straight back at their last Solo or Online table
const BJ_SAT_KEY = 'bjSat';
const BJ_TICKET_KEY = 'bjTableHand';
const BJ_LOCAL_TICKET_KEY = 'bjLocalHand';
// Room codes: 4 characters (6 for tables opened before 2026-10-15), no I, O, 0 or 1 so they read out loud cleanly.
const BJ_CODE = /^([A-HJ-NP-Z2-9]{4}|[A-HJ-NP-Z2-9]{6})$/;
const BJ_PUBLIC = /^P([1-9]|1\d|2[0-4])$/;   // the server's public table ids; a private code can also start with P

function bjOnlineEnabled() {
  try {
    if (window.CASINO_CONFIG && window.CASINO_CONFIG.blackjackOnline) return true;
    const q = new URLSearchParams(location.search);
    return q.has('online') || q.has('table');
  } catch (e) { return false; }
}
function bjStore(key, value) {
  try { if (value === null) localStorage.removeItem(key); else localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch (e) {}
}
function bjLoad(key) { try { return localStorage.getItem(key); } catch (e) { return null; } }
function bjTicket() { try { const t = JSON.parse(bjLoad(BJ_TICKET_KEY) || 'null'); return t && t.table && t.round ? t : null; } catch (e) { return null; } }
// A stake left at the offline table by a page closed mid-hand: that table died with the page, so the stake comes back.
function bjRefundLocalTicket() {
  let t = null; try { t = JSON.parse(bjLoad(BJ_LOCAL_TICKET_KEY) || 'null'); } catch (e) {}
  const B = window.CASINO_BANKROLL;
  if (t && t.staked > 0 && B) B.write(Math.max(0, B.read() + t.staked));
  bjStore(BJ_LOCAL_TICKET_KEY, null);
}
function bjSetTableParam(code) {
  try {
    const u = new URL(location.href);
    if (code) u.searchParams.set('table', code); else u.searchParams.delete('table');
    history.replaceState(null, '', u.pathname + u.search + u.hash);
  } catch (e) {}
}

const toCard = c => c.hidden ? { id: 'o' + c.id, rank: 'A', suit: '♠', hidden: true } : { id: 'o' + c.id, rank: c.r, suit: c.s };
const toHand = h => ({
  cards: h.cards.map(toCard), bet: h.bet, doubled: h.doubled, surrendered: h.surrendered,
  stood: h.stood, busted: h.busted, isSplitAces: h.splitAces, natural: h.natural,
});
function resultsFor(box) {
  if (!box || !box.result) return [];
  return box.result.kinds.map((kind, i) => {
    const bet = box.hands[i] ? box.hands[i].bet : 0;
    return {
      kind, betAmt: bet,
      payout: kind === 'win' ? bet : kind === 'blackjack' ? Math.floor(bet * 1.5) : kind === 'push' ? 0 : kind === 'surrender' ? Math.ceil(bet / 2) : bet,
    };
  });
}
function overallFor(results) {
  if (!results.length) return null;
  if (results.length === 1) return results[0];
  const net = results.reduce((a, r) => a + (r.kind === 'win' || r.kind === 'blackjack' ? r.payout
    : r.kind === 'surrender' ? -(r.betAmt - r.payout) : r.kind === 'lose' || r.kind === 'bust' ? -r.betAmt : 0), 0);
  if (net > 0) return { kind: 'win', payout: net, mixed: true };
  if (net < 0) return { kind: 'lose', payout: -net, betAmt: -net, mixed: true };
  return { kind: 'push', payout: 0, mixed: true };
}
const tableLabel = id => BJ_PUBLIC.test(id) ? 'Table ' + id.slice(1) : id === 'LOCAL' ? 'Offline table' : 'Private table';

// ─── Timer ring ────────────────────────────────────────
// A CSS animation started part-way through (negative delay), so it needs no per-frame rendering.
function TimerRing({ since, deadline, offset, size = 26, color = '#f5d896' }) {
  const total = Math.max(1, deadline - since);
  const elapsed = Math.max(0, Math.min(total, Date.now() + offset - since));
  const r = size / 2 - 2.5, c = 2 * Math.PI * r;
  return (
    <svg key={since + ':' + deadline} width={size} height={size} viewBox={`0 0 ${size} ${size}`} aria-hidden="true" style={{ transform:'rotate(-90deg)', flex:'none' }}>
      <circle cx={size/2} cy={size/2} r={r} fill="rgba(0,0,0,.45)" stroke="rgba(255,255,255,.15)" strokeWidth="3" />
      <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth="3" strokeLinecap="round"
        strokeDasharray={c} style={{ '--ring': c, animation: `bjRing ${total}ms linear ${-elapsed}ms forwards` }} />
    </svg>
  );
}

function SeatBadge({ children, tone = 'dim' }) {
  const tones = {
    dim: { background:'rgba(255,255,255,.08)', color:'var(--ivory-dim)', border:'1px solid rgba(255,255,255,.15)' },
    cpu: { background:'rgba(120,160,220,.16)', color:'#b9cdf0', border:'1px solid rgba(150,180,230,.35)' },
    gold: { background:'linear-gradient(180deg, #f5d896, #c9a26a)', color:'#1a1208', border:'1px solid rgba(245,216,150,.9)' },
    warn: { background:'rgba(255,107,90,.16)', color:'#f6b1a6', border:'1px solid rgba(255,107,90,.4)' },
  };
  return <span style={{ ...tones[tone], fontSize: 9, fontWeight: 800, letterSpacing:'.16em', padding:'2px 6px', borderRadius: 4, textTransform:'uppercase', whiteSpace:'nowrap' }}>{children}</span>;
}

function ResultTag({ r, small }) {
  if (!r) return null;
  const good = r.kind === 'win' || r.kind === 'blackjack', even = r.kind === 'push';
  return (
    <span style={{
      fontSize: small ? 9 : 10, letterSpacing:'.14em', textTransform:'uppercase', fontWeight: 700,
      padding:'2px 6px', borderRadius: 4, whiteSpace:'nowrap',
      background: good ? 'rgba(126,219,156,.2)' : even ? 'rgba(230,197,144,.2)' : 'rgba(255,107,90,.2)',
      color: good ? 'var(--win)' : even ? '#e6c590' : 'var(--lose)',
    }}>
      {r.kind === 'win' ? `+$${r.payout}` : r.kind === 'blackjack' ? `BJ +$${r.payout}` : r.kind === 'push' ? 'Push' : r.kind === 'surrender' ? '½ back' : r.kind === 'bust' ? 'Bust' : `-$${r.betAmt}`}
    </span>
  );
}

function seatBadges(o, snap) {
  const out = [];
  if (o.kind === 'cpu') out.push(<SeatBadge key="cpu" tone="cpu">CPU</SeatBadge>);
  if (o.box && o.box.auto) out.push(<SeatBadge key="auto" tone="gold">Auto</SeatBadge>);
  else if (o.kind === 'human' && !o.online && !o.left) out.push(<SeatBadge key="away" tone="warn">Away</SeatBadge>);
  if (o.sittingOut && snap.phase === 'betting') out.push(<SeatBadge key="out">Sitting out</SeatBadge>);
  return out;
}

// Another seat at the table (landscape): name, cards, total, bet, result, and the clock on their turn.
function OtherSeat({ o, snap, offset }) {
  const turn = snap.turn && snap.turn.seat === o.seat;
  if (o.kind === 'empty') {
    return (
      <div data-seat={o.seat} data-empty style={{ width: 192, height: 150, borderRadius: 14, border:'1.5px dashed rgba(230,197,144,.22)', display:'flex', alignItems:'center', justifyContent:'center', color:'rgba(230,197,144,.4)', fontSize: 10, letterSpacing:'.24em', textTransform:'uppercase' }}>
        Open seat
      </div>
    );
  }
  const hands = o.box ? o.box.hands.map(toHand) : [];
  const results = resultsFor(o.box);
  const many = hands.length > 1;
  const cw = many ? 42 : 60, ch = many ? 59 : 84;
  const bet = o.box ? o.box.staked : o.bet;
  return (
    <div data-seat={o.seat} style={{
      width: 192, minHeight: 164, borderRadius: 14, padding:'7px 9px 7px', position:'relative',
      background: turn ? 'rgba(245,216,150,.08)' : 'rgba(0,0,0,.22)',
      border: turn ? '1.5px solid rgba(245,216,150,.75)' : '1px solid rgba(201,162,106,.18)',
      boxShadow: turn ? '0 0 26px -6px rgba(245,216,150,.6)' : 'none',
      opacity: o.left ? .55 : 1, transition:'all .25s ease', display:'flex', flexDirection:'column', gap: 6,
    }}>
      <div style={{ display:'flex', alignItems:'center', gap: 5, minHeight: 26 }}>
        <span style={{ flex: 1, minWidth: 0, fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 17, color:'var(--brass-2)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{o.name}</span>
        {seatBadges(o, snap)}
        {turn && <TimerRing since={snap.turn.since} deadline={snap.deadline} offset={offset} size={24} />}
      </div>
      <div style={{ display:'flex', gap: many ? 6 : 0, justifyContent:'center', minHeight: ch, flexWrap:'nowrap' }}>
        {hands.map((h, hi) => (
          <div key={hi} style={{ display:'flex', outline: turn && snap.turn.hand === hi && many ? '1px solid rgba(245,216,150,.6)' : 'none', borderRadius: 6 }}>
            {h.cards.map((c, i) => (
              <div key={c.id} style={{ marginLeft: i ? -cw * 0.5 : 0 }}>
                <PlayingCard rank={c.rank} suit={c.suit} w={cw} h={ch} dealIndex={0} fromX={0} fromY={-120} />
              </div>
            ))}
          </div>
        ))}
        {!hands.length && snap.phase === 'betting' && (
          <span style={{ alignSelf:'center', fontSize: 13, color:'var(--ivory-dim)' }}>{o.sittingOut ? 'Sitting this one out' : o.bet ? 'Bet in' : 'Betting…'}</span>
        )}
      </div>
      <div style={{ display:'flex', alignItems:'center', justifyContent:'space-between', gap: 4, flexWrap:'wrap' }}>
        <span style={{ fontFamily:"'JetBrains Mono', monospace", fontSize: 16, fontWeight: 700, color:'var(--ivory)' }}>
          {hands.map((h, hi) => { const v = handValue(h.cards); return (hi ? ' · ' : '') + (h.natural && hands.length === 1 ? 'BJ' : v.total > 21 ? 'Bust' : v.total); }).join('')}
        </span>
        <span style={{ display:'flex', gap: 4, alignItems:'center' }}>
          {results.length ? results.map((r, i) => <ResultTag key={i} r={r} small />) : bet ? <span style={{ fontSize: 14, color:'var(--brass-2)', fontWeight: 700 }}>${bet}</span> : null}
        </span>
      </div>
    </div>
  );
}

// Another seat (portrait): one compact tile.
function SeatPill({ o, snap, offset }) {
  const turn = snap.turn && snap.turn.seat === o.seat;
  if (o.kind === 'empty') return <div data-seat={o.seat} data-empty style={{ flex: 1, minWidth: 0, height: 78, borderRadius: 10, border:'1.5px dashed rgba(230,197,144,.2)', display:'flex', alignItems:'center', justifyContent:'center', fontSize: 9, letterSpacing:'.2em', color:'rgba(230,197,144,.4)', textTransform:'uppercase' }}>Open</div>;
  const hands = o.box ? o.box.hands.map(toHand) : [];
  const results = resultsFor(o.box);
  const red = s => s === '♥' || s === '♦';
  return (
    <div data-seat={o.seat} style={{
      flex: 1, minWidth: 0, minHeight: 92, borderRadius: 10, padding:'6px 6px', position:'relative',
      background: turn ? 'rgba(245,216,150,.1)' : 'rgba(0,0,0,.28)',
      border: turn ? '1.5px solid rgba(245,216,150,.75)' : '1px solid rgba(201,162,106,.18)',
      display:'flex', flexDirection:'column', gap: 3, opacity: o.left ? .55 : 1,
    }}>
      <div style={{ display:'flex', alignItems:'center', gap: 3 }}>
        <span style={{ flex: 1, minWidth: 0, fontSize: 13.5, fontWeight: 700, color:'var(--brass-2)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{o.name}</span>
        {turn && <TimerRing since={snap.turn.since} deadline={snap.deadline} offset={offset} size={18} />}
      </div>
      <div style={{ display:'flex', gap: 3, flexWrap:'wrap' }}>{seatBadges(o, snap)}</div>
      <div style={{ fontFamily:"'JetBrains Mono', monospace", fontSize: 14, fontWeight: 700, lineHeight: 1.3, color:'var(--ivory)', wordBreak:'break-word' }}>
        {hands.map((h, hi) => (
          <div key={hi}>
            {h.cards.map(c => <span key={c.id} style={{ color: red(c.suit) ? '#ff9c8f' : 'var(--ivory)', marginRight: 3 }}>{c.rank}{c.suit}</span>)}
          </div>
        ))}
        {!hands.length && snap.phase === 'betting' && <span style={{ color:'var(--ivory-dim)' }}>{o.sittingOut ? 'Out' : o.bet ? `$${o.bet}` : '…'}</span>}
      </div>
      <div style={{ display:'flex', gap: 3, flexWrap:'wrap', marginTop:'auto' }}>
        {results.length ? results.map((r, i) => <ResultTag key={i} r={r} small />) : (o.box ? <span style={{ fontSize: 10.5, color:'var(--brass-2)', fontWeight: 700 }}>${o.box.staked}</span> : null)}
      </div>
    </div>
  );
}

// ─── Online table ──────────────────────────────────────
function OnlineTable({ mode, code, playerName, onOpenSeat, onBusy, onClosed }) {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const M = window.BJ_TABLE_MODEL, NET = window.BJ_ONLINE, B = window.CASINO_BANKROLL;

  const [bankroll, setBankroll] = useState(() => (B ? B.read() : 1000));
  const [snap, setSnap] = useState(null);
  const [status, setStatus] = useState('connecting');
  const [statusInfo, setStatusInfo] = useState(null);
  const [chips, setChips] = useState([]);
  const [lastChips, setLastChips] = useState([]);
  const [message, setMessage] = useState('');
  const [expression, setExpression] = useState('idle');
  const [flash, setFlash] = useState(null);
  const [stats, setStats] = useState({ played: 0, won: 0, best: B ? B.read() : 1000 });
  const [streak, setStreak] = useState(0);
  const [, setTick] = useState(0);
  const [portrait, setPortrait] = useState(() => !!(window.BJ_CANVAS && window.BJ_CANVAS.portrait));
  const [copied, setCopied] = useState(false);
  const offsetRef = useRef(0);
  const localRef = useRef(null);       // the offline table: { state, timer }
  const localTicketRef = useRef(null); // the offline table's ticket lives only in this page
  const prevRef = useRef(null);
  const startedRef = useRef(false);
  const brokeRef = useRef(false);
  const flashTimer = useRef(null);
  const name = playerName || '';
  const ctx = { player: name, dealer: tweaks.dealerName };

  useEffect(() => {
    const onCanvas = () => setPortrait(!!(window.BJ_CANVAS && window.BJ_CANVAS.portrait));
    window.addEventListener('bj:canvas', onCanvas);
    const onHydrated = () => { if (B) setBankroll(B.read()); };
    const onBonus = (e) => { if (e.detail) e.detail.seen = true; onHydrated(); };   // bonus chips (casino-bonus.js) are already in the stored bankroll
    window.addEventListener('casino:hydrated', onHydrated); window.addEventListener('casino:bonus', onBonus);
    return () => { window.removeEventListener('bj:canvas', onCanvas); window.removeEventListener('casino:hydrated', onHydrated); window.removeEventListener('casino:bonus', onBonus); };
  }, []);

  function showFlash(text) {
    setFlash(text);
    clearTimeout(flashTimer.current);
    flashTimer.current = setTimeout(() => setFlash(null), 2800);
  }

  // ── Money ──
  function readTicket(local) { return local ? localTicketRef.current : bjTicket(); }
  // The offline table's ticket is kept too: that table dies with the page, so a stake found on the next visit comes back.
  function writeTicket(local, t) { if (local) { localTicketRef.current = t; bjStore(BJ_LOCAL_TICKET_KEY, t); } else bjStore(BJ_TICKET_KEY, t); }
  function moveMoney(delta) {
    if (!delta || !B) return;
    const next = Math.max(0, B.read() + delta);
    B.write(next); setBankroll(next);
  }
  function bank(result) {
    moveMoney(result.returned);
    const nb = B ? B.read() : 0;
    if (window.CASINO_STATS) {
      window.CASINO_STATS.recordPeak(nb);
      result.kinds.forEach((kind, i) => {
        const bet = result.bets[i] || 0, won = kind === 'win' || kind === 'blackjack';
        window.CASINO_STATS.recordEvent('blackjack', { won, payout: won ? (kind === 'blackjack' ? Math.floor(bet * 1.5) : bet) : 0, rare: kind === 'blackjack' ? 'blackjack' : null });
      });
    }
    const wins = result.kinds.filter(k => k === 'win' || k === 'blackjack').length;
    const losses = result.kinds.filter(k => k === 'lose' || k === 'bust' || k === 'surrender').length;
    setStats(s => ({ played: s.played + result.kinds.length, won: s.won + wins, best: Math.max(s.best, nb) }));
    setStreak(s => wins > losses ? s + 1 : losses > wins ? 0 : s);
    if (tweaks.soundOn) { if (result.kinds.indexOf('blackjack') >= 0) SFX.bj(); else if (wins > losses) SFX.win(); else if (losses > wins) SFX.lose(); else SFX.push(); }
  }
  function settleMoney(s) {
    const local = s.table.id === 'LOCAL';
    let t = readTicket(local);
    const you = s.you;
    if (t && t.table === s.table.id) {
      if (you.result && you.result.table === t.table && you.result.round === t.round) { bank(you.result); t = null; writeTicket(local, null); }
      // The table was reset (round numbers started over), or that round is long gone and the table no longer has
      // its result for this player (it forgets people after a day away): the stake comes back.
      else if (s.round !== t.round) { moveMoney(t.staked); t = null; writeTicket(local, null); }
    }
    if (s.phase === 'results') return;
    const me = s.seats.find(x => x.you);
    const staked = !me ? 0 : s.phase === 'betting' ? (me.bet || 0) : (me.box && !me.left ? me.box.staked : 0);
    const prev = t && t.table === s.table.id && t.round === s.round ? t.staked : 0;
    if (t && (t.table !== s.table.id || t.round !== s.round) && staked === 0) return;   // an older ticket waits for its result
    if (staked !== prev) {
      moveMoney(prev - staked);
      writeTicket(local, staked ? { table: s.table.id, round: s.round, staked } : null);
      if (staked > prev && !startedRef.current) { startedRef.current = true; if (window.LGPlayNext) window.LGPlayNext.started(); }
    }
  }

  // ── Snapshots (server or offline table) ──
  function onSnap(s, receivedAt) {
    offsetRef.current = s.now - receivedAt;
    settleMoney(s);
    narrate(prevRef.current, s);
    prevRef.current = s;
    setSnap(s);
  }

  // ── The offline table: the same rules, run here, CPU players only ──
  function pumpLocal() {
    const L = localRef.current; if (!L) return;
    const now = Date.now();
    M.advance(L.state, now, ['me']);
    onSnap(M.snapshot(L.state, 'me', now, ['me']), now);
    clearTimeout(L.timer);
    L.timer = setTimeout(pumpLocal, Math.max(40, L.state.round.deadline - Date.now()));
  }
  function startLocal() {
    if (localRef.current) return;
    const now = Date.now(), state = M.makeTable('LOCAL', {}, now);
    M.join(state, 'me', name || 'You', now);
    localRef.current = { state, timer: null };
    pumpLocal();
  }
  function stopLocal() {
    const L = localRef.current; if (!L) return;
    clearTimeout(L.timer); localRef.current = null;
  }
  // Back to the live table only between hands, with nothing riding on the offline one.
  function localIdle() {
    const L = localRef.current; if (!L) return true;
    const s = M.snapshot(L.state, 'me', Date.now(), ['me']), me = s.seats.find(x => x.you);
    return s.phase === 'results' || (s.phase === 'betting' && !(me && me.bet));
  }

  useEffect(() => {
    const t = bjTicket();
    const resume = mode === 'online' && t && BJ_PUBLIC.test(t.table) ? t.table : null;
    NET.start({ mode, code, table: resume, name: name || '' }, {
      onState: (s, at) => { if (localRef.current) stopLocal(); onSnap(s, at); },
      onError: msg => showFlash(msg),
      onStatus: (st, info) => {
        setStatus(st); setStatusInfo(info);
        if (st === 'offline') startLocal();
        if (st === 'closed' && onClosed) onClosed(info);
      },
      canReconnect: localIdle,
    });
    return () => { NET.stop(); stopLocal(); };
  }, []);

  // A clock for the countdowns: 4 times a second while one is showing.
  useEffect(() => {
    const id = setInterval(() => setTick(n => n + 1), 250);
    return () => clearInterval(id);
  }, []);

  const local = !!localRef.current;
  function send(m) {
    if (!snap) return;
    const msg = { round: snap.round, ...m };
    if (localRef.current) {
      const err = M.action(localRef.current.state, 'me', msg, Date.now(), ['me']);
      if (err) showFlash(err);
      pumpLocal();
      return;
    }
    if (!NET.send(msg)) showFlash('Reconnecting to the table…');
  }

  // ── Derived ──
  const serverNow = Date.now() + offsetRef.current;
  const me = snap ? snap.seats.find(x => x.you) : null;
  const myBox = me && me.box && !me.left ? me.box : null;
  const myHands = myBox ? myBox.hands.map(toHand) : [];
  const myTurn = !!(snap && snap.phase === 'turns' && snap.turn && me && snap.turn.seat === me.seat);
  const activeIdx = myTurn ? snap.turn.hand : -1;
  const activeHand = myTurn ? myHands[activeIdx] : null;
  const dealer = snap ? snap.dealer.map(toCard) : [];
  const holeShown = !(dealer[1] && dealer[1].hidden);
  const results = resultsFor(myBox);
  const overall = snap && snap.phase === 'results' ? overallFor(results) : null;
  const chipTotal = chips.reduce((a, b) => a + b, 0);
  const betIn = me && snap && snap.phase === 'betting' ? (me.bet || 0) : 0;
  const secondsLeft = snap ? Math.max(0, Math.ceil((snap.deadline - serverNow) / 1000)) : 0;
  const busy = !!(snap && snap.phase !== 'results' && (betIn || myBox));
  useEffect(() => { if (onBusy) onBusy(busy); }, [busy]);

  // A new betting window: the last bet is laid out again, ready to place.
  const round = snap ? snap.round : 0, phase = snap ? snap.phase : null;
  useEffect(() => {
    if (phase !== 'betting') return;
    const sum = lastChips.reduce((a, b) => a + b, 0);
    setChips(sum && sum <= bankroll ? lastChips : []);
  }, [round, phase === 'betting']);

  // Out of chips → the shared reset modal (it also asks for feedback).
  useEffect(() => {
    const broke = phase === 'betting' && bankroll < 5 && !betIn && !busy;
    if (broke && !brokeRef.current && window.CASINO_RELOAD) {
      brokeRef.current = true;
      window.CASINO_RELOAD.open({
        game: 'blackjack', minBet: 5,
        message: `Looks like the table cleaned you out, ${name || 'friend'}.`,
        onReset: v => { setBankroll(v); brokeRef.current = false; },
        onClose: () => { brokeRef.current = false; },
      });
    }
  }, [phase, bankroll, betIn, busy]);

  // ── The dealer's lines ──
  function say(key, expr, extra) {
    setMessage(pickLine(key, extra ? { ...ctx, ...extra } : ctx));
    if (expr) setExpression(expr);
  }
  function narrate(prev, s) {
    const meNow = s.seats.find(x => x.you);
    if (s.notice && (!prev || !prev.notice || prev.notice.at !== s.notice.at)) { setMessage(s.notice.text); return; }
    if (!prev) { setMessage(s.table.id === 'LOCAL' ? 'The online tables are out of reach. I\'ll deal you in with the regulars until they\'re back.' : pickLine('greet', ctx)); return; }
    if (prev.round === s.round && prev.phase === s.phase && JSON.stringify(prev.turn) === JSON.stringify(s.turn)) return;
    if (s.phase === 'betting' && prev.phase !== 'betting') { setMessage('Place your bets.'); setExpression('idle'); return; }
    if (s.phase === 'dealing') { say('deal', 'deal'); if (tweaks.soundOn) SFX.deal(); return; }
    if (s.phase === 'turns' && meNow && s.turn && s.turn.seat === meNow.seat && meNow.box) {
      const was = prev.turn && prev.turn.seat === meNow.seat && prev.turn.hand === s.turn.hand;
      if (!was) {
        const h = meNow.box.hands[s.turn.hand], v = M.handValue(h.cards);
        say('player_turn', 'idle', { total: v.soft ? `soft ${v.total}` : v.total, up: upName(s.dealer[0].r) });
      }
      return;
    }
    if (s.phase === 'results' && meNow && meNow.box && meNow.box.result) {
      const o = overallFor(resultsFor(meNow.box));
      if (o.kind === 'blackjack') say('player_bj', 'shocked');
      else if (o.kind === 'win') say('player_win', 'sad');
      else if (o.kind === 'push') say('push', 'idle');
      else if (o.kind === 'bust') say('bust', 'bust', { total: M.handValue(meNow.box.hands[0].cards).total });
      else say('dealer_win', 'happy');
      return;
    }
    if (s.phase === 'dealer' && prev.phase !== 'dealer') setExpression('idle');
  }

  // ── Actions ──
  function addChip(v) {
    if (betIn || chipTotal + v > Math.min(bankroll, M.MAX_BET)) return;
    setChips(c => c.concat(v));
    if (tweaks.soundOn) SFX.chip();
  }
  function placeBet() {
    if (!chipTotal || chipTotal > bankroll) return;
    setLastChips(chips);
    send({ type: 'bet', amount: chipTotal });
    if (tweaks.soundOn) SFX.chip();
  }
  function move(type) {
    if (!myTurn || !activeHand) return;
    send({ type, hand: activeIdx, cards: activeHand.cards.length });
    if (tweaks.soundOn && (type === 'hit' || type === 'double')) SFX.card();
  }
  const canDouble = !!(activeHand && activeHand.cards.length === 2 && !activeHand.isSplitAces && bankroll >= activeHand.bet);
  const canSplit = !!(activeHand && activeHand.cards.length === 2 && activeHand.cards[0].rank === activeHand.cards[1].rank && !activeHand.isSplitAces && myHands.length < 4 && bankroll >= activeHand.bet);
  const canSurrender = !!(activeHand && activeHand.cards.length === 2 && myHands.length === 1);
  const doubleReason = !activeHand ? '' : activeHand.cards.length !== 2 ? 'Double is only allowed on your first two cards — you\'ve already hit.' : activeHand.isSplitAces ? 'Split aces get exactly one card — no doubling.' : bankroll < activeHand.bet ? `Doubling needs $${activeHand.bet - bankroll} more than you have.` : '';
  const splitReason = !activeHand ? '' : activeHand.cards.length !== 2 ? 'Split is only allowed on your first two cards.' : activeHand.cards[0].rank !== activeHand.cards[1].rank ? 'Split needs a pair — both cards the same rank.' : myHands.length >= 4 ? 'Four hands is the table limit.' : bankroll < activeHand.bet ? `Splitting needs $${activeHand.bet - bankroll} more than you have.` : '';
  const surrenderReason = !activeHand ? '' : myHands.length !== 1 ? 'No surrendering after a split.' : 'Surrender is only allowed before you hit.';

  const insuranceAsk = !!(snap && snap.phase === 'insurance' && myBox && myBox.ins === null);
  const insAmount = myBox ? Math.floor(myBox.hands[0].bet / 2) : 0;

  // Coach: the same engine as the solo table, on the cards and count this seat can see.
  const hint = useMemo(() => {
    if (!snap || !tweaks.showHints) return null;
    const c = snap.count, tc = c.running / c.decksLeft;
    const count = { running: c.running, trueCount: tc, decksLeft: c.decksLeft, edge: -0.5 + 0.5 * tc, tensPct: c.tensPct };
    const dealerUp = snap.dealer[0] ? [{ rank: snap.dealer[0].r, suit: snap.dealer[0].s }] : [];
    if (snap.phase === 'betting' && me && !betIn) return buildCoachHint({ showHints: true, count, phase: 'bet', bankroll: bankroll - chipTotal, totalBet: chipTotal, hands: [], activeHandIdx: 0, dealer: [] });
    if (insuranceAsk) return buildCoachHint({ showHints: true, count, phase: 'insurance', bankroll, totalBet: 0, hands: [{ bet: myBox.hands[0].bet }], activeHandIdx: 0, dealer: dealerUp });
    if (myTurn && activeHand) return buildCoachHint({ showHints: true, count, phase: 'player', bankroll, totalBet: 0, hands: myHands, activeHandIdx: activeIdx, dealer: dealerUp, canDouble, canSplit, canSurrender });
    return null;
  }, [snap, tweaks.showHints, bankroll, chipTotal, betIn, myTurn, activeIdx, canDouble, canSplit, canSurrender, insuranceAsk]);

  // Keyboard: the solo table's keys.
  const keysRef = useRef({});
  keysRef.current = { myTurn, move, canDouble, canSplit, canSurrender, phase, betIn, chipTotal, placeBet };
  useEffect(() => {
    function onKey(e) {
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) return;
      if (e.metaKey || e.ctrlKey || e.altKey || document.querySelector('[data-bj-seat-card]')) return;
      const K = keysRef.current, k = e.key.toLowerCase();
      if (K.myTurn) {
        if (k === 'h') K.move('hit'); else if (k === 's') K.move('stand');
        else if (k === 'd' && K.canDouble) K.move('double'); else if (k === 'p' && K.canSplit) K.move('split');
        else if (k === 'r' && K.canSurrender) K.move('surrender');
      } else if (K.phase === 'betting' && !K.betIn && K.chipTotal && (k === 'enter' || k === ' ')) { e.preventDefault(); K.placeBet(); }
    }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, []);

  function copyInvite() {
    const link = location.origin + '/blackjack/?table=' + snap.table.id;
    const done = () => { setCopied(true); setTimeout(() => setCopied(false), 2200); };
    try { navigator.clipboard.writeText(link).then(done, () => window.prompt('Copy this link and send it to your friends:', link)); }
    catch (e) { window.prompt('Copy this link and send it to your friends:', link); }
  }

  // ── Render ──
  const others = snap ? snap.seats.filter(x => !x.you) : [];
  const turnSeat = snap && snap.turn ? snap.seats[snap.turn.seat] : null;
  const statusLine = (() => {
    if (!snap) return status === 'closed' ? '' : 'Finding you a seat…';
    if (me && snap.you.waiting) return 'You\'ll sit down at the next deal.';
    if (snap.phase === 'dealing') return 'Dealing…';
    if (snap.phase === 'insurance') return `Insurance? Waiting on the table · ${secondsLeft}s`;
    if (snap.phase === 'turns' && turnSeat && !myTurn) return `${turnSeat.name}${turnSeat.kind === 'cpu' ? '' : turnSeat.box && turnSeat.box.auto ? ' (stand-in)' : ''} is playing · ${secondsLeft}s`;
    if (snap.phase === 'dealer') return 'Dealer is playing the hand';
    if (snap.phase === 'results') return `Next hand in ${secondsLeft}s`;
    if (snap.phase === 'betting' && betIn) return `Your $${betIn} is in · dealing in ${secondsLeft}s`;
    return '';
  })();

  const waitingPanel = text => (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', height: portrait ? 64 : 92, fontSize: 15, letterSpacing:'.22em', color:'var(--ivory-dim)', textTransform:'uppercase', textAlign:'center', gap: 12 }}>
      <span style={{ width: 10, height: 10, borderRadius:'50%', background:'var(--brass)', animation:'glowPulse 1s ease-in-out infinite', flex:'none' }} />
      {text}
    </div>
  );
  const pillBtn = (label, onClick, gold) => (
    <button onClick={onClick} style={{
      padding:'10px 18px', borderRadius: 999, cursor:'pointer', fontFamily:'inherit',
      fontSize: 11, fontWeight: 700, letterSpacing:'.16em', textTransform:'uppercase',
      background: gold ? 'linear-gradient(180deg, #f5d896, #c9a26a)' : 'rgba(20,12,6,.85)',
      color: gold ? '#1a1208' : 'var(--ivory)', border:'1px solid rgba(201,162,106,.45)',
    }}>{label}</button>
  );

  let zone = null;
  if (!snap) zone = waitingPanel(status === 'closed' ? '' : 'Finding you a seat…');
  else if (snap.you.released && !me) zone = (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 14, flexWrap:'wrap', minHeight: 80 }}>
      <span style={{ color:'var(--ivory-dim)', fontSize: 14 }}>You sat out two hands, so a CPU player took your seat.</span>
      {pillBtn('Sit back down', () => send({ type: 'sit' }), true)}
    </div>
  );
  else if (snap.phase === 'betting' && me && !betIn) zone = (
    <div style={{ display:'flex', flexDirection:'column', gap: 8 }}>
      <BettingZone
        bet={chips} bankroll={bankroll - chipTotal}
        onChip={addChip} onClear={() => setChips([])}
        onRebet={() => { const sum = lastChips.reduce((a, b) => a + b, 0); if (sum && sum <= bankroll) setChips(lastChips); }}
        onDeal={placeBet} canRebet={lastChips.length > 0 && lastChips.reduce((a, b) => a + b, 0) <= bankroll && !chipTotal}
        compact={portrait} dealLabel={`Place bet · ${secondsLeft}s`}
      />
      <div style={{ textAlign:'center' }}>
        <button onClick={() => send({ type: 'sitout' })} disabled={me.sittingOut} style={{ background:'none', border:'none', color:'var(--ivory-dim)', fontSize: 12, letterSpacing:'.14em', textTransform:'uppercase', cursor:'pointer', textDecoration: me.sittingOut ? 'none' : 'underline', fontFamily:'inherit' }}>
          {me.sittingOut ? 'Sitting this hand out' : 'Sit this hand out'}
        </button>
      </div>
    </div>
  );
  else if (snap.phase === 'betting' && betIn) zone = (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 14, minHeight: portrait ? 70 : 100, flexWrap:'wrap' }}>
      <span style={{ fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 20, color:'var(--brass-2)' }}>Your ${betIn} is in</span>
      <span style={{ color:'var(--ivory-dim)', fontSize: 13 }}>Dealing in {secondsLeft}s</span>
      {pillBtn('Change bet', () => send({ type: 'unbet' }))}
    </div>
  );
  else if (insuranceAsk) zone = (
    <div>
      <InsuranceZone amount={insAmount} bankroll={bankroll} onYes={() => send({ type: 'insure', yes: true })} onNo={() => send({ type: 'insure', yes: false })} />
      <div style={{ textAlign:'center', marginTop: 6, fontSize: 12, color:'var(--ivory-dim)' }}>{secondsLeft}s · no answer means no insurance</div>
    </div>
  );
  else if (myTurn && activeHand) zone = (
    <div style={{ display:'flex', flexDirection:'column', gap: 6 }}>
      <ActionZone
        hint={hint} canDouble={canDouble} canSplit={canSplit} canSurrender={canSurrender}
        doubleReason={doubleReason} splitReason={splitReason} surrenderReason={surrenderReason}
        onHit={() => move('hit')} onStand={() => move('stand')} onDouble={() => move('double')} onSplit={() => move('split')} onSurrender={() => move('surrender')}
        onBlocked={showFlash} betAmount={activeHand.bet} compact={portrait}
      />
      <div data-bj-clock style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 10, fontSize: portrait ? 15 : 17, fontWeight: 600, color: secondsLeft <= 5 ? 'var(--lose)' : 'var(--ivory)' }}>
        <TimerRing since={snap.turn.since} deadline={snap.deadline} offset={offsetRef.current} size={portrait ? 28 : 32} color={secondsLeft <= 5 ? '#f29a8c' : '#f5d896'} />
        {myBox.auto ? 'The stand-in is playing for you. Make a move to take it back.'
          : <span><b style={{ fontFamily:"'JetBrains Mono', monospace", fontSize: portrait ? 20 : 23 }}>{secondsLeft}s</b> to decide, then the stand-in plays it safe (hit or stand)</span>}
      </div>
    </div>
  );
  else zone = waitingPanel(statusLine);

  const bottomZone = (
    <div style={{ position:'relative', zIndex: 5, padding: portrait ? '8px 12px 14px' : '10px 26px 16px', minHeight: portrait ? 0 : 212, display:'flex', flexDirection:'column', gap: 8, justifyContent:'flex-end' }}>
      {hint && <CoachBar hint={hint} compact={portrait} />}
      {flash && <div style={{ alignSelf:'center', padding:'6px 16px', background:'rgba(20,12,6,.92)', border:'1px solid rgba(230,197,144,.4)', borderRadius: 999, fontSize: 12, color:'var(--ivory)' }}>{flash}</div>}
      {zone}
    </div>
  );

  const statusPill = (() => {
    const live = status === 'live' && !local;
    const dot = live ? '#7edb9c' : status === 'retrying' || status === 'connecting' ? '#e6c590' : '#f29a8c';
    const label = local ? 'Offline table · CPU players only' : status === 'retrying' ? 'Reconnecting…' : status === 'connecting' ? 'Finding a table…'
      : snap ? (snap.table.private ? 'Room code · ' + snap.table.id : tableLabel(snap.table.id)) : '';
    return (
      <div style={{ position:'absolute', top: portrait ? 2 : 6, left: portrait ? 8 : 10, zIndex: 6, display:'flex', gap: 8, alignItems:'center', flexWrap:'wrap', maxWidth:'100%' }}>
        <span data-bj-status={local ? 'local' : status} style={{ display:'inline-flex', alignItems:'center', gap: 7, padding:'5px 11px', borderRadius: 999, background:'rgba(10,6,3,.7)', border:'1px solid rgba(201,162,106,.3)', fontSize: 11, fontWeight: 700, letterSpacing:'.12em', textTransform:'uppercase', color:'var(--ivory)' }}>
          <span style={{ width: 8, height: 8, borderRadius:'50%', background: dot, boxShadow: live ? '0 0 8px #7edb9c' : 'none' }} />{label}
        </span>
        {snap && snap.table.private && !local && (
          <button onClick={copyInvite} style={{ padding:'5px 11px', borderRadius: 999, background: copied ? 'linear-gradient(180deg, #f5d896, #c9a26a)' : 'rgba(10,6,3,.7)', border:'1px solid rgba(201,162,106,.5)', color: copied ? '#1a1208' : 'var(--brass-2)', fontSize: 11, fontWeight: 700, letterSpacing:'.12em', textTransform:'uppercase', cursor:'pointer', fontFamily:'inherit' }}>
            {copied ? '✓ Link copied' : 'Copy invite link'}
          </button>
        )}
      </div>
    );
  })();

  // Opening deal: the dealer's two cards land after each seat's (seat by seat, twice round).
  const boxes = snap ? snap.seats.filter(x => x.box) : [];
  const shownD = dealer.length ? (holeShown ? handValue(dealer) : handValue(dealer.slice(0, 1))) : null;
  const dealerBJ = holeShown && dealer.length === 2 && isBlackjack(dealer);

  const feltArea = (
    <div style={{ position:'absolute', inset: 0, display:'flex', flexDirection:'column' }}>
      {!portrait && (
        <BrassRail bankroll={bankroll} streak={streak} played={stats.played} won={stats.won} best={stats.best}
          showHints={tweaks.showHints} onToggleHints={() => setTweak('showHints', !tweaks.showHints)}
          playerName={name} onOpenProfile={onOpenSeat} />
      )}
      <div style={{ flex: 1, position:'relative', padding: portrait ? '4px 10px 0' : '4px 24px 0', display:'flex', flexDirection:'column' }}>
        {statusPill}
        <div style={{ marginTop: portrait ? 30 : 8, textAlign:'center', position:'relative', zIndex: 3 }}>
          {shownD && <HandValue value={shownD.total} soft={shownD.soft} isBust={shownD.total > 21} isBJ={dealerBJ} label="Dealer" />}
          <div style={{ display:'flex', justifyContent:'center', gap: 8, marginTop: 8, minHeight: portrait ? 84 : 104 }}>
            {dealer.map((c, i) => (
              <PlayingCard key={c.id} rank={c.rank} suit={c.suit} w={portrait ? 60 : 74} h={portrait ? 84 : 104}
                faceDown={!!c.hidden} dealIndex={i < 2 ? i * (boxes.length + 1) + boxes.length : 0} fromX={140} fromY={-200} />
            ))}
          </div>
        </div>
        <div style={{ display:'flex', justifyContent:'center', gap: portrait ? 6 : 14, marginTop: portrait ? 10 : 14, position:'relative', zIndex: 3 }}>
          {others.map((o, i) => portrait
            ? <SeatPill key={o.seat} o={o} snap={snap} offset={offsetRef.current} />
            : <div key={o.seat} style={{ transform: `translateY(${[18, 0, 0, 18][i] || 0}px)` }}><OtherSeat o={o} snap={snap} offset={offsetRef.current} /></div>)}
        </div>
        {overall && <ResultBanner kind={overall.kind} payout={overall.payout} netLoss={overall.kind === 'surrender' ? overall.betAmt - overall.payout : null} />}
        <div style={{ position:'absolute', left: 0, right: 0, bottom: portrait ? 6 : 10, textAlign:'center', zIndex: 3 }}>
          {myHands.length > 0 ? (
            <div data-bj-me>
              <PlayerHandsRow hands={myHands}
                activeIdx={activeIdx < 0 ? 0 : activeIdx} results={results}
                pBJ={myHands.length === 1 && myHands[0].natural}
                shake={!!overall && overall.kind === 'bust'} playerName={name || 'You'} compact={portrait} />
            </div>
          ) : me ? (
            <div style={{ color:'rgba(230,197,144,.55)', fontSize: 11, letterSpacing:'.24em', textTransform:'uppercase', paddingBottom: 10 }}>
              {name || 'You'} · seat {me.seat + 1}{me.sittingOut ? ' · sitting out' : ''}
            </div>
          ) : null}
        </div>
      </div>
      {bottomZone}
    </div>
  );

  const overlay = status === 'elsewhere' ? (
    <div style={{ position:'absolute', inset: 0, zIndex: 50, background:'rgba(8,5,2,.78)', display:'flex', alignItems:'center', justifyContent:'center', flexDirection:'column', gap: 16, textAlign:'center', padding: 24 }}>
      <div style={{ fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 26, color:'var(--brass-2)' }}>You're playing in another tab</div>
      <div style={{ color:'var(--ivory-dim)', fontSize: 14 }}>Your seat moved to the tab you opened last.</div>
      {pillBtn('Play here instead', () => NET.reconnect(), true)}
    </div>
  ) : null;

  return (
    <div style={{ width:'100%', height:'100%', padding: portrait ? 12 : 20, position:'relative', display:'flex', flexDirection: portrait ? 'column' : 'row', gap: portrait ? 10 : 20 }}>
      <style>{'@keyframes bjRing{from{stroke-dashoffset:0}to{stroke-dashoffset:var(--ring)}}'}</style>
      {portrait ? (
        <>
          <PortraitRail bankroll={bankroll} streak={streak} showHints={tweaks.showHints} onToggleHints={() => setTweak('showHints', !tweaks.showHints)} playerName={name} onOpenProfile={onOpenSeat} />
          <DealerStrip name={tweaks.dealerName} message={message} gender={CASINO_DEALERS.get(tweaks.dealerName).art} expression={expression} />
          <div style={{ flex: 1, position:'relative', minHeight: 0 }}><FeltBackdrop />{feltArea}</div>
        </>
      ) : (
        <>
          <DealerPanel name={tweaks.dealerName} gender={CASINO_DEALERS.get(tweaks.dealerName).art} expression={expression} message={message} isIdle={false} mood={0} />
          <div style={{ flex: 1, position:'relative', minWidth: 700 }}><FeltBackdrop />{feltArea}</div>
        </>
      )}
      {overlay}
    </div>
  );
}

// ─── Seat card ─────────────────────────────────────────
// Every visit (with online tables on): the name, and where to sit. The last choice is pre-selected, so Enter sits
// you straight back down. Opened again from the profile pill to change tables between hands.
// A real name: one the player chose (on any table, the profile, or a Google sign-in), not the "Player 7K2Q" the
// leaderboards fall back to (workers/api/boards/names.mjs fallbackName).
const bjRealName = n => !!(n && n.trim() && !/^Player [0-9A-Z]{4}$/.test(n.trim()));

// With a real name already saved, the card doesn't ask for it again: it says who you're playing as, with a Change link.
function SeatCard({ initialName = '', lastMode = 'solo', firstVisit, busy, currentMode, error, working, onChoose, onCancel }) {
  const named = bjRealName(initialName);
  const [name, setName] = React.useState(named ? initialName : '');
  const [editing, setEditing] = React.useState(!named);
  const modeRef = React.useRef(null);
  const [selected, setSelected] = React.useState(lastMode);
  const codeRef = React.useRef(null), startRef = React.useRef(null);
  const [code, setCode] = React.useState('');
  const inputRef = React.useRef(null), dealRef = React.useRef(null);
  const downOnBackdrop = React.useRef(false);
  React.useEffect(() => {
    // Focus the name box when it's asked for, otherwise the Deal button, so Enter sits you down either way.
    const id = setTimeout(() => {
      if (inputRef.current) { inputRef.current.focus(); inputRef.current.select(); }
      else if (dealRef.current) dealRef.current.focus();
      else if (modeRef.current) modeRef.current.focus();
    }, 50);
    return () => clearTimeout(id);
  }, [editing]);
  const dismiss = firstVisit ? () => onChoose(lastMode, name) : onCancel;
  React.useEffect(() => {
    function onKey(e) { if (e.key === 'Escape') { e.preventDefault(); dismiss(); } }
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [dismiss]);
  const locked = m => busy && m !== currentMode;
  function choose(m, c) { if (locked(m) || working) return; setSelected(m); onChoose(m, name, c); }
  function submit(e) { e.preventDefault(); choose(selected); }
  const options = [
    { mode: 'solo', title: 'Solo', sub: 'Just you and the dealer' },
    { mode: 'online', title: 'Online table', sub: 'Real people · CPU players fill empty seats' },
    { mode: 'private', title: 'Play with friends', sub: 'A private table you share with a link' },
  ];
  const cleanCode = code.toUpperCase().replace(/[^A-Z0-9]/g, '').slice(0, 6);
  const codeOk = BJ_CODE.test(cleanCode);
  // Play with friends opens its own two choices: start a new table, or type the code a friend gave you.
  function openFriends(focusCode) {
    setSelected('private');
    setTimeout(() => { const el = focusCode ? codeRef.current : startRef.current; if (el) el.focus(); }, 30);
  }
  return (
    <div data-bj-seat-card
      onMouseDown={e => { downOnBackdrop.current = e.target === e.currentTarget; }}
      onClick={e => { if (e.target === e.currentTarget && downOnBackdrop.current) dismiss(); }}
      style={{ position:'fixed', inset: 0, zIndex: 9000, background:'rgba(8,5,2,.65)', backdropFilter:'blur(8px)', display:'flex', alignItems:'center', justifyContent:'center', padding: 12 }}>
      <form onSubmit={submit} style={{
        background:'linear-gradient(180deg, rgba(35,22,10,.96), rgba(20,12,6,.98))', border:'1px solid rgba(201,162,106,.5)', borderRadius: 16,
        padding:'26px 28px 22px', boxShadow:'0 30px 80px rgba(0,0,0,.7), inset 0 1px 0 rgba(230,197,144,.15)',
        width: 440, maxWidth:'100%', maxHeight:'100%', overflowY:'auto', textAlign:'center',
      }}>
        <div style={{ fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 24, color:'var(--brass-2)', marginBottom: 14 }}>Take a seat</div>
        {editing ? (
          <>
            <input ref={inputRef} value={name} onChange={e => setName(e.target.value)} maxLength={20}
              placeholder="Your first name (optional)" aria-label="Your first name (optional)" autoComplete="off"
              style={{ width:'100%', padding:'11px 16px', background:'rgba(10,6,3,.6)', border:'1px solid rgba(201,162,106,.4)', borderRadius: 10, color:'var(--ivory)', fontFamily:"'Playfair Display', serif", fontSize: 18, fontStyle:'italic', textAlign:'center', outline:'none', boxSizing:'border-box' }} />
            <div style={{ marginTop: 6, fontSize: 12, color:'var(--ivory-dim)' }}>First name only · shown at the table and on the leaderboards</div>
          </>
        ) : (
          <div data-bj-playing-as style={{ fontSize: 15, color:'var(--ivory-dim)' }}>
            Playing as <b style={{ color:'var(--ivory)', fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 18 }}>{name}</b>
            {' · '}<button type="button" onClick={() => setEditing(true)} style={{ background:'none', border:'none', padding: 0, color:'var(--brass-2)', textDecoration:'underline', cursor:'pointer', fontSize: 14, fontFamily:'inherit' }}>Change</button>
          </div>
        )}
        <div role="radiogroup" aria-label="Where to play" style={{ display:'flex', flexDirection:'column', gap: 8, marginTop: 16 }}>
          {options.map(o => {
            const on = selected === o.mode, off = locked(o.mode);
            return (
              <button key={o.mode} ref={on ? modeRef : null} type="button" role="radio" aria-checked={on} data-mode={o.mode} disabled={off || working}
                onClick={() => (o.mode === 'private' ? openFriends(false) : setSelected(o.mode))} onFocus={() => setSelected(o.mode)}
                style={{
                  display:'flex', alignItems:'center', gap: 12, textAlign:'left', padding:'12px 16px', borderRadius: 12, cursor: off ? 'not-allowed' : 'pointer', fontFamily:'inherit',
                  background: on ? 'linear-gradient(180deg, rgba(245,216,150,.22), rgba(201,162,106,.12))' : 'rgba(10,6,3,.5)',
                  border: on ? '1.5px solid rgba(245,216,150,.85)' : '1px solid rgba(201,162,106,.3)', opacity: off ? .45 : 1,
                  boxShadow: on ? '0 0 22px -6px rgba(245,216,150,.6)' : 'none',
                }}>
                <span style={{ width: 16, height: 16, borderRadius:'50%', flex:'none', border:'2px solid rgba(245,216,150,.8)', background: on ? 'radial-gradient(circle, #f5d896 45%, transparent 50%)' : 'transparent' }} />
                <span style={{ flex: 1 }}>
                  <span style={{ display:'block', fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 19, fontWeight: 700, color:'var(--ivory)' }}>{o.title}</span>
                  <span style={{ display:'block', fontSize: 12.5, color:'var(--ivory-dim)', marginTop: 2 }}>{o.sub}</span>
                </span>
              </button>
            );
          })}
        </div>
        {selected === 'private' && !locked('private') && (
          <div data-friends style={{ marginTop: 10, padding:'12px 14px', borderRadius: 12, background:'rgba(10,6,3,.55)', border:'1px solid rgba(245,216,150,.45)', display:'flex', flexDirection:'column', gap: 10, alignItems:'stretch' }}>
            <button ref={startRef} type="button" data-start-private disabled={working} onClick={() => choose('private')} style={{ minHeight: 44, borderRadius: 999, background:'linear-gradient(180deg, #f5d896, #c9a26a)', border:'1px solid rgba(245,216,150,1)', color:'#1a1208', fontWeight: 800, fontSize: 12, letterSpacing:'.14em', textTransform:'uppercase', cursor:'pointer', fontFamily:'inherit' }}>{working ? 'Opening a table…' : 'Start a new table'}</button>
            <div style={{ fontSize: 12, letterSpacing:'.2em', textTransform:'uppercase', color:'var(--ivory-dim)' }}>or join a friend's table</div>
            <div style={{ display:'flex', gap: 8, justifyContent:'center' }}>
              <input ref={codeRef} value={cleanCode} onChange={e => setCode(e.target.value)} placeholder="Room code" aria-label="Room code from a friend" autoComplete="off" autoCapitalize="characters" spellCheck={false}
                onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); if (codeOk) choose('private', cleanCode); } }}
                style={{ flex: 1, minWidth: 0, maxWidth: 200, padding:'10px 12px', background:'rgba(10,6,3,.7)', border:'1px solid rgba(201,162,106,.5)', borderRadius: 10, color:'var(--ivory)', fontFamily:"'JetBrains Mono', monospace", fontSize: 22, letterSpacing:'.3em', textAlign:'center', textTransform:'uppercase' }} />
              <button type="button" data-join-code disabled={!codeOk || working} onClick={() => choose('private', cleanCode)} style={{ padding:'0 18px', borderRadius: 999, background: codeOk ? 'linear-gradient(180deg, #f5d896, #c9a26a)' : 'rgba(40,28,18,.6)', border:'1px solid rgba(201,162,106,.5)', color: codeOk ? '#1a1208' : 'rgba(255,255,255,.4)', fontWeight: 800, fontSize: 12, letterSpacing:'.14em', textTransform:'uppercase', cursor: codeOk ? 'pointer' : 'not-allowed', fontFamily:'inherit' }}>Join</button>
            </div>
            {cleanCode.length >= 4 && !codeOk && <div style={{ fontSize: 12.5, color:'var(--brass-2)' }}>Room codes are 4 letters and numbers (never I, O, 0 or 1).</div>}
          </div>
        )}
        {selected !== 'private' && (
          <button ref={dealRef} type="submit" data-deal disabled={working || locked(selected)} style={{
            width:'100%', minHeight: 52, marginTop: 14, borderRadius: 999, cursor:'pointer', background:'linear-gradient(180deg, #f5d896, #c9a26a)', border:'1px solid rgba(245,216,150,1)',
            color:'#1a1208', fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontWeight: 700, fontSize: 20, boxShadow:'0 6px 20px rgba(0,0,0,.45)',
          }}>{working ? '…' : selected === 'online' ? 'Join the online table →' : 'Deal me in →'}</button>
        )}
        {busy && <div style={{ marginTop: 10, fontSize: 12.5, color:'var(--brass-2)' }}>Finish this hand to change tables.</div>}
        {error && <div role="alert" style={{ marginTop: 10, fontSize: 13, color:'var(--lose)' }}>{error}</div>}
        <div style={{ marginTop: 12, fontSize: 13, color:'var(--ivory-dim)' }}>
          {selected !== 'private' && (
            <button type="button" onClick={() => openFriends(true)} disabled={busy} style={{ background:'none', border:'none', color:'var(--brass-2)', textDecoration:'underline', cursor:'pointer', fontSize: 13, fontFamily:'inherit' }}>Have a room code from a friend?</button>
          )}
        </div>
        <BJSignInNote link />
        <div style={{ display:'flex', gap: 10, marginTop: 14, justifyContent:'center', alignItems:'center' }}>
          {!firstVisit && (
            <button type="button" onClick={onCancel} style={{ minHeight: 40, padding:'0 18px', background:'rgba(20,12,6,.6)', border:'1px solid rgba(201,162,106,.3)', borderRadius: 999, color:'var(--ivory-dim)', fontSize: 11, fontWeight: 700, letterSpacing:'.18em', textTransform:'uppercase', cursor:'pointer' }}>Cancel</button>
          )}
          <a href="/profile/" style={{ fontSize: 10, letterSpacing:'.12em', textTransform:'uppercase', color:'var(--ivory-dim)', textDecoration:'none' }}>Full profile →</a>
        </div>
      </form>
    </div>
  );
}

// ─── Root ──────────────────────────────────────────────
function BJRoot() {
  const enabled = React.useMemo(bjOnlineEnabled, []);
  const linkCode = React.useMemo(() => {
    try { const c = (new URLSearchParams(location.search).get('table') || '').toUpperCase(); return BJ_CODE.test(c) ? c : null; } catch (e) { return null; }
  }, []);
  const lastMode = (() => { const m = bjLoad(BJ_MODE_KEY); return m === 'online' || m === 'private' || m === 'solo' ? m : 'solo'; })();
  // A player who has sat down here before skips the card and goes straight back to their last Solo or Online table (a private
  // table's link expires, so it gets the card). The profile pill opens the card again.
  const resume = enabled && !linkCode && bjLoad(BJ_SAT_KEY) && (lastMode === 'solo' || lastMode === 'online') ? lastMode : null;
  const [mode, setMode] = React.useState(!enabled ? 'solo' : linkCode ? 'private' : resume);
  const [code, setCode] = React.useState(linkCode);
  const [card, setCard] = React.useState(enabled && !linkCode && !resume ? 'first' : null);
  const [name, setName] = React.useState(() => (window.CASINO_PLAYER && window.CASINO_PLAYER.read()) || '');
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState('');
  const [working, setWorking] = React.useState(false);

  function choose(m, typed, joinCode) {
    const trimmed = (typed || '').trim();
    if (trimmed) { const saved = window.CASINO_PLAYER.write(trimmed); setName(saved || trimmed); }
    else if (card === 'first' && !name) window.CASINO_PLAYER.set('nameSkipped', true);
    setError(''); bjStore(BJ_SAT_KEY, '1');
    if (m === 'private' && !joinCode && !(mode === 'private' && code)) {
      setWorking(true);
      window.BJ_ONLINE.createPrivate().then(c => {
        setWorking(false); bjStore(BJ_MODE_KEY, 'private'); setCode(c); setMode('private'); bjSetTableParam(c); setCard(null);
      }, () => { setWorking(false); setError('Couldn\'t open a private table just now. Try again, or play Solo.'); });
      return;
    }
    bjStore(BJ_MODE_KEY, m);
    if (m === 'private') { const c = joinCode || code; setCode(c); bjSetTableParam(c); }
    else bjSetTableParam(null);
    setMode(m); setCard(null);
  }
  function closed(info) {
    setError(info && info.message ? info.message + ' Start a new one, or pick another table.' : 'That table is closed.');
    bjSetTableParam(null); setCode(null); setBusy(false); setMode(null); setCard('edit');
  }

  return (
    <>
      {mode === 'online' || mode === 'private'
        ? <OnlineTable key={mode + ':' + (code || '')} mode={mode} code={code} playerName={name}
            onOpenSeat={() => setCard('edit')} onBusy={setBusy} onClosed={closed} />
        : <App playerName={name} seatCard={enabled} onOpenSeat={() => setCard('edit')} onBusy={setBusy} />}
      {card && (
        <SeatCard initialName={name} lastMode={card === 'first' ? lastMode : (mode || lastMode)} firstVisit={card === 'first'}
          busy={busy} currentMode={mode} error={error} working={working}
          onChoose={choose} onCancel={() => { setCard(null); setError(''); }} />
      )}
    </>
  );
}

bjRefundLocalTicket();
ReactDOM.createRoot(document.getElementById('root')).render(<BJRoot />);
