/* eslint-disable */
// Texas Hold'em: the seat card (Solo / Online table / Play with friends, and the stake) and the online table.
//
// The online table is drawn from the server's snapshots (th-online.js → workers/api/holdem/tables.mjs), with the
// solo table's own pieces (seats, board, pot, action buttons, coach, result card). The page never decides a hand.
//
// Money: you buy in from your casino bankroll and cash out when you leave. The table keeps two totals for you, `in`
// (bought in) and `out` (cashed out); the page keeps what it has already settled per table (localStorage thTables)
// and moves the difference in or out of the bankroll. So a tab closed mid-hand is paid out on the next visit: the
// stand-in finishes the hand, the table cashes you out at the next deal, and the next snapshot settles it.

const TH_MODE_KEY = 'thMode';
const TH_TIER_KEY = 'thTier';
const TH_LEDGER_KEY = 'thTables';
// 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 TH_CODE = /^([A-HJ-NP-Z2-9]{4}|[A-HJ-NP-Z2-9]{6})$/;
const TH_TIER_KEYS = { 100: 'A', 500: 'B', 1000: 'C' };
const TH_HUMAN_COLOR = '#7fb39a';

function thOnlineEnabled() {
  try {
    if (window.CASINO_CONFIG && window.CASINO_CONFIG.holdemOnline) return true;
    const q = new URLSearchParams(location.search);
    return q.has('online') || q.has('table');
  } catch (e) { return false; }
}
function thLoad(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
function thSave(k, v) { try { if (v === null) localStorage.removeItem(k); else localStorage.setItem(k, typeof v === 'string' ? v : JSON.stringify(v)); } catch (e) {} }
function thLedger() { try { const o = JSON.parse(thLoad(TH_LEDGER_KEY) || '{}'); return o && typeof o === 'object' ? o : {}; } catch (e) { return {}; } }
function thSetTableParam(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) {}
}
// A real name: chosen somewhere, not the leaderboards' "Player 7K2Q".
const thRealName = n => !!(n && n.trim() && !/^Player [0-9A-Z]{4}$/.test(n.trim()));
const thTierFor = key => TH_BUYIN_TIERS.find(t => TH_TIER_KEYS[t.buyin] === key) || TH_BUYIN_TIERS[0];

function THTimerRing({ 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: `thRing ${total}ms linear ${-elapsed}ms forwards` }} />
    </svg>
  );
}

// The end-of-hand card's words, from the table's result (the solo table's describeShowdown, by seat).
function thDescribe(r, snap, mySeat) {
  const nameOf = i => (i === mySeat ? 'You' : (snap.seats[i] && snap.seats[i].name) || 'A player');
  const won = i => r.gains[i] || 0;
  const ev = i => (r.ranks && r.ranks[i] ? r.ranks[i] : null);
  const plain = i => (ev(i) ? thPlainHand(ev(i)) : '');
  const labels = {};
  Object.keys(r.ranks || {}).forEach(i => { labels[i] = thShortHand(r.ranks[i]); });
  const back = r.uncalled && r.uncalled.amount > 0
    ? ` ${r.uncalled.seat === mySeat ? 'Your' : nameOf(r.uncalled.seat) + "'s"} uncalled ${thMoney(r.uncalled.amount)} ${r.uncalled.seat === mySeat ? 'comes' : 'goes'} back.` : '';
  let tone, title, detail;
  if (r.kind === 'fold') {
    const w = r.winners[0];
    tone = w === mySeat ? 'win' : 'lose';
    title = `${w === mySeat ? 'You win' : nameOf(w) + ' wins'} ${thMoney(r.contested)}`;
    detail = 'Everyone else folded.' + back;
  } else {
    const main = r.pots.reduce((a, p) => (!a || p.amount > a.amount ? p : a), null);
    const mainIds = main ? main.winners : [];
    if (won(mySeat) > 0) {
      const shared = r.pots.filter(p => p.winners.indexOf(mySeat) >= 0 && p.winners.length > 1);
      if (shared.length) {
        const others = [];
        shared.forEach(p => p.winners.forEach(i => { if (i !== mySeat && others.indexOf(i) < 0) others.push(i); }));
        tone = 'chop'; title = `Split pot: you get ${thMoney(won(mySeat))}`;
        detail = `You and ${others.map(nameOf).join(' and ')} both have ${plain(mySeat)}.`;
      } else {
        tone = 'win'; title = `You win ${thMoney(won(mySeat))}`; detail = `With ${plain(mySeat)}.`;
        const rival = Object.keys(r.ranks).map(Number).filter(i => i !== mySeat).sort((a, b) => TH_HAND.compareEval(r.ranks[b], r.ranks[a]))[0];
        if (rival != null) detail += ` ${nameOf(rival)} had ${plain(rival)}.`;
      }
    } else {
      tone = 'lose';
      const shown = mainIds[0];
      if (mainIds.length > 1) { title = `${mainIds.map(nameOf).join(' and ')} split ${thMoney(main.amount)}`; detail = `Each has ${plain(shown)}.`; }
      else { title = `${nameOf(shown)} wins ${thMoney(won(shown))}`; detail = `With ${plain(shown)}.`; }
      if (mySeat < 0 || !ev(mySeat)) detail += mySeat >= 0 && snap.seats[mySeat] && snap.seats[mySeat].status === 'folded' ? ' You folded.' : '';
      else detail += ` You had ${plain(mySeat)}.`;
    }
    detail += back;
  }
  return { id: r.no, tone, title, detail, winnerIds: r.winners, winCards: r.winCards || [], labels };
}

// ─── Online table ──────────────────────────────────────
function THOnlineTable({ mode, tierKey, code, playerName, onOpenSeat, onBusy, onClosed, onUnreachable, api }) {
  const [tweaks, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const NET = window.TH_ONLINE, B = window.CASINO_BANKROLL;
  const [canvas, setCanvas] = useState(thReadCanvas);
  const [bankroll, setBankroll] = useState(() => (B ? B.read() : 1000));
  const [snap, setSnap] = useState(null);
  const [status, setStatus] = useState('connecting');
  const [flash, setFlash] = useState(null);
  const [raiseTotal, setRaiseTotal] = useState(0);
  const [dealerMessage, setDealerMessage] = useState('');
  const [dealerExpression, setDealerExpression] = useState('idle');
  const [hint, setHint] = useState(null);
  const [copied, setCopied] = useState(false);
  const [confirmLeave, setConfirmLeave] = useState(null);
  const [, setTick] = useState(0);
  const offsetRef = useRef(0), prevRef = useRef(null), buyinRef = useRef(''), brokeRef = useRef(false);
  const recordedRef = useRef({}), startedRef = useRef(false), everLive = useRef(false), flashTimer = useRef(null);
  const leaveWaiters = useRef([]);
  const equityCache = useRef({ key: '', value: null });
  const portrait = canvas.portrait, sideways = canvas.sideways;
  const name = playerName || '';
  const dealerCtx = { player: name || 'friend', dealer: tweaks.dealerName };

  useEffect(() => {
    const onCanvas = () => setCanvas(thReadCanvas());
    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('th:canvas', onCanvas); window.addEventListener('casino:hydrated', onHydrated); window.addEventListener('casino:bonus', onBonus);
    return () => { window.removeEventListener('th:canvas', onCanvas); window.removeEventListener('casino:hydrated', onHydrated); window.removeEventListener('casino:bonus', onBonus); };
  }, []);
  function showFlash(t) { setFlash(t); clearTimeout(flashTimer.current); flashTimer.current = setTimeout(() => setFlash(null), 2800); }
  function dealerSay(key, expression) { setDealerMessage(thPickLine(key, dealerCtx)); setDealerExpression(expression || 'idle'); }

  // ── Money: settle the bankroll from the table's in/out totals ──
  function settleMoney(s) {
    const id = s.table.id, you = s.you, ledger = thLedger(), had = ledger[id];
    if (!had) ledger[id] = { in: you.in, out: you.out, at: Date.now() };   // first sight: anything before was settled elsewhere
    else {
      const delta = (you.out - had.out) - (you.in - had.in);
      if (delta && B) { const next = Math.max(0, B.read() + delta); B.write(next); setBankroll(next); }
      if (you.in > had.in && !startedRef.current) { startedRef.current = true; if (window.LGPlayNext) window.LGPlayNext.started(); }
      ledger[id] = { in: you.in, out: you.out, at: Date.now() };
    }
    // Done with a table (not seated, nothing waiting): forget it.
    if (you.seat === null && !you.waiting && !you.pending) delete ledger[id];
    thSave(TH_LEDGER_KEY, ledger);
  }
  // A hand's result goes into the casino stats and this table's own counters once.
  function recordResult(s) {
    const r = s.you.result;
    if (!r || r.table !== s.table.id) return;
    const key = r.table + ':' + r.hand;
    if (recordedRef.current[key] || thLoad('thLastHand') === key) return;
    recordedRef.current[key] = true; thSave('thLastHand', key);
    const me = s.seats.find(x => x.you);
    if (window.CASINO_STATS) {
      window.CASINO_STATS.recordEvent('texasHoldem', { won: r.won, payout: r.won ? Math.max(0, r.net) : 0, rare: r.won && r.rank && r.rank.rank >= TH_HAND.HAND_RANK.STRAIGHT_FLUSH ? 'straightFlush' : null, pot: r.pot });
      window.CASINO_STATS.recordPeak((B ? B.read() : 0) + (me ? me.stack : 0));
    }
    const st = loadStats();
    const better = !!(r.rank && (!st.bestHandEv || TH_HAND.compareEval(r.rank, st.bestHandEv) > 0));
    saveStats({ handsPlayed: st.handsPlayed + 1, handsWon: st.handsWon + (r.won ? 1 : 0), biggestPot: Math.max(st.biggestPot, r.pot),
      biggestWin: r.won ? Math.max(st.biggestWin, r.net) : st.biggestWin, bestHand: better ? r.label : st.bestHand, bestHandEv: better ? r.rank : st.bestHandEv });
  }
  // Buy in on your own: when you're seated (or waiting) with no chips and the bankroll covers it, as the solo table rebuys.
  function autoBuyin(s) {
    const me = s.seats.find(x => x.you), tier = s.table.tier;
    const empty = (me && me.stack === 0 && (!s.hand || s.hand.result || me.status === 'out' || me.status === 'folded' || !me.status)) || (s.you.waiting && !s.you.pending);
    if (!empty || s.you.leaving) return;
    const key = s.table.id + ':' + (s.hand ? s.hand.no : 0) + ':' + s.you.in;
    if (buyinRef.current === key) return;
    if ((B ? B.read() : 0) >= tier.buyin) { buyinRef.current = key; NET.send({ type: 'buyin' }); return; }
    if (!brokeRef.current && window.CASINO_RELOAD) {
      brokeRef.current = true;
      window.CASINO_RELOAD.open({
        game: 'texas-holdem', minBet: tier.buyin,
        message: `Your stack is gone${name ? ', ' + name : ''}, and your bankroll won't cover another ${thMoney(tier.buyin)} buy-in.`,
        continueLabel: 'Back to the table',
        onReset: v => { setBankroll(v); brokeRef.current = false; buyinRef.current = ''; },
        onClose: () => { brokeRef.current = false; },
      });
    }
  }
  function narrate(prev, s) {
    const h = s.hand, ph = prev && prev.hand;
    if (s.notice && (!prev || !prev.notice || prev.notice.at !== s.notice.at)) { setDealerMessage(s.notice.text); return; }
    if (!prev) { dealerSay('greet', 'happy'); return; }
    if (!h) return;
    if (!ph || ph.no !== h.no) { dealerSay('deal', 'deal'); if (tweaks.soundOn) TH_SFX.deal(); return; }
    if (h.result && !ph.result) {
      const mine = s.seats.findIndex(x => x.you), gain = mine >= 0 ? (h.result.gains[mine] || 0) : 0;
      if (gain > 0) { dealerSay(h.result.kind === 'fold' ? 'everyoneFold' : 'youWin', 'happy'); if (tweaks.soundOn) TH_SFX.win(); }
      else if (mine >= 0 && s.seats[mine].status && s.seats[mine].status !== 'out') { dealerSay('youLose', 'idle'); if (tweaks.soundOn && s.seats[mine].status !== 'folded') TH_SFX.lose(); }
      return;
    }
    if (ph.street !== h.street && TH_STREET_NAME[h.street]) { dealerSay(h.street, 'idle'); if (tweaks.soundOn) TH_SFX.card(); }
    else if (h.street === 'showdown' && ph.street !== 'showdown') { dealerSay('showdown', 'idle'); if (tweaks.soundOn) TH_SFX.showdown(); }
  }
  function onSnap(s, at) {
    offsetRef.current = s.now - at;
    everLive.current = true;
    settleMoney(s); recordResult(s); narrate(prevRef.current, s); autoBuyin(s);
    prevRef.current = s; setSnap(s);
    if (s.you.seat === null && !s.you.waiting && leaveWaiters.current.length) { const w = leaveWaiters.current; leaveWaiters.current = []; w.forEach(f => f()); }
  }

  useEffect(() => {
    const ledger = thLedger();
    // A public table at this stake you were at (a closed tab): back to it, so its result is settled.
    const resume = mode === 'online' ? Object.keys(ledger).filter(id => id[0] === tierKey && /^[ABC]\d+$/.test(id)).sort((a, b) => ledger[b].at - ledger[a].at)[0] : null;
    NET.start({ mode, tier: tierKey, code, table: resume || null, name }, {
      onState: onSnap,
      onError: m => showFlash(m),
      onStatus: (st, info) => {
        setStatus(st);
        if (st === 'closed' && onClosed) onClosed(info);
        // Never reached a table at all: play solo instead (solo is this same table, with the CPU players).
        if (st === 'offline' && !everLive.current && onUnreachable) onUnreachable();
      },
    });
    return () => NET.stop();
  }, []);
  useEffect(() => { const id = setInterval(() => setTick(n => n + 1), 250); return () => clearInterval(id); }, []);

  // Leaving (another table, the lobby link): fold out, cash out, then go once the table confirms (or after 3 s).
  function leaveThen(done) {
    if (!snap || (snap.you.seat === null && !snap.you.waiting)) { done(); return; }
    leaveWaiters.current.push(done);
    NET.send({ type: 'leave' });
    setTimeout(() => { if (leaveWaiters.current.indexOf(done) >= 0) { leaveWaiters.current = leaveWaiters.current.filter(f => f !== done); done(); } }, 3000);
  }
  if (api) api.current = { leaveThen };

  // ── Derived ──
  const serverNow = Date.now() + offsetRef.current;
  const h = snap ? snap.hand : null, tier = snap ? snap.table.tier : thTierFor(tierKey);
  const mySeat = snap ? snap.seats.findIndex(x => x.you) : -1;
  const meS = mySeat >= 0 ? snap.seats[mySeat] : null;
  const result = h && h.result ? thDescribe(h.result, snap, mySeat) : null;
  const myTurn = !!(h && !h.result && snap.phase === 'betting' && h.actor === mySeat && mySeat >= 0);
  const secondsLeft = snap ? Math.max(0, Math.ceil((snap.deadline - serverNow) / 1000)) : 0;
  const inPot = !!(meS && h && !h.result && meS.status && meS.status !== 'out' && meS.status !== 'folded' && meS.total > 0);
  useEffect(() => { if (onBusy) onBusy(inPot); }, [inPot]);
  const statusOf = x => (x.status === 'out' || !x.status ? 'sittingout' : x.status);
  const toSeat = (x, i) => ({
    id: i, name: x.you ? (name || 'You') : x.name, stack: x.stack || 0, bet: x.bet || 0, totalContrib: x.total || 0, status: statusOf(x),
    hole: x.hole || [], lastAction: x.lastAction, canRaise: x.canRaise !== false, isPlayer: !!x.you,
  });
  const positionFor = i => (!h ? 'utg' : i === h.button ? 'button' : i === h.sb ? 'sb' : i === h.bb ? 'bb' : 'utg');
  const blindOf = i => { const p = positionFor(i); return p === 'sb' ? 'SB' : p === 'bb' ? 'BB' : null; };

  // Coach: the solo table's advice, on what this seat can see.
  const hintKey = (tweaks.showHints && myTurn && meS && meS.hole) ? [h.no, h.street, h.currentBet, h.pot, meS.bet, meS.stack].join('|') : '';
  useEffect(() => {
    if (!hintKey) { setHint(null); return; }
    let alive = true;
    const id = setTimeout(() => {
      let x = null;
      try {
        const seats = snap.seats.map((s, i) => ({ id: i, status: statusOf(s), bet: s.bet || 0, totalContrib: s.total || 0 }));
        x = thHintFor({ seats, tier, board: h.board, phase: h.street, currentBet: h.currentBet, minRaise: h.minRaise, totalPot: h.pot },
          { id: mySeat, hole: meS.hole, bet: meS.bet, stack: meS.stack, totalContrib: meS.total, canRaise: meS.canRaise !== false }, positionFor, equityCache.current);
      } catch (e) {}
      if (alive) setHint(x);
    }, 30);
    return () => { alive = false; clearTimeout(id); };
  }, [hintKey]);
  useEffect(() => {
    if (!myTurn || !meS) return;
    const maxTotal = meS.bet + meS.stack;
    setRaiseTotal(Math.min(h.currentBet === 0 ? Math.max(tier.bb, h.minRaise) : h.currentBet + h.minRaise, maxTotal));
  }, [myTurn, h && h.street, h && h.currentBet]);

  function act(type, amount) {
    if (!myTurn) return;
    NET.send({ type, amount, hand: h.no, street: h.street, bet: h.currentBet });
    if (tweaks.soundOn && TH_SFX[type === 'raise' ? 'raise' : type]) TH_SFX[type === 'raise' ? 'raise' : type]();
  }
  const onFold = () => act('fold');
  const onCheckCall = () => act(h.currentBet > meS.bet ? 'call' : 'check');
  const onRaise = total => {
    if (meS.canRaise === false) { onCheckCall(); return; }
    const maxTotal = meS.bet + meS.stack;
    const minTotal = Math.min(h.currentBet === 0 ? Math.max(tier.bb, h.minRaise) : h.currentBet + h.minRaise, maxTotal);
    const t = thClamp(Math.round(Number(total)) || minTotal, minTotal, maxTotal);
    act('raise', t - meS.bet);
  };
  function copyInvite() {
    const link = location.origin + '/texas-holdem/?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); }
  }
  function navigate(e, href) {
    if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey || e.button === 1) return;
    e.preventDefault();
    if (inPot) { setConfirmLeave({ href }); return; }
    leaveThen(() => { window.location.href = href; });
  }

  // ── Seats: you at the bottom, the others clockwise from your left ──
  const order = [];
  for (let k = 1; k <= 3; k++) order.push(((mySeat < 0 ? 0 : mySeat) + k) % 4);
  if (mySeat < 0) order.unshift(0);
  const winCards = result ? result.winCards : [];
  const glowIdx = [];
  if (h) h.board.forEach((c, i) => { if (winCards.indexOf(c.id) >= 0) glowIdx.push(i); });
  const isWinner = i => !!(result && result.winnerIds.indexOf(i) >= 0);
  const opponentSeats = snap ? order.map(i => {
    const x = snap.seats[i];
    if (x.kind === 'empty') {
      return <div key={i} data-seat={i} style={{ flex: 1, minWidth: 0, minHeight: portrait || sideways ? 70 : 150, borderRadius: 14, border:'1.5px dashed rgba(230,197,144,.22)', display:'flex', alignItems:'center', justifyContent:'center', color:'rgba(230,197,144,.45)', fontSize: 10, letterSpacing:'.22em', textTransform:'uppercase' }}>Open seat</div>;
    }
    const arche = x.kind === 'cpu' ? TH_PERSONALITY.ARCHETYPES[x.cpu]
      : { id: 'human', name: x.name, shortName: x.name, color: TH_HUMAN_COLOR, initials: (x.name || 'P')[0].toUpperCase() };
    const shown = !!x.hole;
    const hole = x.hole || (x.cards ? [{ rank: 'A', suit: 'spades', id: 'b' + i + 'a' }, { rank: 'A', suit: 'spades', id: 'b' + i + 'b' }] : []);
    const msg = x.kind === 'human' ? (x.auto ? 'Stand-in playing' : !x.online ? 'Away' : x.sittingOut ? 'Sitting out' : null) : x.say;
    const active = !!(h && !h.result && h.actor === i && snap.phase === 'betting');
    return (
      <div key={i} data-seat={i} style={{ flex: 1, minWidth: 0, position:'relative' }}>
        <THOpponentSeat compact={portrait} side={sideways} archetype={arche} name={x.name + (x.kind === 'cpu' && !portrait && !sideways ? ' · CPU' : '')}
          stack={x.stack || 0} bet={x.bet || 0} status={statusOf(x)} isActive={active} isButton={!!h && h.button === i}
          blindLabel={h ? blindOf(i) : null} hole={hole} holeRevealed={shown} message={msg} winner={isWinner(i)}
          handLabel={shown && result ? result.labels[i] || null : null} glowIds={winCards} />
        {active && (
          <div style={{ position:'absolute', top: 4, right: 4, zIndex: 6 }}>
            <THTimerRing since={h.since} deadline={snap.deadline} offset={offsetRef.current} size={portrait || sideways ? 20 : 26} />
          </div>
        )}
      </div>
    );
  }) : [];
  const meSeat = meS ? toSeat(meS, mySeat) : null;
  const playerPanel = meSeat && meS.status ? (
    <PlayerSeatPanel compact={portrait} side={sideways} cardW={84} seat={meSeat} isActive={myTurn} isButton={!!h && h.button === mySeat}
      blindLabel={h ? blindOf(mySeat) : null} winner={isWinner(mySeat)} handLabel={result ? result.labels[mySeat] || null : null}
      glowIds={winCards} hint={(portrait || sideways) && tweaks.showHints ? hint : null} />
  ) : null;

  const pill = (label, onClick, gold) => (
    <button onClick={onClick} style={{ padding:'10px 18px', borderRadius: 999, cursor:'pointer', fontFamily:'inherit', fontSize: 12, fontWeight: 800, letterSpacing:'.14em', 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>
  );
  const waitLine = text => (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 12, height: portrait || sideways ? 64 : 80, fontSize: portrait || sideways ? 14 : 15, letterSpacing:'.2em', color:'var(--ivory-dim)', textTransform:'uppercase', textAlign:'center' }}>
      <span style={{ width: 10, height: 10, borderRadius:'50%', background:'var(--brass)', animation:'glowPulse 1s ease-in-out infinite', flex:'none' }} />{text}
    </div>
  );
  // Your clock: under the felt on a phone; beside your seat on a desktop, where there's room.
  const turnClock = myTurn && meS ? (
    <div data-th-clock style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 10, fontSize: portrait || sideways ? 15 : 16, fontWeight: 600, color: secondsLeft <= 5 ? 'var(--lose, #f29a8c)' : 'var(--ivory)' }}>
      <THTimerRing since={h.since} deadline={snap.deadline} offset={offsetRef.current} size={portrait || sideways ? 26 : 30} color={secondsLeft <= 5 ? '#f29a8c' : '#f5d896'} />
      {meS.auto ? 'The stand-in is playing for you. Make a move to take it back.'
        : <span><b style={{ fontFamily:"'JetBrains Mono', monospace", fontSize: portrait || sideways ? 20 : 23 }}>{secondsLeft}s</b> to decide, then the stand-in checks or folds</span>}
    </div>
  ) : null;
  const sideClock = myTurn && meS ? (
    <div data-th-clock style={{ display:'flex', flexDirection:'column', alignItems:'center', gap: 6, width: 150, textAlign:'center', color: secondsLeft <= 5 ? '#f29a8c' : 'var(--ivory)' }}>
      <THTimerRing since={h.since} deadline={snap.deadline} offset={offsetRef.current} size={44} color={secondsLeft <= 5 ? '#f29a8c' : '#f5d896'} />
      <b style={{ fontFamily:"'JetBrains Mono', monospace", fontSize: 28, lineHeight: 1 }}>{secondsLeft}s</b>
      <span style={{ fontSize: 14, lineHeight: 1.3, color:'var(--ivory-dim)' }}>{meS.auto ? 'The stand-in is playing. Make a move to take it back.' : 'to decide, then the stand-in checks or folds'}</span>
    </div>
  ) : null;
  let actionArea;
  if (!snap) actionArea = waitLine(status === 'connecting' ? 'Finding you a seat…' : 'Reconnecting…');
  else if (snap.you.released && mySeat < 0 && !snap.you.waiting) actionArea = (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 14, flexWrap:'wrap', minHeight: 80 }}>
      <span style={{ color:'var(--ivory-dim)', fontSize: 15 }}>You're cashed out of this table.</span>
      {pill('Sit back down', () => { buyinRef.current = ''; NET.send({ type: 'sit' }); }, true)}
    </div>
  );
  else if (myTurn && meS) actionArea = (
    <div style={{ display:'flex', flexDirection:'column', gap: 6 }}>
      {(portrait || sideways) && turnClock}
      <THActionZone compact={portrait} side={sideways} toCall={Math.max(0, h.currentBet - meS.bet)} currentBet={h.currentBet} myCurrentBet={meS.bet}
        minRaise={h.minRaise} myStack={meS.stack} pot={h.pot} bigBlind={tier.bb} canRaise={meS.canRaise !== false}
        raiseTotal={raiseTotal} setRaiseTotal={setRaiseTotal} hint={hint} onFold={onFold} onCheckCall={onCheckCall} onRaise={onRaise} />
    </div>
  );
  else if (result) actionArea = (
    <THHandResult compact={portrait || sideways} result={{ ...result, autoMs: Math.max(0, snap.deadline - serverNow), runId: snap.you.ready ? 1 : 0 }}
      onNext={() => NET.send({ type: 'next', hand: h.no })} paused={snap.you.ready} log={h.log} />
  );
  else if (snap.you.sittingOut && mySeat >= 0) actionArea = (
    <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 14, flexWrap:'wrap', minHeight: 80 }}>
      <span style={{ color:'var(--ivory-dim)', fontSize: 15 }}>You're sitting out. The table plays on without you.</span>
      {pill("I'm back", () => NET.send({ type: 'back' }), true)}
    </div>
  );
  else {
    const actor = h && h.actor >= 0 ? snap.seats[h.actor] : null;
    const text = snap.you.waiting ? "You'll be dealt in next hand"
      : !h ? (meS && meS.stack === 0 ? 'Buying in…' : 'Waiting for players…')
      : h.runout ? 'All-in · running it out…'
      : h.street === 'showdown' ? 'Showdown…'
      : actor ? `${actor.you ? 'You' : actor.name} ${actor.kind === 'cpu' ? 'is thinking…' : actor.auto ? '(stand-in) is acting…' : `to act · ${secondsLeft}s`}` : 'Dealing…';
    actionArea = waitLine(text);
  }

  const statusPill = (() => {
    const live = status === 'live';
    const label = status === 'retrying' ? 'Reconnecting…' : status === 'connecting' ? 'Finding a table…'
      : snap ? (snap.table.private ? 'Room code · ' + snap.table.id : 'Table ' + snap.table.id) + ' · $' + tier.sb + '/$' + tier.bb : '';
    return (
      <div style={{ display:'flex', gap: 8, alignItems:'center', flexWrap:'wrap', justifyContent: portrait || sideways ? 'center' : 'flex-start' }}>
        <span data-th-status={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: live ? '#7edb9c' : '#e6c590', boxShadow: live ? '0 0 8px #7edb9c' : 'none' }} />{label}
        </span>
        {snap && snap.table.private && (
          <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>
        )}
        {flash && <span style={{ padding:'5px 11px', borderRadius: 999, background:'rgba(20,12,6,.92)', border:'1px solid rgba(230,197,144,.4)', fontSize: 12, color:'var(--ivory)' }}>{flash}</span>}
      </div>
    );
  })();

  const overlays = (
    <>
      <style>{'@keyframes thRing{from{stroke-dashoffset:0}to{stroke-dashoffset:var(--ring)}}'}</style>
      {confirmLeave && meS && (
        <THConfirmModal title="Leave the table?" compact={portrait || sideways} side={sideways}
          body={`You have ${thMoney(meS.total)} in this pot; it stays in the pot. The rest of your stack, ${thMoney(meS.stack)}, goes back to your bankroll.`}
          confirmLabel="Leave" cancelLabel="Keep playing"
          onConfirm={() => { const href = confirmLeave.href; setConfirmLeave(null); leaveThen(() => { window.location.href = href; }); }}
          onCancel={() => setConfirmLeave(null)} />
      )}
      {status === 'elsewhere' && (
        <div style={{ position:'fixed', inset: 0, zIndex: 9000, background:'rgba(8,5,2,.8)', 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: 15 }}>Your seat moved to the tab you opened last.</div>
          {pill('Play here instead', () => NET.reconnect(), true)}
        </div>
      )}
    </>
  );
  const board = h ? h.board : [], pot = h ? h.pot : 0, street = h ? h.street : 'preflop';
  const tableStack = meS ? meS.stack : null;
  const st = loadStats();

  if (portrait) {
    return (
      <div style={{ width:'100%', height:'100%', padding: 8, position:'relative', display:'flex', flexDirection:'column', gap: 6 }}>
        <THPortraitRail bankroll={bankroll} tableStack={tableStack} showHints={tweaks.showHints} onToggleHints={() => setTweak('showHints', !tweaks.showHints)}
          onNavigate={navigate} playerName={name} onOpenProfile={onOpenSeat} />
        <div style={{ flex: 1, position:'relative', minHeight: 0 }}>
          <THFeltBackdrop />
          <div style={{ position:'absolute', inset: 0, padding: 8, display:'flex', flexDirection:'column', gap: 6 }}>
            {statusPill}
            <THDealerLine name={tweaks.dealerName} message={dealerMessage} />
            <div style={{ display:'flex', gap: 6 }}>{opponentSeats}</div>
            <div style={{ flex: 1, minHeight: 0, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap: 6 }}>
              <div style={{ height: 34, display:'flex', alignItems:'center' }}>{pot > 0 && <THPotDisplay compact pot={pot} />}</div>
              <THCommunityCards compact board={board} phase={street} glowIdx={glowIdx} w={78} h={109} />
            </div>
            {playerPanel}
          </div>
        </div>
        <div style={{ flex:'none', minHeight: 166, display:'flex', flexDirection:'column', justifyContent:'flex-end' }}>{actionArea}</div>
        {overlays}
      </div>
    );
  }
  if (sideways) {
    return (
      <div style={{ width:'100%', height:'100%', padding: 8, position:'relative', display:'flex', flexDirection:'column', gap: 8 }}>
        <THSidewaysRail bankroll={bankroll} tableStack={tableStack} showHints={tweaks.showHints} onToggleHints={() => setTweak('showHints', !tweaks.showHints)}
          onNavigate={navigate} dealerName={tweaks.dealerName} dealerMessage={dealerMessage} playerName={name} onOpenProfile={onOpenSeat} />
        <div style={{ flex: 1, minHeight: 0, display:'flex', gap: 8 }}>
          <div style={{ flex: 1, minWidth: 0, position:'relative' }}>
            <THFeltBackdrop />
            <div style={{ position:'absolute', inset: 0, padding: 8, display:'flex', flexDirection:'column', gap: 6 }}>
              {statusPill}
              <div style={{ display:'flex', gap: 6 }}>{opponentSeats}</div>
              <div style={{ flex: 1, minHeight: 0, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap: 6 }}>
                <div style={{ height: 36, display:'flex', alignItems:'center' }}>{pot > 0 && <THPotDisplay side pot={pot} />}</div>
                <THCommunityCards compact board={board} phase={street} glowIdx={glowIdx} w={90} h={126} />
              </div>
            </div>
          </div>
          <div style={{ width: 340, flex:'none', minHeight: 0, display:'flex', flexDirection:'column', justifyContent:'space-between', gap: 8 }}>
            {playerPanel}
            <div>{actionArea}</div>
          </div>
        </div>
        {overlays}
      </div>
    );
  }
  return (
    <div style={{ width:'100%', height:'100%', display:'flex', alignItems:'center', justifyContent:'center', padding: 24, position:'relative' }}>
      <div style={{ width: 1380, maxWidth:'100%', height:'100%', maxHeight: 860, display:'flex', gap: 14, position:'relative' }}>
        <THDealerStrip name={tweaks.dealerName} gender={CASINO_DEALERS.get(tweaks.dealerName).art} expression={dealerExpression} message={dealerMessage} />
        <div style={{ flex: 1, position:'relative', minWidth: 800 }}>
          <THFeltBackdrop />
          <div style={{ position:'absolute', inset: 0, display:'flex', flexDirection:'column' }}>
            <THBrassRail bankroll={bankroll} tableStack={tableStack} blinds={`$${tier.sb}/$${tier.bb}`} handsPlayed={st.handsPlayed} biggestPot={st.biggestPot}
              showHints={tweaks.showHints} onToggleHints={() => setTweak('showHints', !tweaks.showHints)} onNavigate={navigate}
              playerName={name} onOpenProfile={onOpenSeat} />
            <div style={{ flex: 1, position:'relative', padding:'8px 32px 0', display:'flex', flexDirection:'column' }}>
              <THFeltLogo />
              <div style={{ position:'relative', zIndex: 4 }}>{statusPill}</div>
              <div style={{ marginTop: 10, display:'flex', justifyContent:'space-between', gap: 12, position:'relative', zIndex: 3, paddingRight: tweaks.showHints && myTurn ? 280 : 0, transition:'padding-right .2s ease' }}>
                {opponentSeats}
              </div>
              <div style={{ flex: 1, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap: 14, position:'relative', zIndex: 2, marginTop: 6 }}>
                {pot > 0 && <THPotDisplay pot={pot} />}
                <THCommunityCards board={board} phase={street} glowIdx={glowIdx} />
              </div>
              {meS && (
                <div style={{ position:'relative', zIndex: 3, display:'flex', alignItems:'flex-end', justifyContent:'center', gap: 24, marginBottom: 6 }}>
                  {sideClock && <div style={{ width: 150 }} />}
                  {playerPanel || <div style={{ color:'rgba(230,197,144,.6)', fontSize: 12, letterSpacing:'.22em', textTransform:'uppercase', paddingBottom: 12 }}>{name || 'You'} · {thMoney(meS.stack)}</div>}
                  {sideClock}
                </div>
              )}
              {tweaks.showHints && hint && myTurn && <THHintPanel hint={hint} label={hint.label} equity={hint.equity} potOdds={hint.potOdds} />}
            </div>
            <div style={{ position:'relative', zIndex: 5, padding:'4px 26px 18px', minHeight: 130 }}>
              {h && !h.result && <THActionLog entry={h.log[h.log.length - 1]} />}
              {actionArea}
            </div>
          </div>
        </div>
      </div>
      {overlays}
    </div>
  );
}

// ─── Seat card ─────────────────────────────────────────
// Every visit (with online tables on): where to play and at what stake, and your name only if there isn't one yet.
function THSeatCard({ initialName = '', lastMode = 'solo', lastTier = 'A', bankroll, firstVisit, busy, currentMode, error, working, onChoose, onCancel }) {
  const named = thRealName(initialName);
  const [name, setName] = React.useState(named ? initialName : '');
  const [editing, setEditing] = React.useState(!named);
  const [selected, setSelected] = React.useState(lastMode);
  const affordable = TH_BUYIN_TIERS.filter(t => bankroll >= t.buyin);
  const [tier, setTier] = React.useState(() => {
    const want = thTierFor(lastTier);
    return bankroll >= want.buyin ? TH_TIER_KEYS[want.buyin] : affordable.length ? TH_TIER_KEYS[affordable[affordable.length - 1].buyin] : 'A';
  });
  const codeRef = React.useRef(null), startRef = React.useRef(null);
  const [code, setCode] = React.useState('');
  const inputRef = React.useRef(null), modeRef = React.useRef(null), downOnBackdrop = React.useRef(false);
  React.useEffect(() => {
    const id = setTimeout(() => { if (inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } else if (modeRef.current) modeRef.current.focus(); }, 50);
    return () => clearTimeout(id);
  }, [editing]);
  const dismiss = firstVisit ? () => onChoose(lastMode, tier, 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;
  const canPay = bankroll >= thTierFor(tier).buyin;
  function choose(m, c) { if (locked(m) || working) return; if (!c && !canPay) return; setSelected(m); onChoose(m, tier, name, c); }
  const options = [
    { mode: 'solo', title: 'Solo', sub: 'You and three CPU players' },
    { 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 = TH_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-th-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={e => { e.preventDefault(); choose(selected); }} 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:'22px 26px 20px', boxShadow:'0 30px 80px rgba(0,0,0,.7)', width: 460, maxWidth:'100%', maxHeight:'100%', overflowY:'auto', textAlign:'center',
      }}>
        <div style={{ fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 24, color:'var(--brass-2)', marginBottom: 12 }}>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:'10px 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-th-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="Stake" style={{ display:'flex', gap: 8, marginTop: 14 }}>
          {TH_BUYIN_TIERS.map(t => {
            const k = TH_TIER_KEYS[t.buyin], on = tier === k, off = bankroll < t.buyin || (busy && !on);
            return (
              <button key={k} type="button" role="radio" aria-checked={on} data-tier={k} disabled={off} onClick={() => setTier(k)} style={{
                flex: 1, padding:'9px 6px', borderRadius: 10, cursor: off ? 'not-allowed' : 'pointer', fontFamily:'inherit', opacity: off ? .4 : 1,
                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)', color:'var(--ivory)',
              }}>
                <span style={{ display:'block', fontFamily:"'Playfair Display', serif", fontStyle:'italic', fontSize: 18, fontWeight: 700 }}>{thMoney(t.buyin)}</span>
                <span style={{ display:'block', fontSize: 11.5, color:'var(--ivory-dim)', marginTop: 1 }}>{t.subtitle}</span>
              </button>
            );
          })}
        </div>
        {!affordable.length && <div style={{ marginTop: 8, fontSize: 13, color:'var(--brass-2)' }}>Your bankroll is below the smallest buy-in. Pick a table and you'll be offered a free reset.</div>}
        <div role="radiogroup" aria-label="Where to play" style={{ display:'flex', flexDirection:'column', gap: 8, marginTop: 12 }}>
          {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={() => (!affordable.length ? onChoose(o.mode, tier, name, null, true) : o.mode === 'private' ? openFriends(false) : choose(o.mode))} onFocus={() => setSelected(o.mode)}
                style={{
                  display:'flex', alignItems:'center', gap: 12, textAlign:'left', padding:'11px 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,
                }}>
                <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>
                {on && <span style={{ fontSize: 10, letterSpacing:'.16em', color:'var(--brass-2)', textTransform:'uppercase', fontWeight: 700 }}>{working ? '…' : 'Enter ↵'}</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>
        )}
        {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:'#f29a8c' }}>{error}</div>}
        <div style={{ marginTop: 10, fontSize: 13 }}>
          {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>
        <THSignInNote fontSize={13} />
        <div style={{ display:'flex', gap: 10, marginTop: 12, 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 THRoot() {
  const enabled = React.useMemo(thOnlineEnabled, []);
  const linkCode = React.useMemo(() => {
    try { const c = (new URLSearchParams(location.search).get('table') || '').toUpperCase(); return TH_CODE.test(c) ? c : null; } catch (e) { return null; }
  }, []);
  const lastMode = (() => { const m = thLoad(TH_MODE_KEY); return m === 'online' || m === 'private' || m === 'solo' ? m : 'solo'; })();
  const lastTier = (() => { const t = thLoad(TH_TIER_KEY); return t === 'B' || t === 'C' ? t : 'A'; })();
  const [mode, setMode] = React.useState(!enabled ? 'plain' : linkCode ? 'private' : null);
  const [tier, setTier] = React.useState(lastTier);
  const [code, setCode] = React.useState(linkCode);
  const [card, setCard] = React.useState(enabled && !linkCode ? '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);
  const [soloKey, setSoloKey] = React.useState(0);
  const tableApi = React.useRef(null);

  function go(m, t, c) {
    thSave(TH_MODE_KEY, m); thSave(TH_TIER_KEY, t);
    if (m === 'private') thSetTableParam(c); else thSetTableParam(null);
    setTier(t); setCode(m === 'private' ? c : null); setMode(m); setCard(null); setSoloKey(k => k + 1);
  }
  function choose(m, t, typed, joinCode, broke) {
    const trimmed = (typed || '').trim();
    if (trimmed && trimmed !== name) { const saved = window.CASINO_PLAYER.write(trimmed); setName(saved || trimmed); }
    else if (!trimmed && card === 'first' && !name) window.CASINO_PLAYER.set('nameSkipped', true);
    setError('');
    // Not enough for any buy-in: the solo lobby offers the free reset.
    if (broke) { go('solo', 'A'); return; }
    const leaveFirst = then => (tableApi.current && (mode === 'online' || mode === 'private') ? tableApi.current.leaveThen(then) : then());
    if (m === 'private' && !joinCode && !(mode === 'private' && code)) {
      setWorking(true);
      window.TH_ONLINE.createPrivate(t).then(c => { setWorking(false); leaveFirst(() => go('private', t, c)); },
        () => { setWorking(false); setError('Couldn\'t open a private table just now. Try again, or play Solo.'); });
      return;
    }
    if (m === mode && (m !== 'private' || !joinCode || joinCode === code) && t === tier) { setCard(null); return; }
    leaveFirst(() => go(m, t, joinCode || code));
  }
  function closed(info) {
    setError((info && info.message ? info.message : 'That table is closed.') + ' Start a new one, or pick another table.');
    thSetTableParam(null); setCode(null); setBusy(false); setMode(null); setCard('edit');
  }
  function unreachable() {
    go('solo', tier);
    setTimeout(() => { try { window.alert('The online tables can\'t be reached right now, so you\'re at a solo table with the CPU players.'); } catch (e) {} }, 50);
  }
  const bankroll = window.CASINO_BANKROLL ? window.CASINO_BANKROLL.read() : 0;
  if (mode === 'plain') return <App />;
  return (
    <>
      {mode === 'online' || mode === 'private'
        ? <THOnlineTable key={mode + ':' + tier + ':' + (code || '')} mode={mode} tierKey={tier} code={code} playerName={name} api={tableApi}
            onOpenSeat={() => setCard('edit')} onBusy={setBusy} onClosed={closed} onUnreachable={unreachable} />
        : <App key={'solo' + soloKey} autoTier={mode === 'solo' ? thTierFor(tier) : null} seatCard onOpenSeat={() => setCard('edit')} onBusy={setBusy} />}
      {card && (
        <THSeatCard initialName={name} lastMode={card === 'first' ? lastMode : (mode || lastMode)} lastTier={tier} bankroll={bankroll}
          firstVisit={card === 'first'} busy={busy} currentMode={mode} error={error} working={working}
          onChoose={choose} onCancel={() => { setCard(null); setError(''); }} />
      )}
    </>
  );
}

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