// pages-marketing.jsx — home, how-it-works, fairness, leaderboard, profile

const { useState: useStateP, useEffect: useEffectP, useRef: useRefP, useMemo: useMemoP } = React;

// Per-game accent color, normalized to lowercase. Matches the host game's
// page header so the leaderboard / profile / history surfaces stay coherent.
function gameColor(name, M) {
  const map = {
    flip:M.yellow, slots:M.hot, blackjack:M.cyan, baccarat:M.green, roulette:M.red,
    hilo:M.cyan, videopoker:M.hot, 'video poker':M.hot, mines:M.yellow,
    plinko:M.cyan, towers:M.green,
  };
  return map[(name || '').toLowerCase()] || M.yellow;
}

// ─── HOME ────────────────────────────────────────────────────────
function HomePage({ setPage, palette: M, wallet, fairness, online }) {
  const wins = useLiveWins(wallet.history, wallet.username, 8);
  return (
    <div className="rc-page rc-home" style={{flex:1, padding:'18px 22px 40px', overflowY:'auto'}}>
      {/* Hero terminal — coin centered above the headline */}
      <Frame title="run.exe" accent={M.green} style={{padding:'24px 28px'}}>
        <div style={{display:'flex', flexDirection:'column', alignItems:'center', gap:18, textAlign:'center'}}>
          <PixelCoin size={18}/>
          <div style={{color:M.green, fontSize:18}}>$ ./run --send-it</div>
          <h1 className="rc-h-hero" style={{
            margin:0, fontSize:80, lineHeight:0.92, fontWeight:700, color:M.yellow,
            letterSpacing:'-0.03em', fontFamily:'inherit',
          }}>FLIP. SPIN.<br/>SEND IT.</h1>
          <div style={{fontSize:18, color:M.ink2, fontStyle:'italic'}}>8 games · provably fair · no kyc · keys stay yours</div>
          <div style={{display:'flex', gap:10, flexWrap:'wrap', justifyContent:'center'}}>
            <button onClick={()=>setPage('flip')} style={{
              background:M.hot, color:M.bg, border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:24, fontWeight:700,
              padding:'10px 22px', cursor:'pointer', boxShadow:`6px 6px 0 ${M.green}`, letterSpacing:'0.04em',
            }}>{'>'} RUN IT {'<'}</button>
            <button onClick={()=>setPage('how')} style={{background:'transparent', color:M.green, border:`3px solid ${M.green}`, fontFamily:'inherit', fontSize:18, padding:'10px 18px', cursor:'pointer'}}>[how it works]</button>
            <button onClick={()=>setPage('fairness')} style={{background:'transparent', color:M.cyan, border:`3px solid ${M.cyan}`, fontFamily:'inherit', fontSize:18, padding:'10px 18px', cursor:'pointer'}}>[verify fairness]</button>
          </div>
          <div style={{color:M.ink2, fontSize:16, display:'flex', gap:18, flexWrap:'wrap', justifyContent:'center'}}>
            <span><span style={{color:M.green}}>●</span> {online.toLocaleString()} degens online</span>
            <span><span style={{color:M.yellow}}>★</span> ${(online*0.42).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g,',')} wagered today</span>
            <span><span style={{color:M.hot}}>♥</span> ~0.3s settlement</span>
          </div>
        </div>
      </Frame>

      {/* ASCII sep */}
      <div style={{textAlign:'center', color:M.green, fontSize:14, opacity:0.5, padding:'14px 0', whiteSpace:'pre', overflow:'hidden'}}>═══════════════════════════════════════════════════════════════════════════════════</div>

      <h2 style={{margin:'4px 0 12px', fontSize:28, color:M.green}}>{'>'} pick your poison_</h2>
      <div className="rc-game-grid" style={{display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:14}}>
        <GameCard featured title="COIN FLIP" sub="99% RTP · the OG · 50/50 odds · 2× payout" hue={M.yellow} icon="◐" rtp="99%" edge="1%" onClick={()=>setPage('flip')}/>
        <GameCard title="SLOTS" sub="96.8% RTP · 3 or 6 reels" hue={M.hot} icon={(
          <svg width="48" height="38" viewBox="0 0 48 38" fill="none" stroke="currentColor" strokeWidth="3" aria-hidden="true">
            <rect x="2"    y="3" width="11" height="32"/>
            <rect x="18.5" y="3" width="11" height="32"/>
            <rect x="35"   y="3" width="11" height="32"/>
          </svg>
        )} rtp="96.8%" edge="3.2%" onClick={()=>setPage('slots')}/>
        <GameCard title="BLACKJACK" sub="99% RTP · 6-deck shoe" hue={M.cyan} icon="♠" rtp="99%" edge="1%" onClick={()=>setPage('blackjack')}/>
        <GameCard title="ROULETTE" sub="97.3% RTP · single-zero" hue={M.red} icon="◯" rtp="97.3%" edge="2.7%" onClick={()=>setPage('roulette')}/>
        <GameCard title="BACCARAT" sub="98.94% RTP · banker odds" hue={M.green} icon="♣" rtp="98.94%" edge="1.06%" onClick={()=>setPage('baccarat')}/>
        <GameCard title="HI-LO" sub="99% RTP · higher or lower" hue={M.cyan} icon="⇅" rtp="99%" edge="1%" onClick={()=>setPage('hilo')}/>
        <GameCard title="VIDEO POKER" sub="99.10% RTP · jacks or better" hue={M.hot} icon="♥" rtp="99.10%" edge="0.90%" onClick={()=>setPage('poker')}/>
        <GameCard title="MINES" sub="99% RTP · pick safe, cash out" hue={M.yellow} icon="▣" rtp="99%" edge="1%" onClick={()=>setPage('mines')}/>
      </div>

      <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:14, marginTop:24}}>
        <Frame title="♔ TOP DEGENS · 7d ♔" accent={M.green}><LeaderboardMini palette={M}/></Frame>
        <Frame title="► LIVE FEED" accent={M.hot}><LiveFeed wins={wins} palette={M}/></Frame>
      </div>

      {/* CTA banner */}
      <Frame title="" accent={M.yellow} style={{marginTop:18, padding:'18px 22px', display:'flex', alignItems:'center', gap:18, flexWrap:'wrap'}}>
        <span style={{fontSize:36, color:M.yellow}}>★</span>
        <div style={{flex:1, minWidth:200}}>
          <div style={{fontSize:22, color:M.yellow}}>not just degen — verifiable.</div>
          <div style={{fontSize:16, color:M.ink2}}>every roll is sha-256 of (server seed : client seed : nonce). committed before, revealed after, no funny business.</div>
        </div>
        <button onClick={()=>setPage('fairness')} style={{background:M.yellow, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:18, padding:'8px 16px', cursor:'pointer', boxShadow:`4px 4px 0 ${M.bg}`}}>[verify a roll]</button>
      </Frame>
    </div>
  );
}

function PixelCoin({ size=10 }) {
  const M = window.MEMEColors;
  const grid = ['00111100','01211110','12111213','12112213','13211223','13211233','01333310','00333300'];
  return (
    <div style={{display:'inline-block', position:'relative'}}>
      {grid.map((row,y)=>(
        <div key={y} style={{display:'flex', height:size}}>
          {row.split('').map((cell,x)=>{
            const c = cell==='0'?'transparent':cell==='1'?M.yellow:cell==='2'?'#fffba0':'#b88a00';
            return <div key={x} style={{width:size, height:size, background:c}}/>;
          })}
        </div>
      ))}
    </div>
  );
}

function GameCard({ title, sub, hue, icon, featured, onClick, rtp, edge }) {
  const M = window.MEMEColors;
  const ref = useRefP();
  return (
    <button onClick={onClick} ref={ref} style={{
      background:M.panel, border:`2px solid ${hue}`, padding:0, color:M.ink, fontFamily:'inherit',
      cursor:'pointer', textAlign:'left', position:'relative', overflow:'hidden',
      gridRow: featured?'span 2':'auto', minHeight:featured?240:130,
      boxShadow:`6px 6px 0 ${hue}`, transition:'all 0.1s',
    }} onMouseEnter={e=>{e.currentTarget.style.transform='translate(-2px, -2px)'; e.currentTarget.style.boxShadow=`8px 8px 0 ${hue}`;}}
       onMouseLeave={e=>{e.currentTarget.style.transform='translate(0,0)'; e.currentTarget.style.boxShadow=`6px 6px 0 ${hue}`;}}>
      <div style={{
        height: featured?160:70, background: `repeating-linear-gradient(45deg, ${hue}11 0, ${hue}11 4px, transparent 4px, transparent 8px)`,
        display:'flex',alignItems:'center',justifyContent:'center', borderBottom:`2px solid ${hue}`,
      }}>
        <div style={{fontSize:featured?80:50, color:hue}}>{icon}</div>
      </div>
      <div style={{padding:'10px 14px'}}>
        <div style={{fontWeight:700, fontSize:featured?28:20, color:hue, letterSpacing:'-0.01em'}}>{title}</div>
        <div style={{fontSize:14, color:M.ink2, marginTop:2}}>{sub}</div>
        <div style={{display:'flex', gap:8, marginTop:6, fontSize:12, color:M.ink2}}>
          <span>RTP <span style={{color:M.green}}>{rtp}</span></span>
          <span>edge <span style={{color:M.hot}}>{edge}</span></span>
        </div>
      </div>
    </button>
  );
}

function LeaderboardMini({ palette: M }) {
  const rows = [
    {u:'gigachad', w:228.4}, {u:'milady', w:146.7}, {u:'jitobro', w:107.1},
    {u:'frenpump', w:74.1}, {u:'cryptodegen', w:54.2}, {u:'wagmi420', w:43.0},
  ];
  return (
    <div>{rows.map((r,i)=>(
      <div key={r.u} style={{display:'flex', alignItems:'center', gap:10, padding:'5px 0', fontSize:18, borderBottom:i<rows.length-1?`1px dashed ${M.green}33`:'none'}}>
        <span style={{width:30, color:i===0?M.yellow:i===1?M.cyan:i===2?M.hot:M.ink2, fontWeight:700}}>#{i+1}</span>
        <span style={{flex:1, color: i<3?M.yellow:M.ink}}>{r.u}{i===0?' 👑':''}</span>
        <span style={{color:M.green, fontWeight:700}}>{r.w.toFixed(2)} SOL</span>
      </div>
    ))}</div>
  );
}

function LiveFeed({ wins, palette: M }) {
  return (
    <div style={{maxHeight:200, overflow:'hidden', display:'flex', flexDirection:'column', gap:4, fontSize:16}}>
      {wins.length === 0 ? (
        <div style={{color:M.ink2, lineHeight:1.4}}>{'>'} no wins yet — pick a game and run it.</div>
      ) : wins.slice(0,7).map(w=>(
        <div key={w.id} style={{lineHeight:1.4}}>
          <span style={{color:M.green}}>[{new Date(w.t).toLocaleTimeString().slice(0,5)}]</span>
          <span style={{color:M.yellow}}> {w.user}</span>
          <span style={{color:M.ink2}}> won </span>
          <span style={{color:M.hot, fontWeight:700}}>{w.amt} {w.coin}</span>
          <span style={{color:M.ink2}}> on <span style={{textTransform:'capitalize'}}>{w.game}</span> </span>
          <span style={{color:M.cyan}}>({w.mult.toFixed(1)}x)</span>
        </div>
      ))}
    </div>
  );
}

// ─── HOW IT WORKS ────────────────────────────────────────────────
function HowPage({ setPage, palette: M }) {
  const steps = [
    { n:'01', t:'connect a wallet', d:'phantom or solflare. we never touch your keys. signing a message proves the bag is yours.' },
    { n:'02', t:'pick a game', d:'8 games. every one publishes a server-seed hash before you bet. that hash is the commitment.' },
    { n:'03', t:'commit your seed + place a bet', d:'you set a client seed (or roll a random one). it gets mixed with the server seed and a nonce to produce the round.' },
    { n:'04', t:'roll', d:'we sha-256 (server : client : nonce). the resulting hex deterministically picks the outcome — coin side, slot reels, dealer hand, ball pocket, card draw.' },
    { n:'05', t:'verify', d:'rotate your seed on the fairness page any time. when you do, we reveal the previous server seed and you can recompute hmac-sha256(server, client:nonce) against any recorded round.' },
  ];
  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.yellow, margin:'4px 0 4px', textShadow:`3px 3px 0 ${M.hot}`}}>HOW IT WORKS</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:18}}>5 minutes of reading and you'll know more about online gambling than 99% of online gamblers.</div>
      <div style={{display:'flex', flexDirection:'column', gap:14}}>
        {steps.map(s => (
          <div key={s.n} className="rc-how-step" style={{display:'grid', gridTemplateColumns:'80px 1fr', gap:14, border:`2px dashed ${M.green}`, padding:'14px 18px', background:M.panel}}>
            <div style={{fontSize:48, color:M.yellow, fontWeight:700, lineHeight:1}}>{s.n}</div>
            <div>
              <div style={{fontSize:24, color:M.green}}>{s.t}</div>
              <div style={{fontSize:16, color:M.ink, marginTop:4, lineHeight:1.5}}>{s.d}</div>
            </div>
          </div>
        ))}
      </div>

      <h2 style={{fontSize:28, color:M.hot, marginTop:28, marginBottom:10}}>{'>'} the math_</h2>
      <Frame title="rtps & house edges" accent={M.cyan}>
        <table style={{width:'100%', borderCollapse:'collapse', fontSize:16, color:M.ink, tableLayout:'auto'}}>
          <thead><tr style={{color:M.ink2}}>
            <th style={{textAlign:'left', padding:'4px 8px'}}>game</th>
            <th style={{textAlign:'right', padding:'4px 8px'}}>RTP</th>
            <th style={{textAlign:'right', padding:'4px 8px'}}>edge</th>
            <th className="rc-hide-mobile" style={{textAlign:'left', padding:'4px 8px'}}>note</th>
          </tr></thead>
          <tbody>
            {[
              ['flip','99%','1%','50/50 odds @ 2× payout · 1% commission on wins'],
              ['slots','96.8%','3.2%','3-reel · 11 symbols · 3-of-a-kind / family / pair · pro mode 6-reel runs 97.8%'],
              ['blackjack','99%','1%','6-deck shoe · h17 · double any · split aces once'],
              ['roulette','97.3%','2.7%','european single-zero · pari-mutuel pays'],
              ['baccarat','98.94%','1.06%','banker bet · 5% commission'],
              ['hi-lo','99%','1%','higher / lower vs dealer card · per-rank multipliers · ties push'],
              ['video poker','99.10%','0.90%','jacks or better · 10/6 paytable · royal 200× · sf 30×'],
              ['mines','99%','1%','5×5 grid · pick safe tiles · cash out before a mine'],
            ].map((r,i)=>(
              <tr key={i} style={{borderTop:`1px dashed ${M.green}33`}}>
                <td style={{padding:'6px 8px', color:M.yellow, textTransform:'uppercase'}}>{r[0]}</td>
                <td style={{padding:'6px 8px', textAlign:'right', color:M.green}}>{r[1]}</td>
                <td style={{padding:'6px 8px', textAlign:'right', color:M.hot}}>{r[2]}</td>
                <td className="rc-hide-mobile" style={{padding:'6px 8px', color:M.ink2}}>{r[3]}</td>
              </tr>
            ))}
          </tbody>
        </table>
      </Frame>

      <Frame title="readme.txt" accent={M.green} style={{marginTop:18}}>
        <div style={{fontSize:16, color:M.ink, lineHeight:1.6}}>
          {'>'} casinos are designed to make money. that is fine.<br/>
          {'>'} the deal at run! is: we tell you the edge. we publish every seed. we let you verify every roll.<br/>
          {'>'} you cannot beat the math. you can only have fun on the way.<br/>
          {'>'} <span style={{color:M.yellow}}>set a budget. lose it. log off. play tomorrow.</span>
        </div>
      </Frame>
    </div>
  );
}

// ─── FAIRNESS ────────────────────────────────────────────────────
function FairnessPage({ palette: M, wallet, fairness }) {
  // Persist verify-input across page navigation. Without this, leaving the
  // page (e.g. to copy a hash from a game) wipes whatever's been pasted in.
  const [verifyInput, setVerifyInput] = useStateP(() => {
    try {
      const s = localStorage.getItem('run!verifyInput');
      if (s) {
        const p = JSON.parse(s);
        return { server: p.server || '', client: p.client || '', nonce: +p.nonce || 0 };
      }
    } catch {}
    return { server:'', client:'', nonce:0 };
  });
  useEffectP(() => {
    try { localStorage.setItem('run!verifyInput', JSON.stringify(verifyInput)); } catch {}
  }, [verifyInput]);
  const [verifyOut, setVerifyOut] = useStateP(null);
  const [signing, setSigning] = useStateP(false);
  const [signOut, setSignOut] = useStateP(null);
  const [rotating, setRotating] = useStateP(false);

  // Verifier input validation (audit MED-2): require hex, sane lengths.
  function validInputs() {
    const s = (verifyInput.server || '').trim().toLowerCase();
    const c = (verifyInput.client || '').trim().toLowerCase();
    const n = Number(verifyInput.nonce);
    if (!/^[0-9a-f]{32,128}$/.test(s)) return { err: 'server seed must be 32-128 hex characters' };
    if (!/^[0-9a-f]{16,128}$/.test(c)) return { err: 'client seed must be 16-128 hex characters' };
    if (!Number.isInteger(n) || n < 0 || n > 1e9) return { err: 'nonce must be a non-negative integer' };
    return { server: s, client: c, nonce: n };
  }

  async function computeVerify(parsed) {
    const hex = await hmacSha256Hex(parsed.server, `${parsed.client}:${parsed.nonce}`);
    // Cross-check that the supplied server seed matches a published commitment.
    const commitmentHash = await sha256Hex(parsed.server);
    let matchedCommitment = null;
    if (commitmentHash === fairness.serverHash) {
      matchedCommitment = { hash: fairness.serverHash, label: 'current' };
    } else if (fairness.revealedSeeds) {
      const m = fairness.revealedSeeds.find(r => r.commitment === commitmentHash);
      if (m) matchedCommitment = { hash: commitmentHash, label: 'previously revealed' };
    }
    // Cross-check the computed round hash against any recorded round.
    const matchedRound = fairness.history.find(h =>
      h.hex === hex && h.clientSeed === parsed.client && h.nonce === parsed.nonce
    );
    return { hex, commitmentHash, matchedCommitment, matchedRound };
  }

  async function doVerify() {
    const parsed = validInputs();
    if (parsed.err) { setVerifyOut({ err: parsed.err }); return; }
    setVerifyOut(await computeVerify(parsed));
  }

  // Auto-run verifier on input change (debounced 300ms). Silent on validation
  // failure so partial typing doesn't flash error toasts; the compute button
  // still surfaces an explicit error if clicked with invalid fields.
  useEffectP(() => {
    const parsed = validInputs();
    if (parsed.err) { setVerifyOut(null); return; }
    let cancelled = false;
    const t = setTimeout(async () => {
      const result = await computeVerify(parsed);
      if (!cancelled) setVerifyOut(result);
    }, 300);
    return () => { cancelled = true; clearTimeout(t); };
  }, [verifyInput, fairness.serverHash, fairness.revealedSeeds, fairness.history]); // eslint-disable-line

  async function doRotate() {
    if (rotating) return;
    setRotating(true);
    try { await fairness.rotateSeed(); } finally { setRotating(false); }
  }

  async function doSign() {
    setSigning(true);
    try {
      const msg = `Run! casino: I, ${wallet.address}, am alive at ${new Date().toISOString()}.`;
      const sig = await wallet.signMessage(msg);
      setSignOut({ msg, sig });
    } catch (e) {
      setSignOut({ err: e?.message || 'rejected' });
    } finally { setSigning(false); }
  }

  return (
    <div className="rc-page rc-fairness-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.green, margin:'4px 0 4px', textShadow:`3px 3px 0 ${M.hot}`}}>PROVABLY FAIR</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:18}}>commit-reveal HMAC-SHA-256. rotate the seed to reveal it. verify here, in any hmac tool, or write your own.</div>

      <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:14}}>
        <Frame title="active commitment" accent={M.green} className="rc-fairness-commitment">
          {/* Rows wrapped in rc-table-scroll so long hex strings can scroll
              sideways within the Frame on narrow viewports (matches the
              recent-rolls pattern). minWidth on the inner div triggers the
              overflow when the Frame's content area is narrower. */}
          <div className="rc-table-scroll" style={{overflowX:'auto'}}>
            <div style={{minWidth:540}}>
              <Row label="server seed (hidden until rotate)" value={'•'.repeat(48) + ' [hidden]'} M={M} mono/>
              <Row label="server hash (committed)" value={fairness.serverHash} copy M={M} mono/>
              <Row label="client seed" value={fairness.clientSeed} M={M} mono/>
              <Row label="next nonce" value={fairness.nonce.toString()} M={M} mono/>
            </div>
          </div>
          <button onClick={doRotate} disabled={rotating || !fairness.serverHash} style={{
            marginTop:6, background:M.yellow, color:M.bg, border:`2px solid ${M.bg}`,
            fontFamily:'inherit', fontSize:14, padding:'6px 12px',
            cursor: rotating ? 'wait' : 'pointer', boxShadow:`3px 3px 0 ${M.bg}`,
            fontWeight:700, letterSpacing:'0.05em', opacity: fairness.serverHash ? 1 : 0.5,
          }}>{rotating ? 'rotating…' : '↻ rotate seed (reveals current)'}</button>
          <div style={{marginTop:6, fontSize:12, color:M.ink2, lineHeight:1.4}}>
            {'>'} rotation reveals the current server seed and installs a new one.
            past rounds become verifiable against the revealed seed below.
          </div>
        </Frame>

        <Frame title="verify a roll" accent={M.cyan} className="rc-fairness-verify">
          {/* Input rows wrapped in rc-table-scroll so the wide inputs (64-char
              hex) can scroll sideways within the Frame on narrow viewports
              (matches the recent-rolls pattern). Button stays outside the
              scroll wrapper so it doesn't slide off-screen. */}
          <div className="rc-table-scroll" style={{overflowX:'auto'}}>
            <div style={{minWidth:540}}>
              <Row label="server seed (revealed)" M={M}>
                <input value={verifyInput.server} placeholder="paste rotated server seed" maxLength={128} onChange={e=>{
                  const v = e.target.value.trim().toLowerCase();
                  if (v && !/^[0-9a-f]*$/.test(v)) return;
                  setVerifyInput(x=>({...x, server: v}));
                }} style={{flex:1, background:M.bg, border:`1px solid ${M.cyan}`, color:M.yellow, padding:'4px 8px', fontFamily:'JetBrains Mono, monospace', fontSize:14, outline:'none', minWidth:0}}/>
              </Row>
              <Row label="client seed" M={M}>
                <input value={verifyInput.client} placeholder="0x…" maxLength={128} onChange={e=>{
                  const v = e.target.value.trim().toLowerCase();
                  if (v && !/^[0-9a-f]*$/.test(v)) return;
                  setVerifyInput(x=>({...x, client: v}));
                }} style={{flex:1, background:M.bg, border:`1px solid ${M.cyan}`, color:M.yellow, padding:'4px 8px', fontFamily:'JetBrains Mono, monospace', fontSize:14, outline:'none', minWidth:0}}/>
              </Row>
              <Row label="nonce" M={M}>
                <input type="number" min={0} max={1e9} value={verifyInput.nonce} onChange={e=>{
                  const n = Math.max(0, Math.min(1e9, Math.floor(Number(e.target.value) || 0)));
                  setVerifyInput(x=>({...x, nonce: n}));
                }} style={{flex:1, background:M.bg, border:`1px solid ${M.cyan}`, color:M.yellow, padding:'4px 8px', fontFamily:'JetBrains Mono, monospace', fontSize:14, outline:'none', minWidth:0}}/>
              </Row>
            </div>
          </div>
          <button onClick={doVerify} style={{marginTop:10, background:M.cyan, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:18, padding:'8px 16px', cursor:'pointer', boxShadow:`4px 4px 0 ${M.bg}`}}>► compute hmac-sha256</button>
          {verifyOut?.hex && (
            <div style={{marginTop:10, padding:10, border:`1px dashed ${M.green}`, fontSize:13, fontFamily:'JetBrains Mono, monospace', color:M.green, wordBreak:'break-all'}}>
              <div style={{fontSize:12, color:M.ink2, fontFamily:'inherit', marginBottom:2}}>round hash</div>
              {verifyOut.hex}
              {/* Commitment check — does sha256(server_seed) match a published commitment? */}
              <div style={{marginTop:10, paddingTop:10, borderTop:`1px dashed ${M.green}33`, fontFamily:'inherit', fontSize:14}}>
                {verifyOut.matchedCommitment ? (
                  <span style={{color:M.green, fontWeight:700}}>✓ server seed matches the {verifyOut.matchedCommitment.label} commitment</span>
                ) : (
                  <span style={{color:M.red, fontWeight:700}}>✗ server seed does not match any published commitment</span>
                )}
              </div>
              {/* Round match — does the computed hash match a recorded round in history? */}
              <div style={{marginTop:6, fontFamily:'inherit', fontSize:14}}>
                {verifyOut.matchedRound ? (
                  <span style={{color:M.green, fontWeight:700}}>✓ round verified — matches recorded {verifyOut.matchedRound.game} roll at nonce {verifyOut.matchedRound.nonce}</span>
                ) : (
                  <span style={{color:M.yellow}}>{'>'} no recorded round in this session matches — compare manually to your saved hash</span>
                )}
              </div>
            </div>
          )}
          {verifyOut?.err && <div style={{marginTop:10, color:M.red, fontSize:14}}>{'>'} {verifyOut.err}</div>}
        </Frame>
      </div>

      {/* Revealed seeds — populated by past rotations */}
      {fairness.revealedSeeds && fairness.revealedSeeds.length > 0 && (
        <Frame title="revealed seeds" accent={M.hot} style={{marginTop:18}}>
          <div style={{fontSize:14, color:M.ink2, marginBottom:10, lineHeight:1.5}}>
            {'>'} each previously-active server seed, revealed at rotation. paste these
            into the verifier above to confirm any past round.
          </div>
          {fairness.revealedSeeds.map((rs, i) => (
            <div key={i} style={{marginBottom:10, padding:'8px 10px', border:`1px dashed ${M.hot}55`, background:M.bg, fontFamily:'JetBrains Mono, monospace', fontSize:12, wordBreak:'break-all'}}>
              <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', gap:8, marginBottom:2}}>
                <span style={{color:M.ink2}}>seed (revealed {new Date(rs.revealedAt).toLocaleString()})</span>
                <button onClick={()=>setVerifyInput(x=>({...x, server: rs.seed}))} style={{
                  background:M.cyan, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:11,
                  padding:'2px 8px', cursor:'pointer', boxShadow:`2px 2px 0 ${M.bg}`, flexShrink:0,
                  letterSpacing:'0.05em', fontWeight:700,
                }}>► use in verifier</button>
              </div>
              <div style={{color:M.yellow}}>{rs.seed}</div>
              <div style={{color:M.ink2, marginTop:4}}>commitment: <span style={{color:M.cyan}}>{rs.commitment}</span></div>
              <div style={{color:M.ink2, marginTop:2}}>nonces 0..{rs.lastNonce - 1} (count: {rs.lastNonce})</div>
            </div>
          ))}
        </Frame>
      )}

      {/* Round history. Each row's verify button auto-fills all three
          verifier fields with that round's exact server seed (looked up
          via commitment in revealedSeeds), client seed, and nonce. Rounds
          played under the still-active commitment need a rotate first to
          expose the seed; their button stays disabled with an explainer
          tooltip. */}
      <Frame title="recent rolls" accent={M.yellow} style={{marginTop:18}}>
        {fairness.history.length === 0 && <div style={{color:M.ink2, fontSize:16}}>no rolls yet. play a game.</div>}
        {fairness.history.length > 0 && (
         <div className="rc-table-scroll" style={{overflowX:'auto'}}>
          <table style={{width:'100%', borderCollapse:'collapse', fontSize:14, fontFamily:'JetBrains Mono, monospace', minWidth:560}}>
            <thead><tr style={{color:M.ink2, textAlign:'left'}}>
              <th style={{padding:'4px 8px'}}>game</th>
              <th style={{padding:'4px 8px'}}>nonce</th>
              <th style={{padding:'4px 8px'}}>client</th>
              <th style={{padding:'4px 8px'}}>commitment</th>
              <th style={{padding:'4px 8px'}}>roll hash</th>
              <th style={{padding:'4px 8px'}}>verify</th>
            </tr></thead>
            <tbody>{fairness.history.slice(0,8).map((h,i)=>{
              const revealed = (fairness.revealedSeeds || []).find(rs => rs.commitment === h.serverHash);
              const canVerify = !!revealed;
              return (
                <tr key={i} style={{borderTop:`1px dashed ${M.green}22`}}>
                  <td style={{padding:'4px 8px', color:M.yellow}}>{h.game}</td>
                  <td style={{padding:'4px 8px', color:M.cyan}}>{h.nonce}</td>
                  <td style={{padding:'4px 8px', color:M.ink}}>{h.clientSeed.slice(0,12)}…</td>
                  <td style={{padding:'4px 8px', color:M.ink2}}>{h.serverHash.slice(0,12)}…</td>
                  <td style={{padding:'4px 8px', color:M.green}}>{h.hex.slice(0,16)}…</td>
                  <td style={{padding:'4px 8px'}}>
                    <button
                      onClick={() => {
                        if (!canVerify) return;
                        setVerifyInput({ server: revealed.seed, client: h.clientSeed, nonce: h.nonce });
                      }}
                      disabled={!canVerify}
                      title={canVerify
                        ? 'fill the verifier above with this round’s exact seeds + nonce'
                        : 'rotate the seed first to reveal it before this round can be verified'}
                      style={{
                        background: canVerify ? M.cyan : M.bg,
                        color:      canVerify ? M.bg   : M.ink2,
                        border:`2px solid ${canVerify ? M.cyan : `${M.ink2}55`}`,
                        fontFamily:'inherit', fontSize:11, padding:'3px 8px',
                        cursor: canVerify ? 'pointer' : 'not-allowed', fontWeight:700,
                        letterSpacing:'0.05em', boxShadow: canVerify ? `2px 2px 0 ${M.bg}` : 'none',
                      }}>► verify</button>
                  </td>
                </tr>
              );
            })}</tbody>
          </table>
         </div>
        )}
      </Frame>

      {/* Sign in */}
      <Frame title="sign-in proof (optional)" accent={M.hot} style={{marginTop:18}}>
        <div style={{fontSize:16, color:M.ink, lineHeight:1.5}}>prove you control your wallet by signing a timestamped message. nothing is sent — purely client-side cryptography.</div>
        <div style={{display:'flex', gap:10, marginTop:10}}>
          <button onClick={doSign} disabled={!wallet.connected||signing} style={{
            background: wallet.connected?M.hot:M.ink2, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:18, padding:'8px 16px', cursor: wallet.connected?'pointer':'not-allowed', boxShadow:`4px 4px 0 ${M.bg}`,
          }}>{signing ? 'signing…' : '✎ sign message'}</button>
          {!wallet.connected && <span style={{alignSelf:'center', color:M.ink2, fontSize:14}}>connect a wallet first</span>}
        </div>
        {signOut?.sig && (
          <div style={{marginTop:10, padding:10, border:`1px dashed ${M.hot}`, fontSize:13, fontFamily:'JetBrains Mono, monospace', wordBreak:'break-all', color:M.green}}>
            <div style={{color:M.ink2, fontFamily:'inherit', fontSize:14}}>{signOut.msg}</div>
            <div style={{marginTop:6}}>sig: {signOut.sig}</div>
          </div>
        )}
        {signOut?.err && <div style={{marginTop:10, color:M.red, fontSize:14}}>{'>'} {signOut.err}</div>}
      </Frame>
    </div>
  );
}

function Row({ label, value, copy, mono, children, M }) {
  return (
    <div style={{marginBottom:8}}>
      <div style={{fontSize:13, color:M.ink2, marginBottom:3}}>{label}</div>
      <div style={{display:'flex', gap:6, alignItems:'center', background:M.bg, border:`1px solid ${M.green}33`, padding:'4px 8px'}}>
        {children ? children : (
          <>
            <code style={{flex:1, fontSize: mono?13:14, fontFamily: mono?'JetBrains Mono, monospace':'inherit', color:M.green, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{value}</code>
            {copy && <button onClick={()=>{ try { navigator.clipboard.writeText(value); } catch{} }} style={{background:'transparent', color:M.ink2, border:`1px solid ${M.ink2}`, padding:'2px 8px', fontFamily:'inherit', fontSize:13, cursor:'pointer'}}>copy</button>}
          </>
        )}
      </div>
    </div>
  );
}

// ─── LEADERBOARD ─────────────────────────────────────────────────
function LeaderboardPage({ palette: M, online, wallet }) {
  const [tab, setTab] = useStateP('7d');
  const rows = useMemoP(() => {
    const windows = { '24h': 86400e3, '7d': 7*86400e3, '30d': 30*86400e3 };
    const cutoff = tab === 'all' ? 0 : Date.now() - windows[tab];
    const h = (wallet?.history || []).filter(e => (e.t || 0) >= cutoff);
    // Aggregate per game, converting everything to SOL-equivalent so the
    // "net SOL" column stays honest across multi-coin play.
    const solPrice = COIN_USD.SOL || 1;
    const agg = {};
    for (const e of h) {
      const g = normGameName(e.game);
      const fx = (COIN_USD[e.coin] || 0) / solPrice;
      if (!agg[g]) agg[g] = { plays:0, wagered:0, net:0 };
      agg[g].plays++;
      agg[g].wagered += (e.bet || 0) * fx;
      agg[g].net     += ((e.payout || 0) - (e.bet || 0)) * fx;
    }
    const u = (wallet?.username) || 'you';
    return Object.entries(agg)
      .map(([g, a]) => {
        const roi = a.wagered > 0 ? (a.net / a.wagered) * 100 : 0;
        const sign = roi >= 0 ? '+ ' : '− ';
        return { u, g, w: +a.net.toFixed(3), p: sign + Math.abs(roi).toFixed(0) + '%', plays: a.plays };
      })
      .sort((a, b) => b.w - a.w);
  }, [wallet?.history, wallet?.username, tab]);
  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.yellow, margin:'4px 0 4px', textShadow:`3px 3px 0 ${M.hot}`}}>♔ LEADERBOARD ♔</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:18}}>your games ranked by net SOL · updates live from your play history · top 3 split a 100 SOL pool weekly</div>

      <div style={{display:'flex', gap:6, marginBottom:14}}>
        {['24h','7d','30d','all'].map(t => (
          <button key={t} onClick={()=>setTab(t)} style={{background:tab===t?M.green:'transparent', color:tab===t?M.bg:M.green, border:`2px solid ${M.green}`, fontFamily:'inherit', fontSize:18, padding:'4px 16px', cursor:'pointer'}}>{t}</button>
        ))}
        <div style={{flex:1}}/>
        <span style={{color:M.ink2, fontSize:16, alignSelf:'center'}}>{online.toLocaleString()} online</span>
      </div>

      <Frame title={`top games · ${tab}`} accent={M.green}>
       <div className="rc-table-scroll" style={{overflowX:'auto'}}>
        <table style={{width:'100%', borderCollapse:'collapse', fontSize:18, minWidth:580}}>
          <thead><tr style={{color:M.ink2, textAlign:'left'}}>
            <th style={{padding:'6px 10px', width:60}}>rank</th>
            <th style={{padding:'6px 10px'}}>player</th>
            <th style={{padding:'6px 10px'}}>game</th>
            <th style={{padding:'6px 10px', textAlign:'right'}}>plays</th>
            <th style={{padding:'6px 10px', textAlign:'right'}}>roi</th>
            <th style={{padding:'6px 10px', textAlign:'right'}}>net SOL</th>
          </tr></thead>
          <tbody>{rows.length === 0 ? (
            <tr><td colSpan={6} style={{padding:'18px 10px', color:M.ink2, textAlign:'center'}}>no plays in this window yet — pick a game and run a few rounds.</td></tr>
          ) : rows.map((r,i)=>{
            const positive = r.w >= 0;
            const netColor = positive ? M.green : M.red;
            return (
              <tr key={r.g} style={{borderTop:`1px dashed ${M.green}22`, background: i<3 ? `${i===0?M.yellow:i===1?M.cyan:M.hot}11` : 'transparent'}}>
                <td style={{padding:'8px 10px', color: i===0?M.yellow:i===1?M.cyan:i===2?M.hot:M.ink2, fontWeight:700, fontSize:22}}>#{i+1}</td>
                <td style={{padding:'8px 10px', color: i<3?M.yellow:M.ink}}>{i===0?'👑 ':i===1?'🥈 ':i===2?'🥉 ':''}{r.u}</td>
                <td style={{padding:'8px 10px', color: gameColor(r.g, M), textTransform:'capitalize', fontWeight:700}}>{r.g}</td>
                <td style={{padding:'8px 10px', textAlign:'right', color:M.ink2}}>{r.plays}</td>
                <td style={{padding:'8px 10px', textAlign:'right', color:netColor}}>{r.p}</td>
                <td style={{padding:'8px 10px', textAlign:'right', color:netColor, fontWeight:700, fontFamily:'JetBrains Mono, monospace'}}>{r.w.toFixed(3)} SOL</td>
              </tr>
            );
          })}</tbody>
        </table>
       </div>
      </Frame>

      <Frame title="prize pool" accent={M.hot} style={{marginTop:18}}>
        <div className="rc-3col" style={{display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:14}}>
          {[['1st','50 SOL', M.yellow],['2nd','30 SOL', M.cyan],['3rd','20 SOL', M.hot]].map(([t,p,c],i)=>(
            <div key={t} style={{border:`2px solid ${c}`, padding:'14px 18px', background:M.bg, boxShadow:`4px 4px 0 ${c}`}}>
              <div style={{fontSize:16, color:M.ink2}}>{t}</div>
              <div style={{fontSize:32, color:c, fontWeight:700}}>{p}</div>
            </div>
          ))}
        </div>
        <div style={{marginTop:10, color:M.ink2, fontSize:14}}>distributes monday 00:00 UTC · automatic · on-chain · no claims to fill</div>
      </Frame>
    </div>
  );
}

// ─── PROFILE / WALLET ────────────────────────────────────────────
function ProfilePage({ palette: M, wallet, setPage, fairness, onConnect }) {
  const [editing, setEditing] = useStateP(false);
  const [draft, setDraft] = useStateP(wallet.username);
  useEffectP(() => { setDraft(wallet.username); }, [wallet.username]);

  const total = COINS.reduce((s,c)=>s + wallet.balance[c]*COIN_USD[c], 0);

  // Compute stats from history
  const h = wallet.history;
  const totalPlays = h.length;
  const wins = h.filter(x=>x.win).length;
  const losses = totalPlays - wins;
  const winRate = totalPlays ? (wins/totalPlays*100) : 0;
  const totalWagered = h.reduce((s,x)=>s + (x.bet||0), 0);
  const totalReturned = h.reduce((s,x)=>s + (x.payout||0), 0);
  const netSol = totalReturned - totalWagered; // approximation, all SOL
  const biggestWin = h.reduce((m,x)=>{ const p=(x.payout||0)-(x.bet||0); return p>m?p:m; }, 0);
  const biggestLoss = h.reduce((m,x)=>{ const p=(x.payout||0)-(x.bet||0); return p<m?p:m; }, 0);
  // Longest win streak across all history — only counts consecutive wins;
  // loss streaks are deliberately omitted from the profile tile.
  let longestWinStreak = 0;
  let runningWins = 0;
  for (let i = h.length - 1; i >= 0; i--) {
    if (h[i].win) {
      runningWins++;
      if (runningWins > longestWinStreak) longestWinStreak = runningWins;
    } else {
      runningWins = 0;
    }
  }
  // by-game breakdown
  const byGame = {};
  for (const e of h) {
    const g = e.game || '?';
    if (!byGame[g]) byGame[g] = { plays:0, wins:0, wagered:0, returned:0 };
    byGame[g].plays++;
    if (e.win) byGame[g].wins++;
    byGame[g].wagered += e.bet||0;
    byGame[g].returned += e.payout||0;
  }
  const games = Object.entries(byGame).sort((a,b)=>b[1].plays-a[1].plays);

  function fmtAgo(t) {
    const s = Math.floor((Date.now()-t)/1000);
    if (s<60) return s+'s';
    if (s<3600) return Math.floor(s/60)+'m';
    if (s<86400) return Math.floor(s/3600)+'h';
    return Math.floor(s/86400)+'d';
  }

  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <div style={{display:'flex', alignItems:'baseline', gap:12, marginBottom:10}}>
        <h1 className="rc-h-page" style={{fontSize:48, color:M.green, margin:0, textShadow:`3px 3px 0 ${M.hot}`}}>PROFILE</h1>
        <span style={{color:M.ink2, fontSize:14}}>~/me/</span>
      </div>

      {/* Identity card */}
      <Frame title="player.json" accent={M.yellow} style={{marginBottom:18}}>
        <div style={{display:'flex', alignItems:'center', gap:16, flexWrap:'wrap'}}>
          {/* avatar — initials block */}
          <div style={{
            width:72, height:72, background:M.yellow, color:M.bg, display:'flex', alignItems:'center',
            justifyContent:'center', fontSize:36, fontFamily:'"VT323", monospace', fontWeight:700,
            border:`3px solid ${M.bg}`, boxShadow:`4px 4px 0 ${M.hot}`,
          }}>{(wallet.username[0]||'?').toUpperCase()}{(wallet.username[1]||'').toUpperCase()}</div>

          <div style={{flex:1, minWidth:200}}>
            <div style={{fontSize:13, color:M.ink2, marginBottom:2}}>display name</div>
            {editing ? (
              <div style={{display:'flex', gap:6, alignItems:'center'}}>
                <input
                  value={draft}
                  autoFocus
                  maxLength={18}
                  onChange={e=>setDraft(e.target.value)}
                  onKeyDown={e=>{ if(e.key==='Enter'){ wallet.setUsername(draft); setEditing(false); } if(e.key==='Escape'){ setDraft(wallet.username); setEditing(false); } }}
                  style={{background:M.bg, border:`2px solid ${M.green}`, color:M.green, padding:'6px 10px', fontFamily:'inherit', fontSize:24, outline:'none', width:'100%', maxWidth:240, minWidth:0}}/>
                <button onClick={()=>{ wallet.setUsername(draft); setEditing(false); }} style={{background:M.green, color:M.bg, border:'none', fontFamily:'inherit', fontSize:14, padding:'8px 12px', cursor:'pointer', fontWeight:700}}>save</button>
                <button onClick={()=>{ setDraft(wallet.username); setEditing(false); }} style={{background:'transparent', color:M.ink2, border:`1px solid ${M.ink2}`, fontFamily:'inherit', fontSize:14, padding:'7px 10px', cursor:'pointer'}}>cancel</button>
              </div>
            ) : (
              <div style={{display:'flex', alignItems:'center', gap:10}}>
                <span style={{fontSize:30, color:M.green, fontWeight:700, lineHeight:1}}>{wallet.username}</span>
                <button onClick={()=>setEditing(true)} style={{background:'transparent', color:M.cyan, border:`1px solid ${M.cyan}`, fontFamily:'inherit', fontSize:13, padding:'4px 10px', cursor:'pointer'}}>✎ edit</button>
              </div>
            )}
            <div style={{fontSize:13, color:M.ink2, marginTop:6}}>
              {wallet.connected
                ? <>wallet · <span style={{color:M.ink}}>{wallet.address?.slice(0,6)}…{wallet.address?.slice(-4)}</span> · <span style={{color:M.yellow, textTransform:'capitalize'}}>{wallet.walletId}</span></>
                : <>not connected · <button onClick={onConnect} style={{background:'transparent',color:M.hot,border:'none',fontFamily:'inherit',fontSize:13,cursor:'pointer',textDecoration:'underline'}}>connect wallet</button></>}
            </div>
          </div>

          <div style={{textAlign:'right'}}>
            <div style={{fontSize:13, color:M.ink2}}>net session (SOL)</div>
            <div style={{fontSize:30, fontWeight:700, color: netSol>=0 ? M.green : M.red, lineHeight:1}}>
              {netSol>=0?'+':''}{netSol.toFixed(3)}
            </div>
          </div>
        </div>
      </Frame>

      {/* Stat strip */}
      <div className="rc-stat-6" style={{display:'grid', gridTemplateColumns:'repeat(6, 1fr)', gap:10, marginBottom:18}}>
        <Stat label="bag" value={`${wallet.balance.SOL.toFixed(2)}`} unit="SOL" color={M.yellow} M={M}/>
        <Stat label="plays" value={totalPlays} color={M.cyan} M={M}/>
        <Stat label="wins" value={wins} color={M.green} M={M}/>
        <Stat label="losses" value={losses} color={M.red} M={M}/>
        <Stat label="win rate" value={`${winRate.toFixed(1)}%`} color={winRate>=50?M.green:M.hot} M={M}/>
        <Stat label="longest win streak" value={longestWinStreak ? `${longestWinStreak}` : '—'} color={longestWinStreak ? M.green : M.ink2} M={M}/>
      </div>

      <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:14, marginBottom:18}}>
        <Frame title="hi-scores" accent={M.green}>
          <StatRow label="biggest win"  value={biggestWin>0?`+${biggestWin.toFixed(3)} SOL`:'—'} color={M.green} M={M}/>
          <StatRow label="biggest loss" value={biggestLoss<0?`${biggestLoss.toFixed(3)} SOL`:'—'} color={M.red} M={M}/>
          <StatRow label="total wagered" value={`${totalWagered.toFixed(3)} SOL`} color={M.yellow} M={M}/>
          <StatRow label="total returned" value={`${totalReturned.toFixed(3)} SOL`} color={M.cyan} M={M} last/>
        </Frame>

        <Frame title="games played" accent={M.cyan}>
          {games.length === 0 ? (
            <div style={{color:M.ink2, fontSize:14, padding:'8px 0'}}>{'>'} no games played yet. start flipping.</div>
          ) : games.map(([g, s], i)=>(
            <div key={g} style={{display:'grid', gridTemplateColumns:'1fr 60px 80px 80px', gap:10, alignItems:'center', padding:'5px 0', borderBottom:i<games.length-1?`1px dashed ${M.green}33`:'none', fontSize:14}}>
              <span style={{color: gameColor(g, M), fontWeight:700}}>{g}</span>
              <span style={{color:M.ink2, textAlign:'right'}}>{s.plays}p</span>
              <span style={{color:M.green, textAlign:'right'}}>{s.plays?(s.wins/s.plays*100).toFixed(0):0}%</span>
              <span style={{color: (s.returned-s.wagered)>=0?M.green:M.red, textAlign:'right'}}>
                {(s.returned-s.wagered)>=0?'+':''}{(s.returned-s.wagered).toFixed(2)}
              </span>
            </div>
          ))}
        </Frame>
      </div>

      {/* Play history */}
      <Frame title={`play-history (${h.length})`} accent={M.hot}>
        <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom:8}}>
          <div style={{fontSize:13, color:M.ink2}}>most recent first · stored locally</div>
          {h.length>0 && (
            <button onClick={()=>{ if(confirm('Clear all play history?')) wallet.clearHistory(); }} style={{background:'transparent', color:M.red, border:`1px solid ${M.red}`, fontFamily:'inherit', fontSize:12, padding:'3px 10px', cursor:'pointer'}}>clear</button>
          )}
        </div>
        {h.length === 0 ? (
          <div style={{padding:'30px 0', textAlign:'center', color:M.ink2, fontSize:14}}>
            {'>'} no plays logged yet.<br/>
            <button onClick={()=>setPage('flip')} style={{marginTop:10, background:M.hot, color:M.bg, border:'none', fontFamily:'inherit', fontSize:14, padding:'6px 16px', cursor:'pointer', fontWeight:700}}>► run a flip</button>
          </div>
        ) : (
          <div className="rc-table-scroll" style={{maxHeight:340, overflowY:'auto', overflowX:'auto', fontSize:13, fontFamily:'JetBrains Mono, monospace'}}>
            <div style={{display:'grid', gridTemplateColumns:'50px 80px 1fr 90px 90px 100px', gap:10, padding:'4px 0', borderBottom:`2px solid ${M.green}`, color:M.ink2, fontSize:11, textTransform:'uppercase', letterSpacing:1, position:'sticky', top:0, background:M.panel, minWidth:520}}>
              <span>when</span><span>game</span><span>detail</span><span style={{textAlign:'right'}}>bet</span><span style={{textAlign:'right'}}>payout</span><span style={{textAlign:'right'}}>P/L</span>
            </div>
            {h.slice(0,50).map((e,i)=>{
              const pl = (e.payout||0)-(e.bet||0);
              const detail = e.game==='Flip' ? `pick ${e.pick||'?'} → ${e.side||'?'}`
                : e.game==='Roulette' ? `pocket ${e.pocket}`
                : e.game==='Blackjack' ? (e.outcome||'-')
                : e.game==='Baccarat' ? (e.outcome||'-')
                : e.game==='Slots' ? (e.mult ? `${e.mult}× match` : 'no match')
                : '-';
              return (
                <div key={i} style={{display:'grid', gridTemplateColumns:'50px 80px 1fr 90px 90px 100px', gap:10, padding:'4px 0', borderBottom:`1px dashed ${M.green}22`, alignItems:'center', minWidth:520}}>
                  <span style={{color:M.ink2}}>{fmtAgo(e.t)}</span>
                  <span style={{color: gameColor(e.game, M), fontWeight:700}}>{e.game}</span>
                  <span style={{color:M.ink}}>{detail}</span>
                  <span style={{color:M.ink2, textAlign:'right'}}>{(e.bet||0).toFixed(3)}</span>
                  <span style={{color:e.payout?M.green:M.ink2, textAlign:'right'}}>{(e.payout||0).toFixed(3)}</span>
                  <span style={{color:pl>=0?M.green:M.red, textAlign:'right', fontWeight:700}}>{pl>=0?'+':''}{pl.toFixed(3)}</span>
                </div>
              );
            })}
          </div>
        )}
      </Frame>

      {/* Bag breakdown */}
      <Frame title="bag.json" accent={M.green} style={{marginTop:18}}>
        <div className="rc-3col" style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:12}}>
          {COINS.map(c=>{
            const active = wallet.activeCoin===c;
            return (
              <div key={c} onClick={()=>wallet.setActiveCoin(c)} style={{
                border:`2px solid ${active?M.yellow:M.green}`, padding:'12px 14px', background:M.bg, cursor:'pointer',
                boxShadow: active ? `4px 4px 0 ${M.yellow}` : 'none',
              }}>
                <div style={{display:'flex', alignItems:'center', gap:10}}>
                  <CoinIcon coin={c} size={22}/>
                  <div style={{fontSize:18, fontWeight:700, color:active?M.yellow:M.ink}}>{c}</div>
                  {active && <span style={{marginLeft:'auto', fontSize:11, color:M.bg, background:M.yellow, padding:'1px 6px'}}>USING</span>}
                </div>
                <div style={{fontSize:24, color:M.green, fontWeight:700, marginTop:6}}>{wallet.balance[c].toFixed(c==='SOL'?3:2)}</div>
                <div style={{fontSize:12, color:M.ink2}}>≈ {fmtUSD(wallet.balance[c]*COIN_USD[c])}</div>
              </div>
            );
          })}
        </div>
      </Frame>
    </div>
  );
}

function Stat({ label, value, unit, color, M }) {
  return (
    <div style={{background:M.panel, border:`2px solid ${color}55`, padding:'10px 12px'}}>
      <div style={{fontSize:11, color:M.ink2, textTransform:'uppercase', letterSpacing:1}}>{label}</div>
      <div style={{fontSize:24, color, fontWeight:700, lineHeight:1.1, marginTop:4}}>{value}{unit && <span style={{fontSize:13, color:M.ink2, marginLeft:4}}>{unit}</span>}</div>
    </div>
  );
}
function StatRow({ label, value, color, M, last }) {
  return (
    <div style={{display:'flex', justifyContent:'space-between', alignItems:'center', padding:'6px 0', borderBottom: last?'none':`1px dashed ${M.green}22`, fontSize:14}}>
      <span style={{color:M.ink2}}>{label}</span>
      <span style={{color, fontWeight:700}}>{value}</span>
    </div>
  );
}

// ─── WALLET / BAG ────────────────────────────────────────────────
function WalletPage({ palette: M, wallet, setPage, onConnect }) {
  const [tab, setTab] = useStateP('deposit');
  const [amt, setAmt] = useStateP('');
  const [addr, setAddr] = useStateP('');
  const [swapFrom, setSwapFrom] = useStateP('SOL');
  const [swapTo, setSwapTo] = useStateP('USDC');
  const [swapAmt, setSwapAmt] = useStateP('');
  const [toast, setToast] = useStateP(null);

  const total = COINS.reduce((s,c)=>s + wallet.balance[c]*COIN_USD[c], 0);
  const totalSol = total / COIN_USD.SOL;

  function flash(msg, color) { setToast({msg,color}); setTimeout(()=>setToast(null), 2400); }

  function doDeposit() {
    const n = parseFloat(amt);
    if (!n || n<=0) return flash('enter an amount', M.red);
    wallet.credit(wallet.activeCoin, n);
    flash(`+ ${n} ${wallet.activeCoin} credited (mock)`, M.green);
    setAmt('');
  }
  function doWithdraw() {
    const n = parseFloat(amt);
    if (!n || n<=0) return flash('enter an amount', M.red);
    if (n > wallet.balance[wallet.activeCoin]) return flash('insufficient bag', M.red);
    if (!addr || addr.length<8) return flash('paste a destination address', M.red);
    wallet.debit(wallet.activeCoin, n);
    flash(`− ${n} ${wallet.activeCoin} → ${addr.slice(0,6)}…${addr.slice(-4)} (mock)`, M.yellow);
    setAmt(''); setAddr('');
  }
  function doSwap() {
    const n = parseFloat(swapAmt);
    if (!n || n<=0) return flash('enter an amount', M.red);
    if (swapFrom === swapTo) return flash('pick two different coins', M.red);
    if (n > wallet.balance[swapFrom]) return flash('insufficient bag', M.red);
    const out = (n * COIN_USD[swapFrom]) / COIN_USD[swapTo] * 0.998; // 0.2% fee
    wallet.debit(swapFrom, n);
    wallet.credit(swapTo, out);
    flash(`swapped ${n} ${swapFrom} → ${out.toFixed(swapTo==='SOL'?4:2)} ${swapTo}`, M.cyan);
    setSwapAmt('');
  }

  const swapOut = (() => {
    const n = parseFloat(swapAmt);
    if (!n || swapFrom===swapTo) return 0;
    return (n * COIN_USD[swapFrom]) / COIN_USD[swapTo] * 0.998;
  })();

  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto', position:'relative'}}>
      <div style={{display:'flex', alignItems:'baseline', gap:12, marginBottom:10}}>
        <h1 className="rc-h-page" style={{fontSize:48, color:M.green, margin:0, textShadow:`3px 3px 0 ${M.hot}`}}>BAG / WALLET</h1>
        <span style={{color:M.ink2, fontSize:14}}>~/wallet/</span>
      </div>

      {/* Bag summary */}
      <Frame title="bag.json" accent={M.yellow} style={{marginBottom:18}}>
        <div style={{display:'flex', alignItems:'flex-end', justifyContent:'space-between', flexWrap:'wrap', gap:14, marginBottom:14}}>
          <div>
            <div style={{fontSize:13, color:M.ink2}}>total bag value</div>
            <div style={{fontSize:44, color:M.yellow, fontWeight:700, lineHeight:1, fontFamily:'JetBrains Mono, monospace'}}>{totalSol.toFixed(2)} <span style={{fontSize:24, color:M.ink2}}>SOL</span></div>
            <div style={{fontSize:13, color:M.ink2, marginTop:4}}>≈ {fmtUSD(total)}</div>
          </div>
          <div style={{textAlign:'right'}}>
            <div style={{fontSize:13, color:M.ink2}}>connected</div>
            {wallet.connected ? (
              <>
                <div style={{fontSize:18, color:M.green, fontWeight:700}}>{wallet.address?.slice(0,6)}…{wallet.address?.slice(-4)}</div>
                <div style={{fontSize:12, color:M.ink2, textTransform:'capitalize'}}>{wallet.walletId} · solana mainnet</div>
                <button onClick={wallet.disconnect} style={{marginTop:6, background:'transparent', color:M.red, border:`1px solid ${M.red}`, fontFamily:'inherit', fontSize:12, padding:'3px 10px', cursor:'pointer'}}>disconnect</button>
              </>
            ) : (
              <button onClick={onConnect} style={{background:M.hot, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:16, padding:'6px 14px', cursor:'pointer', boxShadow:`3px 3px 0 ${M.green}`, fontWeight:700}}>connect wallet</button>
            )}
          </div>
        </div>

        <div className="rc-3col" style={{display:'grid', gridTemplateColumns:'repeat(3,1fr)', gap:12}}>
          {COINS.map(c=>{
            const active = wallet.activeCoin===c;
            const sol = (wallet.balance[c]*COIN_USD[c])/COIN_USD.SOL;
            return (
              <div key={c} onClick={()=>wallet.setActiveCoin(c)} style={{
                border:`2px solid ${active?M.yellow:M.green}`, padding:'12px 14px', background:M.bg, cursor:'pointer',
                boxShadow: active ? `4px 4px 0 ${M.yellow}` : 'none',
              }}>
                <div style={{display:'flex', alignItems:'center', gap:10}}>
                  <CoinIcon coin={c} size={22}/>
                  <div style={{fontSize:18, fontWeight:700, color:active?M.yellow:M.ink}}>{c}</div>
                  {active && <span style={{marginLeft:'auto', fontSize:11, color:M.bg, background:M.yellow, padding:'1px 6px'}}>USING</span>}
                </div>
                <div style={{fontSize:24, color:M.green, fontWeight:700, marginTop:6, fontFamily:'JetBrains Mono, monospace'}}>{wallet.balance[c].toFixed(c==='SOL'?3:2)}</div>
                <div style={{fontSize:12, color:M.ink2}}>≈ {sol.toFixed(2)} SOL</div>
              </div>
            );
          })}
        </div>
      </Frame>

      {/* Action tabs */}
      <div className="rc-row-wrap" style={{display:'flex', gap:0, marginBottom:0, borderBottom:`2px solid ${M.green}`}}>
        {[['deposit','◢ DEPOSIT', M.green], ['withdraw','◣ WITHDRAW', M.yellow], ['swap','◆ SWAP', M.cyan]].map(([k,l,c])=>(
          <button key={k} onClick={()=>setTab(k)} style={{
            background: tab===k ? c : 'transparent', color: tab===k ? M.bg : c,
            border:'none', borderBottom: tab===k ? `none` : `2px solid transparent`,
            fontFamily:'inherit', fontSize:18, padding:'10px 22px', cursor:'pointer', fontWeight:700,
            marginBottom:-2,
          }}>{l}</button>
        ))}
      </div>

      <div style={{border:`2px solid ${M.green}`, borderTop:'none', background:M.panel, padding:'20px 22px', marginBottom:18}}>
        {tab === 'deposit' && (
          <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1.2fr 1fr', gap:24}}>
            <div>
              <div style={{fontSize:13, color:M.ink2, marginBottom:4}}>your deposit address ({wallet.activeCoin})</div>
              <div style={{display:'flex', gap:8, alignItems:'center'}}>
                <code style={{flex:1, background:M.bg, border:`1px dashed ${M.green}`, padding:'10px 12px', color:M.green, fontSize:14, fontFamily:'JetBrains Mono, monospace', wordBreak:'break-all'}}>
                  {wallet.address || `7nZbxK${'9aPe2vR4mY3xLqW8tFh6Bs1Vu5Cd'.slice(0,18)}…fAk3`}
                </code>
                <button onClick={()=>{ navigator.clipboard?.writeText(wallet.address || 'mock-address'); flash('copied to clipboard', M.cyan); }} style={{background:M.green, color:M.bg, border:'none', fontFamily:'inherit', fontSize:14, padding:'10px 14px', cursor:'pointer', fontWeight:700}}>copy</button>
              </div>
              <div style={{fontSize:12, color:M.ink2, marginTop:8, lineHeight:1.5}}>
                {'>'} send only <span style={{color:M.yellow}}>{wallet.activeCoin}</span> on the {wallet.activeCoin==='SOL'?'solana':'solana SPL'} network<br/>
                {'>'} 1 confirmation required · usually credited in &lt; 30s<br/>
                {'>'} no min · no fee · house never touches your funds
              </div>

              <div style={{marginTop:18, fontSize:13, color:M.ink2, marginBottom:4}}>or simulate a deposit (prototype)</div>
              <div style={{display:'flex', gap:8}}>
                <input value={amt} onChange={e=>setAmt(e.target.value)} placeholder={`amount in ${wallet.activeCoin}`} style={{flex:1, background:M.bg, border:`2px solid ${M.green}`, color:M.green, padding:'10px 12px', fontFamily:'inherit', fontSize:18, outline:'none'}}/>
                <button onClick={doDeposit} style={{background:M.green, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:16, padding:'10px 22px', cursor:'pointer', boxShadow:`3px 3px 0 ${M.hot}`, fontWeight:700}}>+ ADD</button>
              </div>
            </div>

            <div style={{display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', border:`2px dashed ${M.green}`, padding:20}}>
              {/* fake QR — pixel grid */}
              <div style={{display:'grid', gridTemplateColumns:'repeat(21, 8px)', gap:0, padding:6, background:M.ink, border:`3px solid ${M.green}`}}>
                {Array.from({length:441}).map((_,i)=>{
                  // deterministic pseudo-pattern based on index, with corner finder squares
                  const x = i%21, y = Math.floor(i/21);
                  const inFinder = (x<7&&y<7)||(x>13&&y<7)||(x<7&&y>13);
                  const finderOn = inFinder && ((x===0||x===6||y===0||y===6||(x>=2&&x<=4&&y>=2&&y<=4)) ||
                                                 (x>=14&&(x===14||x===20||(x>=16&&x<=18&&y>=2&&y<=4))) ||
                                                 (y>=14&&(y===14||y===20||(y>=16&&y<=18&&x>=2&&x<=4))));
                  const on = finderOn || (!inFinder && ((i*2654435761>>>16)&1));
                  return <div key={i} style={{width:8, height:8, background: on ? M.bg : 'transparent'}}/>;
                })}
              </div>
              <div style={{marginTop:10, fontSize:12, color:M.ink2}}>scan to deposit · {wallet.activeCoin}</div>
            </div>
          </div>
        )}

        {tab === 'withdraw' && (
          <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1fr 360px', gap:24}}>
            <div>
              <div style={{fontSize:13, color:M.ink2, marginBottom:4}}>destination address</div>
              <input value={addr} onChange={e=>setAddr(e.target.value)} placeholder={`paste ${wallet.activeCoin} address`} style={{width:'100%', boxSizing:'border-box', background:M.bg, border:`2px solid ${M.yellow}`, color:M.yellow, padding:'10px 12px', fontFamily:'JetBrains Mono, monospace', fontSize:14, outline:'none'}}/>

              <div style={{fontSize:13, color:M.ink2, marginTop:14, marginBottom:4}}>amount</div>
              <div style={{display:'flex', gap:8}}>
                <input value={amt} onChange={e=>setAmt(e.target.value)} placeholder={`max ${wallet.balance[wallet.activeCoin].toFixed(wallet.activeCoin==='SOL'?3:2)} ${wallet.activeCoin}`} style={{flex:1, background:M.bg, border:`2px solid ${M.yellow}`, color:M.yellow, padding:'10px 12px', fontFamily:'inherit', fontSize:18, outline:'none'}}/>
                <button onClick={()=>setAmt(wallet.balance[wallet.activeCoin].toString())} style={{background:'transparent', color:M.yellow, border:`2px solid ${M.yellow}`, fontFamily:'inherit', fontSize:13, padding:'8px 12px', cursor:'pointer'}}>MAX</button>
              </div>
              <div style={{display:'flex', gap:6, marginTop:8}}>
                {[0.25, 0.5, 0.75].map(p => (
                  <button key={p} onClick={()=>setAmt((wallet.balance[wallet.activeCoin]*p).toFixed(4))} style={{background:'transparent', color:M.ink2, border:`1px dashed ${M.ink2}`, fontFamily:'inherit', fontSize:12, padding:'4px 10px', cursor:'pointer'}}>{p*100}%</button>
                ))}
              </div>

              <button onClick={doWithdraw} style={{marginTop:18, background:M.yellow, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:18, padding:'10px 22px', cursor:'pointer', boxShadow:`3px 3px 0 ${M.hot}`, fontWeight:700, width:'100%'}}>► withdraw {amt || '0'} {wallet.activeCoin}</button>
            </div>

            <div style={{background:M.bg, border:`2px solid ${M.yellow}`, padding:'16px 18px'}}>
              <div style={{fontSize:13, color:M.ink2, marginBottom:8, textTransform:'uppercase', letterSpacing:1}}>summary</div>
              <SumRow k="amount" v={`${amt||'0'} ${wallet.activeCoin}`} c={M.ink} M={M}/>
              <SumRow k="network fee" v={`~ 0.000005 ${wallet.activeCoin}`} c={M.ink2} M={M}/>
              <SumRow k="house fee" v="0" c={M.green} M={M}/>
              <div style={{borderTop:`1px dashed ${M.yellow}55`, marginTop:8, paddingTop:8}}>
                <SumRow k="you receive" v={`${amt||'0'} ${wallet.activeCoin}`} c={M.yellow} M={M} bold/>
                <SumRow k="≈ in SOL" v={`${(((parseFloat(amt)||0)*COIN_USD[wallet.activeCoin])/COIN_USD.SOL).toFixed(3)} SOL`} c={M.ink2} M={M}/>
              </div>
            </div>
          </div>
        )}

        {tab === 'swap' && (
          <div className="rc-2col" style={{display:'grid', gridTemplateColumns:'1fr 360px', gap:24}}>
            <div>
              <div style={{fontSize:13, color:M.ink2, marginBottom:4}}>from</div>
              <div style={{display:'flex', gap:8}}>
                <select value={swapFrom} onChange={e=>setSwapFrom(e.target.value)} style={{background:M.bg, border:`2px solid ${M.cyan}`, color:M.cyan, padding:'10px 12px', fontFamily:'inherit', fontSize:18, outline:'none', minWidth:90}}>
                  {COINS.map(c=><option key={c} value={c}>{c}</option>)}
                </select>
                <input value={swapAmt} onChange={e=>setSwapAmt(e.target.value)} placeholder={`max ${wallet.balance[swapFrom].toFixed(swapFrom==='SOL'?3:2)}`} style={{flex:1, background:M.bg, border:`2px solid ${M.cyan}`, color:M.cyan, padding:'10px 12px', fontFamily:'inherit', fontSize:18, outline:'none'}}/>
                <button onClick={()=>setSwapAmt(wallet.balance[swapFrom].toString())} style={{background:'transparent', color:M.cyan, border:`2px solid ${M.cyan}`, fontFamily:'inherit', fontSize:13, padding:'8px 12px', cursor:'pointer'}}>MAX</button>
              </div>

              <div style={{textAlign:'center', margin:'12px 0', fontSize:24, color:M.hot}}>
                <button onClick={()=>{ const a=swapFrom; setSwapFrom(swapTo); setSwapTo(a); }} style={{background:'transparent', border:`2px solid ${M.hot}`, color:M.hot, fontFamily:'inherit', fontSize:18, width:40, height:40, cursor:'pointer'}}>⇅</button>
              </div>

              <div style={{fontSize:13, color:M.ink2, marginBottom:4}}>to (estimated)</div>
              <div style={{display:'flex', gap:8}}>
                <select value={swapTo} onChange={e=>setSwapTo(e.target.value)} style={{background:M.bg, border:`2px solid ${M.green}`, color:M.green, padding:'10px 12px', fontFamily:'inherit', fontSize:18, outline:'none', minWidth:90}}>
                  {COINS.map(c=><option key={c} value={c}>{c}</option>)}
                </select>
                <div style={{flex:1, background:M.bg, border:`2px dashed ${M.green}`, color:M.green, padding:'10px 12px', fontSize:18, fontFamily:'JetBrains Mono, monospace'}}>
                  {swapOut ? swapOut.toFixed(swapTo==='SOL'?4:2) : '—'}
                </div>
              </div>

              <button onClick={doSwap} style={{marginTop:18, background:M.cyan, color:M.bg, border:`2px solid ${M.bg}`, fontFamily:'inherit', fontSize:18, padding:'10px 22px', cursor:'pointer', boxShadow:`3px 3px 0 ${M.hot}`, fontWeight:700, width:'100%'}}>◆ confirm swap</button>
            </div>

            <div style={{background:M.bg, border:`2px solid ${M.cyan}`, padding:'16px 18px'}}>
              <div style={{fontSize:13, color:M.ink2, marginBottom:8, textTransform:'uppercase', letterSpacing:1}}>quote</div>
              <SumRow k="rate" v={`1 ${swapFrom} = ${(COIN_USD[swapFrom]/COIN_USD[swapTo]).toFixed(swapTo==='SOL'?6:4)} ${swapTo}`} c={M.ink} M={M}/>
              <SumRow k="slippage" v="0.5%" c={M.ink2} M={M}/>
              <SumRow k="house fee" v="0.2%" c={M.ink2} M={M}/>
              <SumRow k="route" v={`${swapFrom} → ${swapTo}`} c={M.cyan} M={M}/>
              <div style={{borderTop:`1px dashed ${M.cyan}55`, marginTop:8, paddingTop:8}}>
                <SumRow k="you receive" v={`${swapOut ? swapOut.toFixed(swapTo==='SOL'?4:2) : '0'} ${swapTo}`} c={M.cyan} M={M} bold/>
              </div>
              <div style={{marginTop:10, fontSize:11, color:M.ink2, lineHeight:1.5}}>
                {'>'} routed via jupiter aggregator (mock)<br/>
                {'>'} settles in &lt; 5s
              </div>
            </div>
          </div>
        )}
      </div>

      {/* Recent transactions */}
      <Frame title="recent transactions" accent={M.hot}>
        <div className="rc-table-scroll" style={{fontSize:13, fontFamily:'JetBrains Mono, monospace', overflowX:'auto'}}>
          <div style={{display:'grid', gridTemplateColumns:'80px 90px 1fr 110px 80px', gap:10, padding:'4px 0', borderBottom:`2px solid ${M.green}`, color:M.ink2, fontSize:11, textTransform:'uppercase', letterSpacing:1, minWidth:520}}>
            <span>type</span><span>coin</span><span>tx hash</span><span style={{textAlign:'right'}}>amount</span><span style={{textAlign:'right'}}>status</span>
          </div>
          {[
            {type:'deposit', coin:'SOL', hash:'5xK9aPe2vR4mY3xL…fAk3', amt:'+ 5.000', status:'confirmed', c:M.green},
            {type:'play', coin:'SOL', hash:'7tFh6Bs1Vu5Cd9aP…Lq8W', amt:'− 0.250', status:'settled', c:M.ink2},
            {type:'win',  coin:'SOL', hash:'8aPe2vR4mY3xLqW8…Vu5C', amt:'+ 0.495', status:'settled', c:M.green},
            {type:'swap', coin:'SOL→USDC', hash:'9Bs1Vu5Cd9aPe2vR…mY3x', amt:'− 1.000', status:'confirmed', c:M.cyan},
            {type:'withdraw', coin:'USDT', hash:'aPe2vR4mY3xLqW8t…Fh6B', amt:'− 50.00', status:'pending', c:M.yellow},
          ].map((r,i)=>(
            <div key={i} style={{display:'grid', gridTemplateColumns:'80px 90px 1fr 110px 80px', gap:10, padding:'6px 0', borderBottom:`1px dashed ${M.green}22`, alignItems:'center', minWidth:520}}>
              <span style={{color:r.c, textTransform:'uppercase'}}>{r.type}</span>
              <span style={{color:M.yellow}}>{r.coin}</span>
              <span style={{color:M.ink2}}>{r.hash}</span>
              <span style={{color:r.amt.startsWith('+')?M.green:M.ink, textAlign:'right'}}>{r.amt}</span>
              <span style={{color:r.status==='pending'?M.yellow:M.green, textAlign:'right', fontSize:11}}>● {r.status}</span>
            </div>
          ))}
        </div>
      </Frame>

      {toast && (
        <div style={{position:'absolute', bottom:24, right:24, background:M.bg, border:`2px solid ${toast.color}`, padding:'10px 16px', boxShadow:`4px 4px 0 ${toast.color}`, color:toast.color, fontSize:14, fontWeight:700}}>{'>'} {toast.msg}</div>
      )}
    </div>
  );
}

function SumRow({ k, v, c, M, bold }) {
  return (
    <div style={{display:'flex', justifyContent:'space-between', padding:'4px 0', fontSize:14}}>
      <span style={{color:M.ink2}}>{k}</span>
      <span style={{color:c, fontWeight:bold?700:400, fontFamily:'JetBrains Mono, monospace'}}>{v}</span>
    </div>
  );
}

// ─── AdminPage — full-screen overlay (§5.61) ──────────────────────
// Owner-only dashboard rendered as a position:fixed overlay above the
// casino shell. The casino's topbar / sidebar / chat / GameMount tree
// stay mounted underneath, so autospins, blackjack hands, and the
// fairness chain keep running while the owner dips in.
//
// Layout modeled on YouTube Studio: own compact header bar at top,
// left vertical nav with three views (Dashboard / Analytics / Realtime),
// main canvas with chart-forward cards. Pure inline SVG charts — no
// chart libs (we have no bundler).
//
// Self-reported-telemetry caveat is rendered above every view. Closing
// that gap is the Path B server-authoritative migration.

const ADM_VIEWS = [
  { k: 'dashboard', l: 'Dashboard',  g: '⌂' },
  { k: 'analytics', l: 'Analytics',  g: '∿' },
  { k: 'plays',     l: 'Plays',      g: '☷' },
  { k: 'players',   l: 'Players',    g: '☖' },
  { k: 'realtime',  l: 'Realtime',   g: '⚡' },
];

// All 8 games, in the canonical order used across the admin (per-game tables,
// breakdowns). Used to surface zero-play games as empty rows in the per-game
// breakdown — pre-§5.72 the table only showed games that had at least one row.
const ADM_GAMES = ['Flip', 'Slots', 'Roulette', 'Baccarat', 'Blackjack', 'Hilo', 'VideoPoker', 'Mines'];

// Time-range buckets used by AnalyticsView. 24h granularity is hourly,
// 3d / 7d are hourly (72 / 168 buckets), 30d is daily.
const ADM_RANGES = {
  '24h': { label:'Last 24 hours', windowMs: 24 * 3600e3,         bucketCount: 24,  bucketMs: 3600e3,       fmt: (t) => new Date(t).toLocaleTimeString([], {hour:'numeric'}) },
  '3d':  { label:'Last 3 days',   windowMs: 3  * 24 * 3600e3,    bucketCount: 72,  bucketMs: 3600e3,       fmt: (t) => new Date(t).toLocaleDateString([], {month:'numeric', day:'numeric'}) },
  '7d':  { label:'Last 7 days',   windowMs: 7  * 24 * 3600e3,    bucketCount: 168, bucketMs: 3600e3,       fmt: (t) => new Date(t).toLocaleDateString([], {weekday:'short'}) },
  '30d': { label:'Last 30 days',  windowMs: 30 * 24 * 3600e3,    bucketCount: 30,  bucketMs: 24 * 3600e3,  fmt: (t) => new Date(t).toLocaleDateString([], {month:'numeric', day:'numeric'}) },
};

// Leaderboard windows for "biggest wins / losses" — `all` short-circuits the
// time filter. Order matters: rendered as a RadioStrip in this order.
const ADM_WINDOWS = {
  '24h': { label:'24h', windowMs: 24 * 3600e3 },
  '3d':  { label:'3d',  windowMs: 3 * 24 * 3600e3 },
  '7d':  { label:'7d',  windowMs: 7 * 24 * 3600e3 },
  'all': { label:'all', windowMs: Infinity },
};

const ADM_METRICS = {
  plays:    { label:'Plays',         color: (M) => M.green,  fmt: (v) => v.toLocaleString() },
  wagered:  { label:'Wagered',       color: (M) => M.cyan,   fmt: (v) => fmtNum(v) },
  paid:     { label:'Paid out',      color: (M) => M.yellow, fmt: (v) => fmtNum(v) },
  net:      { label:'House net',     color: (M) => M.hot,    fmt: (v) => (v >= 0 ? '+' : '') + fmtNum(v) },
  users:    { label:'Unique users',  color: (M) => M.cyan,   fmt: (v) => v.toLocaleString() },
};

// fmtNum — raw number with thousands separator and up to 3 decimals.
// Caller adds the coin label (or omits it if mixed/dimensionless).
function fmtNum(n) {
  return Number(n || 0).toLocaleString('en-US', { maximumFractionDigits: 3 });
}
// Back-compat alias — older render paths assume the label is " SOL".
function fmtSol(n) { return fmtNum(n); }
// fmtCoin — formats a number with the coin label. USDC/USDT use 2dp; SOL keeps 3dp.
function fmtCoin(n, coin) {
  const dp = (coin === 'SOL' || !coin) ? 3 : 2;
  return Number(n || 0).toLocaleString('en-US', { maximumFractionDigits: dp }) + ' ' + (coin || 'SOL');
}

// aggregateByCoin — per-coin slice of the plays array. Returns
// { SOL: {plays, wagered, paid, net, avgBet}, USDC: {...}, USDT: {...} }.
function aggregateByCoin(plays) {
  const m = { SOL: { plays:0, wagered:0, paid:0 }, USDC: { plays:0, wagered:0, paid:0 }, USDT: { plays:0, wagered:0, paid:0 } };
  for (const p of plays) {
    const c = p.coin || 'SOL';
    const o = m[c] || (m[c] = { plays:0, wagered:0, paid:0 });
    o.plays++;
    o.wagered += Number(p.bet)    || 0;
    o.paid    += Number(p.payout) || 0;
  }
  for (const c of Object.keys(m)) {
    const o = m[c];
    o.net = o.wagered - o.paid;
    o.avgBet = o.plays > 0 ? o.wagered / o.plays : 0;
  }
  return m;
}

// aggregateByUser — per-UID stats with the latest-seen username + favorite game.
// Returns [{uid, user, plays, wagered, paid, net, avgBet, firstSeen, lastSeen,
//           favoriteGame, coins:Set}], sorted by `wagered` desc.
function aggregateByUser(plays) {
  const m = {};
  for (const p of plays) {
    const uid = p.uid || p.user || 'anon';
    const o = m[uid] || (m[uid] = {
      uid, user: p.user || 'anon',
      plays: 0, wagered: 0, paid: 0,
      firstSeen: Infinity, lastSeen: 0,
      games: {}, coins: new Set(),
    });
    // Use the latest username we see for this uid (some clients change names).
    if (p.t && p.t > o.lastSeen) { o.lastSeen = p.t; if (p.user) o.user = p.user; }
    if (p.t && p.t < o.firstSeen) o.firstSeen = p.t;
    o.plays++;
    o.wagered += Number(p.bet)    || 0;
    o.paid    += Number(p.payout) || 0;
    if (p.game) o.games[p.game] = (o.games[p.game] || 0) + 1;
    if (p.coin) o.coins.add(p.coin);
  }
  return Object.values(m).map(o => {
    let favoriteGame = '—', favoriteCount = 0;
    for (const g of Object.keys(o.games)) {
      if (o.games[g] > favoriteCount) { favoriteGame = g; favoriteCount = o.games[g]; }
    }
    return {
      uid: o.uid,
      user: o.user,
      plays: o.plays,
      wagered: o.wagered,
      paid: o.paid,
      net: o.wagered - o.paid,
      avgBet: o.plays > 0 ? o.wagered / o.plays : 0,
      firstSeen: o.firstSeen === Infinity ? 0 : o.firstSeen,
      lastSeen: o.lastSeen,
      favoriteGame,
      coins: Array.from(o.coins),
    };
  }).sort((a, b) => b.wagered - a.wagered);
}

function distinctUidsInWindow(plays, now, windowMs) {
  const cutoff = windowMs === Infinity ? 0 : now - windowMs;
  const s = new Set();
  for (const p of plays) if (p.t && p.t >= cutoff && (p.uid || p.user)) s.add(p.uid || p.user);
  return s.size;
}

function topByProfitInWindow(plays, n, now, windowMs) {
  const cutoff = windowMs === Infinity ? 0 : now - windowMs;
  return plays.filter(p => !p.t || p.t >= cutoff)
    .map(p => ({ ...p, profit: (Number(p.payout) || 0) - (Number(p.bet) || 0) }))
    .sort((a, b) => b.profit - a.profit).slice(0, n);
}
function topByLossInWindow(plays, n, now, windowMs) {
  const cutoff = windowMs === Infinity ? 0 : now - windowMs;
  return plays.filter(p => !p.t || p.t >= cutoff)
    .map(p => ({ ...p, profit: (Number(p.payout) || 0) - (Number(p.bet) || 0) }))
    .sort((a, b) => a.profit - b.profit).slice(0, n);
}

function AdminPage({ palette: M, wallet, isOwner, anonUid, setPage }) {
  const [view, setView] = React.useState('dashboard');

  return (
    <div style={{
      position:'fixed', inset:0, zIndex:90,
      background:M.bg, color:M.ink,
      display:'flex', flexDirection:'column',
      fontFamily:'inherit',
      overflow:'hidden',
    }}>
      <AdminHeaderBar palette={M} wallet={wallet} anonUid={anonUid} isOwner={isOwner} setPage={setPage}/>

      {!isOwner ? (
        <AdminLocked palette={M} wallet={wallet} setPage={setPage}/>
      ) : (
        <div style={{flex:1, display:'flex', minHeight:0}}>
          <AdminSideNav palette={M} view={view} setView={setView}/>
          <main style={{flex:1, overflowY:'auto', overflowX:'hidden', padding:'18px 24px 40px', minWidth:0}}>
            <AdminCaveat palette={M}/>
            {view === 'dashboard' && <DashboardView palette={M} anonUid={anonUid}/>}
            {view === 'analytics' && <AnalyticsView palette={M} anonUid={anonUid}/>}
            {view === 'plays'     && <PlaysView     palette={M} anonUid={anonUid}/>}
            {view === 'players'   && <PlayersView   palette={M} anonUid={anonUid}/>}
            {view === 'realtime'  && <RealtimeView  palette={M} anonUid={anonUid}/>}
          </main>
        </div>
      )}
    </div>
  );
}

// ─── Chrome: header + side nav + caveat ───────────────────────────
function AdminHeaderBar({ palette: M, wallet, anonUid, isOwner, setPage }) {
  return (
    <header style={{
      flex:'0 0 auto', display:'flex', alignItems:'center', gap:14,
      padding:'10px 18px', borderBottom:`2px solid ${M.green}`,
      background:M.bg,
    }}>
      <div style={{display:'flex', alignItems:'center', gap:10}}>
        <span style={{
          fontSize:24, fontWeight:700, color:M.green, lineHeight:1,
          textShadow:`0 0 10px ${M.green}, 2px 2px 0 ${M.hot}`, letterSpacing:'-0.02em',
        }}>RUN!</span>
        <span style={{
          fontSize:13, color:M.ink2, letterSpacing:'0.18em',
          borderLeft:`2px solid ${M.green}55`, paddingLeft:10,
        }}>ADMIN STUDIO</span>
      </div>

      <div style={{flex:1}}/>

      {isOwner && (
        <div style={{fontSize:12, color:M.ink2, lineHeight:1.3, textAlign:'right'}}>
          <div>wallet: <span style={{color:M.green}}>{wallet.address?.slice(0,6)}…{wallet.address?.slice(-4)}</span></div>
          <div>uid: <span style={{color: anonUid ? M.cyan : M.red, userSelect:'text', cursor:'text'}}>{anonUid || 'signing in…'}</span></div>
        </div>
      )}

      <a
        href="https://console.firebase.google.com/project/run-casino-339c4/database/run-casino-339c4-default-rtdb/data"
        target="_blank" rel="noreferrer"
        style={{
          background:M.bg, color:M.cyan, border:`1px solid ${M.cyan}`, padding:'6px 12px',
          fontFamily:'inherit', fontSize:12, cursor:'pointer', textDecoration:'none', fontWeight:700,
          letterSpacing:'0.05em',
        }}
      >↗ FIREBASE</a>

      <button onClick={()=>setPage('home')} style={{
        background:M.green, color:M.bg, border:`2px solid ${M.bg}`,
        padding:'6px 14px', fontFamily:'inherit', fontSize:12, fontWeight:700, cursor:'pointer',
        letterSpacing:'0.05em', boxShadow:`3px 3px 0 ${M.green}55`,
      }}>← BACK TO CASINO</button>
    </header>
  );
}

function AdminSideNav({ palette: M, view, setView }) {
  return (
    <aside style={{
      flex:'0 0 200px', width:200,
      borderRight:`2px solid ${M.green}`, background:M.panel,
      padding:'18px 0', display:'flex', flexDirection:'column',
      overflowY:'auto',
    }}>
      <div style={{padding:'0 16px 6px', color:M.green, fontSize:12, letterSpacing:'0.18em'}}>┤ STUDIO ├</div>
      {ADM_VIEWS.map(v => {
        const active = view === v.k;
        return (
          <button key={v.k} onClick={()=>setView(v.k)} style={{
            display:'flex', alignItems:'center', gap:12, width:'100%',
            padding:'10px 16px', background: active ? M.green : 'transparent',
            color: active ? M.bg : M.ink, border:0, cursor:'pointer',
            fontFamily:'inherit', fontSize:15, textAlign:'left',
            borderLeft: active ? `4px solid ${M.bg}` : '4px solid transparent',
          }}>
            <span style={{
              width:22, height:22, display:'inline-flex', alignItems:'center', justifyContent:'center',
              color: active ? M.bg : M.green, fontSize:18, lineHeight:1,
            }}>{v.g}</span>
            <span style={{textTransform:'lowercase'}}>{v.l}</span>
          </button>
        );
      })}

      <div style={{marginTop:'auto', padding:'12px 16px', borderTop:`2px dashed ${M.green}55`, fontSize:11, color:M.ink2, lineHeight:1.5}}>
        <div style={{color:M.green, letterSpacing:'0.08em'}}>build 0010 · §5.61</div>
        <div>chart engine: inline-svg</div>
      </div>
    </aside>
  );
}

function AdminLocked({ palette: M, setPage, wallet }) {
  return (
    <div style={{flex:1, display:'flex', alignItems:'center', justifyContent:'center', padding:32}}>
      <div style={{maxWidth:520, padding:'24px 28px', border:`3px solid ${M.red}`, background:M.panel, boxShadow:`6px 6px 0 ${M.red}55`}}>
        <div style={{fontSize:20, color:M.red, fontWeight:700, letterSpacing:'0.06em', marginBottom:10}}>⊘ ADMIN · LOCKED</div>
        <div style={{fontSize:14, color:M.ink, lineHeight:1.6}}>
          this page is owner-only.<br/>
          {wallet.connected
            ? <>the connected wallet <span style={{color:M.yellow}}>{wallet.address.slice(0,6)}…{wallet.address.slice(-4)}</span> is not the owner.</>
            : 'connect the owner wallet to unlock.'}
        </div>
        <button onClick={()=>setPage('home')} style={{
          marginTop:18, background:M.bg, color:M.green, border:`2px solid ${M.green}`,
          padding:'8px 18px', fontFamily:'inherit', fontSize:14, cursor:'pointer', fontWeight:700,
          boxShadow:`3px 3px 0 ${M.green}`,
        }}>← back to casino</button>
      </div>
    </div>
  );
}

function AdminCaveat({ palette: M }) {
  return (
    <div style={{
      marginBottom:16, padding:'8px 14px',
      background:`${M.yellow}11`, borderLeft:`4px solid ${M.yellow}`,
      fontSize:12, color:M.ink, lineHeight:1.5,
    }}>
      <span style={{color:M.yellow, fontWeight:700, letterSpacing:'0.05em'}}>⚠ self-reported telemetry</span>{' '}
      <span style={{color:M.ink2}}>·</span>{' '}
      game resolution runs in the browser; modified-JS users could push fake <code style={{color:M.cyan}}>/plays</code> rows.
      treat these numbers as engagement signal, not accounting truth — the fix is the Path B server-authoritative migration.
    </div>
  );
}

// ─── Plays aggregation helpers ────────────────────────────────────
// Both Dashboard and Analytics views run the same per-plays aggregation,
// so we centralize the bucket + per-game math once and let each view
// project what it needs.

function bucketizePlays(plays, range, now) {
  const cfg = ADM_RANGES[range];
  const start = now - cfg.windowMs;
  const buckets = Array.from({ length: cfg.bucketCount }, (_, i) => ({
    t: start + i * cfg.bucketMs,
    plays: 0, wagered: 0, paid: 0, users: new Set(),
  }));
  for (const p of plays) {
    if (!p.t || p.t < start) continue;
    const idx = Math.floor((p.t - start) / cfg.bucketMs);
    if (idx < 0 || idx >= buckets.length) continue;
    const b = buckets[idx];
    b.plays++;
    b.wagered += Number(p.bet)    || 0;
    b.paid    += Number(p.payout) || 0;
    if (p.user) b.users.add(p.user);
  }
  return buckets.map(b => ({ ...b, users: b.users.size, net: b.wagered - b.paid }));
}

// aggregatePerGame — per-game stats. With `includeAll = true` every entry in
// ADM_GAMES is included even if it has zero plays (lets the breakdown table
// show all 8 games consistently regardless of which have data yet).
function aggregatePerGame(plays, includeAll = false) {
  const m = {};
  if (includeAll) {
    for (const g of ADM_GAMES) m[g] = { plays:0, wagered:0, paid:0, users: new Set() };
  }
  for (const p of plays) {
    const g = p.game || '?';
    const o = m[g] || (m[g] = { plays:0, wagered:0, paid:0, users: new Set() });
    o.plays++;
    o.wagered += Number(p.bet)    || 0;
    o.paid    += Number(p.payout) || 0;
    if (p.user) o.users.add(p.user);
  }
  return Object.entries(m).map(([g, o]) => ({
    game:    g,
    plays:   o.plays,
    wagered: o.wagered,
    paid:    o.paid,
    net:     o.wagered - o.paid,
    rtp:     o.wagered > 0 ? (o.paid / o.wagered) * 100 : 0,
    users:   o.users.size,
    ppu:     o.users.size > 0 ? o.plays / o.users.size : 0,
  })).sort((a, b) => b.wagered - a.wagered);
}

// distinctUsersInWindow — legacy helper (username-keyed). Kept for back-compat
// with the Dashboard's per-window active-users tile, which renders username
// counts. Per-UID counts go through distinctUidsInWindow instead.
function distinctUsersInWindow(plays, now, windowMs) {
  const cutoff = windowMs === Infinity ? 0 : now - windowMs;
  const s = new Set();
  for (const p of plays) if (p.t && p.t >= cutoff && (p.uid || p.user)) s.add(p.uid || p.user);
  return s.size;
}

// ─── SVG chart primitives ─────────────────────────────────────────
// All charts render into a parent-measured width via ResizeObserver, so
// the line+area chart scales fluidly with the canvas. Falls back to a
// sensible default width (800) before mount.

function useElementWidth(initial = 800) {
  const ref = React.useRef(null);
  const [w, setW] = React.useState(initial);
  React.useEffect(() => {
    if (!ref.current || typeof ResizeObserver === 'undefined') return;
    const ro = new ResizeObserver(entries => {
      const e = entries[0];
      if (e && e.contentRect.width > 0) setW(Math.floor(e.contentRect.width));
    });
    ro.observe(ref.current);
    return () => ro.disconnect();
  }, []);
  return [ref, w];
}

function scaleLinear(d0, d1, r0, r1) {
  if (d1 === d0) return () => (r0 + r1) / 2;
  const k = (r1 - r0) / (d1 - d0);
  return v => r0 + (v - d0) * k;
}

// LineChart — time-series with optional area fill. `data` is [{x, y}],
// x is a timestamp (ms), y is the metric value. yFmt + xFmt are tick
// label formatters. `color` is the line color (area fill is color@33).
function LineChart({ data, height = 260, color, fill = true, palette: M, yFmt = String, xFmt = (t) => new Date(t).toLocaleTimeString([], {hour:'numeric'}), xTicks = 6, yTicks = 4 }) {
  const [wrapRef, width] = useElementWidth();

  if (!data || data.length === 0) {
    return (
      <div ref={wrapRef} style={{height, display:'flex', alignItems:'center', justifyContent:'center', color:M.ink2, fontStyle:'italic', fontSize:13}}>
        — no data in window —
      </div>
    );
  }

  const margins = { left: 56, right: 12, top: 10, bottom: 32 };
  const w = Math.max(160, width - margins.left - margins.right);
  const h = Math.max(60, height - margins.top - margins.bottom);

  const xs = data.map(d => d.x);
  const ys = data.map(d => d.y);
  const xMin = Math.min(...xs);
  const xMax = Math.max(...xs);
  const yMinRaw = Math.min(0, ...ys);
  const yMaxRaw = Math.max(1, ...ys);
  // Round y-domain to nice values for readability.
  const yPad = (yMaxRaw - yMinRaw) * 0.08;
  const yMin = yMinRaw < 0 ? yMinRaw - yPad : 0;
  const yMax = yMaxRaw + yPad;

  const sx = scaleLinear(xMin, xMax, margins.left, margins.left + w);
  const sy = scaleLinear(yMin, yMax, margins.top + h, margins.top);

  const pathD = data.map((d, i) => `${i === 0 ? 'M' : 'L'} ${sx(d.x).toFixed(1)} ${sy(d.y).toFixed(1)}`).join(' ');
  const baseY = sy(yMin < 0 ? 0 : yMin).toFixed(1);
  const areaD = pathD + ` L ${sx(xMax).toFixed(1)} ${baseY} L ${sx(xMin).toFixed(1)} ${baseY} Z`;

  const yTickVals = Array.from({ length: yTicks + 1 }, (_, i) => yMin + (yMax - yMin) * i / yTicks);
  const xStep = Math.max(1, Math.floor(data.length / xTicks));
  const xTickIdxs = [];
  for (let i = 0; i < data.length; i += xStep) xTickIdxs.push(i);
  if (xTickIdxs[xTickIdxs.length - 1] !== data.length - 1) xTickIdxs.push(data.length - 1);

  return (
    <div ref={wrapRef} style={{width:'100%'}}>
      <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`} style={{display:'block'}}>
        {/* Horizontal grid lines */}
        {yTickVals.map((v, i) => (
          <line key={i} x1={margins.left} y1={sy(v)} x2={margins.left + w} y2={sy(v)} stroke={`${M.ink2}33`} strokeDasharray="2,3"/>
        ))}
        {/* Zero line, if domain crosses zero */}
        {yMin < 0 && yMax > 0 && (
          <line x1={margins.left} y1={sy(0)} x2={margins.left + w} y2={sy(0)} stroke={`${M.ink2}88`}/>
        )}
        {/* Y tick labels */}
        {yTickVals.map((v, i) => (
          <text key={i} x={margins.left - 8} y={sy(v) + 4} fontSize="11" fill={M.ink2} textAnchor="end" fontFamily="JetBrains Mono, monospace">{yFmt(v)}</text>
        ))}
        {/* X tick labels */}
        {xTickIdxs.map((idx, i) => (
          <text key={i} x={sx(data[idx].x)} y={margins.top + h + 18} fontSize="10" fill={M.ink2} textAnchor="middle" fontFamily="JetBrains Mono, monospace">{xFmt(data[idx].x)}</text>
        ))}
        {/* Area fill */}
        {fill && <path d={areaD} fill={`${color}33`}/>}
        {/* Line */}
        <path d={pathD} fill="none" stroke={color} strokeWidth="2" strokeLinejoin="round" strokeLinecap="round"/>
        {/* Endpoint dot — current value */}
        <circle cx={sx(data[data.length - 1].x)} cy={sy(data[data.length - 1].y)} r="3.5" fill={color} stroke={M.bg} strokeWidth="1.5"/>
      </svg>
    </div>
  );
}

// BarChart — horizontal bar per category, sorted by value desc. `data`
// is [{ label, value, color? }]. Labels render on the left, value on
// the right of each bar. Bar widths are scaled to the max in the set.
function BarChart({ data, palette: M, height = 220, valueFmt = (v) => String(v), accentFor }) {
  const [wrapRef, width] = useElementWidth();
  if (!data || data.length === 0) {
    return <div style={{color:M.ink2, fontStyle:'italic', fontSize:13, padding:'12px 0'}}>— no data —</div>;
  }

  const labelCol = 90;
  const valueCol = 80;
  const gap = 8;
  const barH = Math.max(14, Math.floor((height - gap * (data.length - 1)) / data.length));
  const innerW = Math.max(60, width - labelCol - valueCol - 12);
  const maxV = Math.max(1, ...data.map(d => Math.abs(d.value)));

  return (
    <div ref={wrapRef} style={{width:'100%'}}>
      <svg width={width} height={data.length * (barH + gap)} style={{display:'block'}}>
        {data.map((d, i) => {
          const y = i * (barH + gap);
          const w = (Math.abs(d.value) / maxV) * innerW;
          const c = d.color || (accentFor ? accentFor(d) : M.green);
          return (
            <g key={d.label} transform={`translate(0 ${y})`}>
              <text x={labelCol - 8} y={barH * 0.68} fontSize="12" fill={M.ink} textAnchor="end" fontFamily="JetBrains Mono, monospace">{d.label}</text>
              <rect x={labelCol} y={0} width={innerW} height={barH} fill={`${M.ink2}22`}/>
              <rect x={labelCol} y={0} width={w} height={barH} fill={c}/>
              <text x={labelCol + w + 6} y={barH * 0.68} fontSize="12" fill={c} fontFamily="JetBrains Mono, monospace">{valueFmt(d.value)}</text>
            </g>
          );
        })}
      </svg>
    </div>
  );
}

// MiniSparkline — small inline chart for KPI cards. Just the line, no
// axes or labels. Renders into a fixed CSS-aspect container.
function MiniSparkline({ values, color, palette: M, height = 36 }) {
  const [wrapRef, width] = useElementWidth(160);
  if (!values || values.length === 0) {
    return <div ref={wrapRef} style={{height, width:'100%'}}/>;
  }
  const w = Math.max(40, width);
  const h = height;
  const maxV = Math.max(1, ...values);
  const minV = Math.min(0, ...values);
  const sx = scaleLinear(0, values.length - 1, 0, w);
  const sy = scaleLinear(minV, maxV, h - 2, 2);
  const pathD = values.map((v, i) => `${i === 0 ? 'M' : 'L'} ${sx(i).toFixed(1)} ${sy(v).toFixed(1)}`).join(' ');
  const areaD = pathD + ` L ${sx(values.length - 1).toFixed(1)} ${h} L ${sx(0).toFixed(1)} ${h} Z`;
  return (
    <div ref={wrapRef} style={{width:'100%', height}}>
      <svg width={w} height={h} style={{display:'block'}}>
        <path d={areaD} fill={`${color}33`}/>
        <path d={pathD} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round"/>
      </svg>
    </div>
  );
}

// ─── Reusable cards ───────────────────────────────────────────────
// AdminCard — flat panel with thin border + title strip. Used across
// all admin views. Distinct from Frame (window-chrome) — admin gets
// the cleaner flat look that data-dense layouts read better in.
function AdminCard({ title, accent, children, right, padding = '14px 16px' }) {
  const M = window.MEMEColors;
  const c = accent || M.green;
  return (
    <div style={{ border:`2px solid ${c}`, background:M.panel, display:'flex', flexDirection:'column', minHeight:0 }}>
      <div style={{
        display:'flex', alignItems:'center', justifyContent:'space-between', gap:8,
        padding:'8px 14px', borderBottom:`1px dashed ${c}55`,
      }}>
        <span style={{fontSize:11, color:c, letterSpacing:'0.12em', fontWeight:700, textTransform:'uppercase'}}>{title}</span>
        {right}
      </div>
      <div style={{padding, flex:1, minHeight:0}}>{children}</div>
    </div>
  );
}

// KpiCard — big-number tile with optional sparkline + delta indicator.
function KpiCard({ label, value, color, palette: M, sparkValues, deltaPct }) {
  return (
    <div style={{
      padding:'14px 16px', border:`2px solid ${color}`, background:M.panel,
      display:'flex', flexDirection:'column', gap:6, minHeight:0,
    }}>
      <div style={{fontSize:10, color:M.ink2, letterSpacing:'0.12em', textTransform:'uppercase', fontWeight:700}}>{label}</div>
      <div className="display" style={{fontSize:32, color, fontFamily:'VT323, monospace', lineHeight:1, wordBreak:'break-word'}}>{value}</div>
      {deltaPct !== undefined && (
        <div style={{fontSize:11, color: deltaPct >= 0 ? M.green : M.red, fontFamily:'JetBrains Mono, monospace'}}>
          {deltaPct >= 0 ? '▲ +' : '▼ '}{deltaPct.toFixed(1)}% vs. prior window
        </div>
      )}
      {sparkValues && sparkValues.length > 0 && (
        <MiniSparkline values={sparkValues} color={color} palette={M}/>
      )}
    </div>
  );
}

// ─── Dashboard view — overview cards, glanceable ──────────────────
function DashboardView({ palette: M, anonUid }) {
  const { items: plays, err } = useFirebaseList('plays', { orderBy:'t', limit:5000, enabled: !!anonUid });
  const { items: presence }   = useFirebaseList('presence', { orderBy:'t', limit:1000, enabled: !!anonUid });

  if (err) return <DataError palette={M} message={err}/>;
  if (plays.length === 0) return <NoPlaysYet palette={M}/>;

  const now = Date.now();
  const buckets24 = bucketizePlays(plays, '24h', now);
  const buckets48Prior = bucketizePlays(plays, '24h', now - 24 * 3600e3);

  const sum = (arr, k) => arr.reduce((s, b) => s + b[k], 0);
  const sum24      = { plays: sum(buckets24, 'plays'),      wagered: sum(buckets24, 'wagered'),      paid: sum(buckets24, 'paid') };
  const sum24prior = { plays: sum(buckets48Prior, 'plays'), wagered: sum(buckets48Prior, 'wagered'), paid: sum(buckets48Prior, 'paid') };
  const houseNet24 = sum24.wagered - sum24.paid;
  const houseNet24prior = sum24prior.wagered - sum24prior.paid;
  const rtp24 = sum24.wagered > 0 ? (sum24.paid / sum24.wagered) * 100 : 0;
  const avgBet24 = sum24.plays > 0 ? sum24.wagered / sum24.plays : 0;

  // All-time totals (KPIs that survive past the 24h window).
  const totalWagered = plays.reduce((s, p) => s + (Number(p.bet) || 0), 0);
  const totalPaid    = plays.reduce((s, p) => s + (Number(p.payout) || 0), 0);
  const totalNet     = totalWagered - totalPaid;
  const signedWalletsAll = new Set(plays.map(p => p.uid || p.user).filter(Boolean)).size;

  const pctDelta = (cur, prior) => prior === 0 ? (cur > 0 ? 100 : 0) : ((cur - prior) / Math.abs(prior)) * 100;

  // Per-game (all-time) for the top-games bar chart — all 8 even if empty.
  const perGame = aggregatePerGame(plays, true).slice(0, 8);

  // Per-coin breakdown (all-time).
  const perCoin = aggregateByCoin(plays);

  // Recent activity (last 10 plays, newest first — list is t-ascending)
  const recent = [...plays].slice(-10).reverse();

  return (
    <div style={{display:'flex', flexDirection:'column', gap:16}}>
      <ViewHeader palette={M} title="Channel analytics" subtitle="last 24 hours, vs. prior 24h · all-time totals at right"/>

      <div style={{display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:12}}>
        <KpiCard palette={M} label="Plays · 24h"     value={sum24.plays.toLocaleString()}       color={M.green}  sparkValues={buckets24.map(b => b.plays)}   deltaPct={pctDelta(sum24.plays, sum24prior.plays)}/>
        <KpiCard palette={M} label="Wagered · 24h"   value={fmtNum(sum24.wagered)}              color={M.cyan}   sparkValues={buckets24.map(b => b.wagered)} deltaPct={pctDelta(sum24.wagered, sum24prior.wagered)}/>
        <KpiCard palette={M} label="Paid out · 24h"  value={fmtNum(sum24.paid)}                 color={M.yellow} sparkValues={buckets24.map(b => b.paid)}    deltaPct={pctDelta(sum24.paid, sum24prior.paid)}/>
        <KpiCard palette={M} label="House net · 24h" value={(houseNet24 >= 0 ? '+' : '') + fmtNum(houseNet24)} color={houseNet24 >= 0 ? M.green : M.red} sparkValues={buckets24.map(b => b.net)} deltaPct={pctDelta(houseNet24, houseNet24prior)}/>
      </div>

      <div style={{display:'grid', gridTemplateColumns:'repeat(4, 1fr)', gap:12}}>
        <KpiCard palette={M} label="House P/L · all-time" value={(totalNet >= 0 ? '+' : '') + fmtNum(totalNet)} color={totalNet >= 0 ? M.green : M.red}/>
        <KpiCard palette={M} label="Signed wallets"       value={signedWalletsAll.toLocaleString()}              color={M.cyan}/>
        <KpiCard palette={M} label="Avg bet · 24h"        value={fmtNum(avgBet24)}                                color={M.cyan}/>
        <KpiCard palette={M} label="Online now"           value={String(presence.length)}                         color={M.cyan}/>
      </div>

      <AdminCard title="Per-coin breakdown · all-time" accent={M.yellow}>
        <div style={{display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:14}}>
          {['SOL','USDC','USDT'].map(coin => {
            const c = perCoin[coin] || { plays:0, wagered:0, paid:0, net:0, avgBet:0 };
            const netColor = c.net >= 0 ? M.green : M.red;
            return (
              <div key={coin} style={{padding:'10px 12px', border:`1px solid ${M.green}33`, background:`${M.green}07`}}>
                <div style={{display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:6}}>
                  <span style={{fontSize:18, color:M.green, fontWeight:700, letterSpacing:'0.05em'}}>{coin}</span>
                  <span style={{fontSize:11, color:M.ink2}}>{c.plays.toLocaleString()} rounds</span>
                </div>
                <div style={{display:'grid', gridTemplateColumns:'1fr auto', rowGap:3, fontSize:12, fontFamily:'JetBrains Mono, monospace'}}>
                  <span style={{color:M.ink2}}>wagered</span>
                  <span style={{color:M.cyan, textAlign:'right'}}>{fmtCoin(c.wagered, coin)}</span>
                  <span style={{color:M.ink2}}>paid out</span>
                  <span style={{color:M.yellow, textAlign:'right'}}>{fmtCoin(c.paid, coin)}</span>
                  <span style={{color:M.ink2}}>house net</span>
                  <span style={{color:netColor, textAlign:'right', fontWeight:700}}>{c.net >= 0 ? '+' : ''}{fmtCoin(c.net, coin)}</span>
                  <span style={{color:M.ink2}}>avg bet</span>
                  <span style={{color:M.ink, textAlign:'right'}}>{fmtCoin(c.avgBet, coin)}</span>
                </div>
              </div>
            );
          })}
        </div>
      </AdminCard>

      <div style={{display:'grid', gridTemplateColumns:'1.6fr 1fr', gap:16}}>
        <AdminCard title="Plays · last 24h · 1h buckets" accent={M.green}>
          <LineChart
            data={buckets24.map(b => ({ x: b.t, y: b.plays }))}
            color={M.green} palette={M} height={240}
            yFmt={v => String(Math.round(v))}
            xFmt={ADM_RANGES['24h'].fmt}
          />
        </AdminCard>

        <AdminCard title={`Top games · all-time (top ${perGame.length})`} accent={M.cyan}>
          <BarChart
            palette={M} height={Math.max(140, perGame.length * 30)}
            data={perGame.map(g => ({ label: normGameName(g.game), value: g.wagered }))}
            valueFmt={fmtSol}
            accentFor={d => gameColor(d.label, M)}
          />
          <div style={{marginTop:8, fontSize:11, color:M.ink2}}>x-axis: wagered SOL · sorted by wagered</div>
        </AdminCard>
      </div>

      <div style={{display:'grid', gridTemplateColumns:'1fr 1fr 1fr', gap:16}}>
        <AdminCard title="Realized RTP · 24h" accent={M.yellow}>
          <div className="display" style={{fontSize:42, color: rtp24 > 100 ? M.red : M.green, fontFamily:'VT323, monospace', lineHeight:1}}>{rtp24.toFixed(2)}%</div>
          <div style={{fontSize:11, color:M.ink2, marginTop:4}}>house edge: <span style={{color: rtp24 <= 100 ? M.green : M.red}}>{(100 - rtp24).toFixed(2)}%</span></div>
          <div style={{marginTop:10, fontSize:11, color:M.ink2}}>over {sum24.plays.toLocaleString()} plays · {fmtSol(sum24.wagered)} SOL wagered</div>
        </AdminCard>

        <AdminCard title="Active users" accent={M.cyan}>
          {[
            { l: '24h', v: distinctUsersInWindow(plays, now, 24 * 3600e3) },
            { l: '3d',  v: distinctUsersInWindow(plays, now, 3 * 24 * 3600e3) },
            { l: '7d',  v: distinctUsersInWindow(plays, now, 7 * 24 * 3600e3) },
            { l: 'all', v: distinctUidsInWindow(plays, now, Infinity) },
          ].map((row, i) => (
            <div key={row.l} style={{display:'flex', alignItems:'baseline', justifyContent:'space-between', padding:'3px 0', borderBottom: i < 3 ? `1px dashed ${M.ink2}33` : 0}}>
              <span style={{fontSize:12, color:M.ink2, letterSpacing:'0.05em', textTransform:'uppercase'}}>{row.l}</span>
              <span className="display" style={{fontSize:26, color:M.cyan, fontFamily:'VT323, monospace', lineHeight:1}}>{row.v}</span>
            </div>
          ))}
        </AdminCard>

        <AdminCard title="Recent activity" accent={M.green}>
          {recent.length === 0 ? (
            <div style={{color:M.ink2, fontStyle:'italic', fontSize:13}}>— no plays —</div>
          ) : (
            <div style={{maxHeight:200, overflowY:'auto', fontSize:12, fontFamily:'JetBrains Mono, monospace'}}>
              {recent.map(p => {
                const profit = (Number(p.payout) || 0) - (Number(p.bet) || 0);
                return (
                  <div key={p.id} style={{display:'grid', gridTemplateColumns:'auto 1fr auto', columnGap:8, padding:'3px 0', borderBottom:`1px dashed ${M.ink2}22`}}>
                    <span style={{color:M.ink2, fontSize:10}}>{p.t ? new Date(p.t).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'}) : '—'}</span>
                    <span style={{color: gameColor(p.game, M), overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{normGameName(p.game)} · {p.user || 'anon'}</span>
                    <span style={{color: profit >= 0 ? M.green : M.red, textAlign:'right'}}>{profit >= 0 ? '+' : ''}{profit.toFixed(3)}</span>
                  </div>
                );
              })}
            </div>
          )}
        </AdminCard>
      </div>
    </div>
  );
}

// ─── Analytics view — deep charts ─────────────────────────────────
function AnalyticsView({ palette: M, anonUid }) {
  const { items: rawPlays, err } = useFirebaseList('plays', { orderBy:'t', limit:5000, enabled: !!anonUid });
  const [range, setRange]   = React.useState('24h');
  const [metric, setMetric] = React.useState('plays');
  const [coin, setCoin]     = React.useState('all'); // all | SOL | USDC | USDT
  const [winWindow, setWinWindow] = React.useState('all');

  if (err) return <DataError palette={M} message={err}/>;
  if (rawPlays.length === 0) return <NoPlaysYet palette={M}/>;

  // Coin filter applies to every downstream aggregation. `all` is no-op.
  const plays = coin === 'all' ? rawPlays : rawPlays.filter(p => (p.coin || 'SOL') === coin);

  const now = Date.now();
  const buckets = bucketizePlays(plays, range, now);
  const M_DEF   = ADM_METRICS[metric];
  const lineColor = M_DEF.color(M);

  const chartData = buckets.map(b => ({ x: b.t, y: b[metric] }));

  // Cumulative house net within the window — running sum across buckets.
  let acc = 0;
  const cumNet = buckets.map(b => { acc += b.net; return { x: b.t, y: acc }; });

  const perGame = aggregatePerGame(plays, true);

  return (
    <div style={{display:'flex', flexDirection:'column', gap:16}}>
      <ViewHeader palette={M} title="Analytics" subtitle={`${ADM_RANGES[range].label} · ${M_DEF.label.toLowerCase()} · ${coin === 'all' ? 'all coins' : coin}`}/>

      <AdminCard
        title={`${M_DEF.label} over time`}
        accent={lineColor}
        right={
          <div style={{display:'flex', gap:14, alignItems:'center', flexWrap:'wrap'}}>
            <RadioStrip palette={M} options={[{k:'all', l:'all'}, {k:'SOL', l:'SOL'}, {k:'USDC', l:'USDC'}, {k:'USDT', l:'USDT'}]} value={coin} onChange={setCoin}/>
            <RadioStrip palette={M} options={Object.keys(ADM_RANGES).map(k => ({ k, l: k }))} value={range} onChange={setRange}/>
            <RadioStrip palette={M} options={Object.keys(ADM_METRICS).map(k => ({ k, l: ADM_METRICS[k].label }))} value={metric} onChange={setMetric}/>
          </div>
        }
      >
        <LineChart
          data={chartData}
          color={lineColor} palette={M} height={280}
          yFmt={M_DEF.fmt}
          xFmt={ADM_RANGES[range].fmt}
          xTicks={range === '7d' ? 7 : range === '3d' ? 6 : 6}
        />
        <div style={{display:'flex', gap:16, marginTop:8, flexWrap:'wrap', fontSize:11, color:M.ink2, fontFamily:'JetBrains Mono, monospace'}}>
          <span>peak: <span style={{color: lineColor}}>{M_DEF.fmt(Math.max(0, ...chartData.map(d => d.y)))}</span></span>
          <span>total: <span style={{color: lineColor}}>{M_DEF.fmt(chartData.reduce((s, d) => s + d.y, 0))}</span></span>
          <span>buckets: <span style={{color: M.ink}}>{buckets.length}</span></span>
          <span>granularity: <span style={{color: M.ink}}>{ADM_RANGES[range].bucketMs >= 24 * 3600e3 ? 'daily' : 'hourly'}</span></span>
        </div>
      </AdminCard>

      <div style={{display:'grid', gridTemplateColumns:'1.4fr 1fr', gap:16}}>
        <AdminCard title="Cumulative house net" accent={M.hot}>
          <LineChart
            data={cumNet}
            color={M.hot} palette={M} height={220}
            yFmt={v => (v >= 0 ? '+' : '') + fmtNum(v)}
            xFmt={ADM_RANGES[range].fmt}
            xTicks={range === '7d' ? 7 : 6}
          />
          <div style={{marginTop:8, fontSize:11, color:M.ink2}}>positive = house ahead within the window · zero crossing is breakeven</div>
        </AdminCard>

        <AdminCard title="Per-game net" accent={M.green}>
          <BarChart
            palette={M} height={Math.max(140, perGame.length * 30)}
            data={perGame.map(g => ({ label: normGameName(g.game), value: g.net }))}
            valueFmt={v => (v >= 0 ? '+' : '') + fmtNum(v)}
            accentFor={d => {
              const r = perGame.find(g => normGameName(g.game) === d.label);
              return r && r.net < 0 ? M.red : gameColor(d.label, M);
            }}
          />
          <div style={{marginTop:8, fontSize:11, color:M.ink2}}>negative bars (red) = players ahead on that game · all 8 games · zeros are pre-data</div>
        </AdminCard>
      </div>

      <AdminCard title={`Per-game breakdown · all-time${coin === 'all' ? '' : ` · ${coin} only`}`} accent={M.green}>
        <div style={{display:'grid', gridTemplateColumns:'1.2fr 0.7fr 1fr 1fr 0.9fr 1fr 0.7fr 0.7fr', columnGap:10, rowGap:6, fontSize:13, fontFamily:'JetBrains Mono, monospace', alignItems:'center'}}>
          <span style={{color:M.ink2, fontSize:11}}>game</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>plays</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>wagered</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>paid</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>RTP%</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>net</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>users</span>
          <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>p/u</span>
          {perGame.map(r => (
            <React.Fragment key={r.game}>
              <span style={{color: gameColor(r.game, M)}}>{normGameName(r.game)}</span>
              <span style={{color:M.ink, textAlign:'right'}}>{r.plays}</span>
              <span style={{color:M.cyan, textAlign:'right'}}>{fmtNum(r.wagered)}</span>
              <span style={{color:M.yellow, textAlign:'right'}}>{fmtNum(r.paid)}</span>
              <span style={{color: r.plays === 0 ? M.ink2 : (r.rtp > 100 ? M.red : M.green), textAlign:'right'}}>{r.plays === 0 ? '—' : r.rtp.toFixed(1)}</span>
              <span style={{color: r.plays === 0 ? M.ink2 : (r.net >= 0 ? M.green : M.red), textAlign:'right'}}>{r.plays === 0 ? '—' : (r.net >= 0 ? '+' : '') + fmtNum(r.net)}</span>
              <span style={{color:M.ink, textAlign:'right'}}>{r.users}</span>
              <span style={{color:M.ink2, textAlign:'right'}}>{r.plays === 0 ? '—' : r.ppu.toFixed(1)}</span>
            </React.Fragment>
          ))}
        </div>
      </AdminCard>

      <AdminCard
        title="Biggest wins / losses"
        accent={M.green}
        right={<RadioStrip palette={M} options={Object.keys(ADM_WINDOWS).map(k => ({ k, l: ADM_WINDOWS[k].label }))} value={winWindow} onChange={setWinWindow}/>}
      >
        <div style={{display:'grid', gridTemplateColumns:'1fr 1fr', gap:16}}>
          <div>
            <div style={{fontSize:12, color:M.green, fontWeight:700, letterSpacing:'0.06em', marginBottom:6}}>BIGGEST WINS · {ADM_WINDOWS[winWindow].label}</div>
            <LeaderBoard rows={topByProfitInWindow(plays, 5, now, ADM_WINDOWS[winWindow].windowMs)} M={M} kind="win"/>
          </div>
          <div>
            <div style={{fontSize:12, color:M.red, fontWeight:700, letterSpacing:'0.06em', marginBottom:6}}>BIGGEST LOSSES · {ADM_WINDOWS[winWindow].label}</div>
            <LeaderBoard rows={topByLossInWindow(plays, 5, now, ADM_WINDOWS[winWindow].windowMs)} M={M} kind="loss"/>
          </div>
        </div>
      </AdminCard>
    </div>
  );
}

// ─── Plays view — raw paginated /plays table ──────────────────────
// Every /plays row with all its game-specific fields (outcome / mode /
// pocket / kind / mult / pick / hands etc.). Filter by game / coin /
// user-search; paginate at 50 rows.
function PlaysView({ palette: M, anonUid }) {
  const { items: plays, err } = useFirebaseList('plays', { orderBy:'t', limit:5000, enabled: !!anonUid });
  const [gameF, setGameF] = React.useState('all');
  const [coinF, setCoinF] = React.useState('all');
  const [search, setSearch] = React.useState('');
  const [page, setPage] = React.useState(0);
  // Reset page when filters change. MUST live before any early return below
  // so the hook count stays constant across renders (React Rules of Hooks).
  React.useEffect(() => { setPage(0); }, [gameF, coinF, search]);

  const ROWS_PER_PAGE = 50;

  if (err) return <DataError palette={M} message={err}/>;
  if (plays.length === 0) return <NoPlaysYet palette={M}/>;

  // Filters run against the descending-time list (newest first).
  const desc = [...plays].reverse();
  const filtered = desc.filter(p => {
    if (gameF !== 'all' && (p.game || '?') !== gameF) return false;
    if (coinF !== 'all' && (p.coin || 'SOL') !== coinF) return false;
    if (search) {
      const q = search.toLowerCase();
      const hay = `${p.user || ''} ${p.uid || ''} ${p.hex || ''}`.toLowerCase();
      if (!hay.includes(q)) return false;
    }
    return true;
  });

  const totalPages = Math.max(1, Math.ceil(filtered.length / ROWS_PER_PAGE));
  const pageClamped = Math.min(page, totalPages - 1);
  const rows = filtered.slice(pageClamped * ROWS_PER_PAGE, (pageClamped + 1) * ROWS_PER_PAGE);

  return (
    <div style={{display:'flex', flexDirection:'column', gap:16}}>
      <ViewHeader palette={M} title="Raw /plays table" subtitle={`${filtered.length.toLocaleString()} of ${plays.length.toLocaleString()} rows · page ${pageClamped + 1}/${totalPages}`}/>

      <AdminCard title="Filters" accent={M.cyan}>
        <div style={{display:'flex', gap:14, alignItems:'center', flexWrap:'wrap'}}>
          <div style={{display:'flex', alignItems:'center', gap:8}}>
            <span style={{fontSize:11, color:M.ink2, letterSpacing:'0.06em', textTransform:'uppercase'}}>game</span>
            <RadioStrip palette={M} options={[{k:'all', l:'all'}, ...ADM_GAMES.map(g => ({k:g, l:normGameName(g)}))]} value={gameF} onChange={setGameF}/>
          </div>
          <div style={{display:'flex', alignItems:'center', gap:8}}>
            <span style={{fontSize:11, color:M.ink2, letterSpacing:'0.06em', textTransform:'uppercase'}}>coin</span>
            <RadioStrip palette={M} options={[{k:'all', l:'all'}, {k:'SOL', l:'SOL'}, {k:'USDC', l:'USDC'}, {k:'USDT', l:'USDT'}]} value={coinF} onChange={setCoinF}/>
          </div>
          <div style={{display:'flex', alignItems:'center', gap:8, flex:1, minWidth:220}}>
            <span style={{fontSize:11, color:M.ink2, letterSpacing:'0.06em', textTransform:'uppercase'}}>search</span>
            <input
              type="text"
              value={search}
              onChange={e=>setSearch(e.target.value)}
              placeholder="user / uid / hex"
              style={{
                flex:1, background:M.bg, border:`1px solid ${M.green}55`, color:M.ink,
                padding:'4px 8px', fontFamily:'inherit', fontSize:13, outline:'none',
              }}
            />
          </div>
        </div>
      </AdminCard>

      <AdminCard title={`Rows · ${rows.length} on page · ${filtered.length} total`} accent={M.green}>
        <div style={{overflowX:'auto'}}>
          <table style={{width:'100%', borderCollapse:'collapse', fontFamily:'JetBrains Mono, monospace', fontSize:12, minWidth:1100}}>
            <thead>
              <tr style={{borderBottom:`2px solid ${M.green}55`}}>
                {['time','game','mode/outcome','user','coin','bet','payout','profit','mult','hex','src'].map(h => (
                  <th key={h} style={{textAlign: h === 'bet' || h === 'payout' || h === 'profit' || h === 'mult' ? 'right' : 'left', padding:'6px 8px', color:M.ink2, fontSize:11, fontWeight:700, letterSpacing:'0.06em', textTransform:'uppercase'}}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {rows.map(p => {
                const profit = (Number(p.payout) || 0) - (Number(p.bet) || 0);
                const detail = p.outcome ? p.outcome
                  : p.kind   ? p.kind
                  : p.pocket !== undefined ? ('pocket ' + p.pocket)
                  : p.mode   ? p.mode
                  : p.pick   ? ('pick ' + p.pick)
                  : '—';
                return (
                  <tr key={p.id} style={{borderBottom:`1px dashed ${M.ink2}22`}}>
                    <td style={{padding:'5px 8px', color:M.ink2, fontSize:11, whiteSpace:'nowrap'}}>{p.t ? new Date(p.t).toLocaleString([], {month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit', second:'2-digit'}) : '—'}</td>
                    <td style={{padding:'5px 8px', color: gameColor(p.game, M)}}>{normGameName(p.game)}</td>
                    <td style={{padding:'5px 8px', color:M.ink, whiteSpace:'nowrap'}}>{detail}{p.hands && p.hands > 1 ? ` · ${p.hands}h` : ''}{p.steps ? ` · ${p.steps}s` : ''}</td>
                    <td style={{padding:'5px 8px', color:M.cyan, maxWidth:120, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}} title={p.uid || p.user || ''}>{p.user || 'anon'}</td>
                    <td style={{padding:'5px 8px', color:M.ink2}}>{p.coin || 'SOL'}</td>
                    <td style={{padding:'5px 8px', color:M.cyan, textAlign:'right'}}>{fmtNum(p.bet)}</td>
                    <td style={{padding:'5px 8px', color:M.yellow, textAlign:'right'}}>{fmtNum(p.payout)}</td>
                    <td style={{padding:'5px 8px', color: profit >= 0 ? M.green : M.red, textAlign:'right', fontWeight:700}}>{profit >= 0 ? '+' : ''}{profit.toFixed(3)}</td>
                    <td style={{padding:'5px 8px', color:M.ink2, textAlign:'right'}}>{p.mult ? p.mult.toFixed(2) + '×' : '—'}</td>
                    <td style={{padding:'5px 8px', color:M.ink2, fontSize:11, maxWidth:120, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}} title={p.hex || ''}>{p.hex ? p.hex.slice(0, 12) + '…' : '—'}</td>
                    <td style={{padding:'5px 8px', color: p.source === 'server' ? M.green : M.yellow, fontSize:11, letterSpacing:'0.05em'}}>{p.source === 'server' ? 'svr' : 'cli'}</td>
                  </tr>
                );
              })}
              {rows.length === 0 && (
                <tr><td colSpan={11} style={{padding:14, color:M.ink2, fontStyle:'italic', fontSize:13, textAlign:'center'}}>— no rows match filters —</td></tr>
              )}
            </tbody>
          </table>
        </div>

        <div style={{display:'flex', alignItems:'center', justifyContent:'space-between', marginTop:12, paddingTop:10, borderTop:`1px dashed ${M.ink2}33`}}>
          <span style={{fontSize:11, color:M.ink2}}>page {pageClamped + 1} of {totalPages} · {filtered.length} rows · 50/page</span>
          <div style={{display:'flex', gap:6}}>
            <button onClick={()=>setPage(0)} disabled={pageClamped === 0} style={pageBtn(M, pageClamped === 0)}>{'<<'}</button>
            <button onClick={()=>setPage(p => Math.max(0, p - 1))} disabled={pageClamped === 0} style={pageBtn(M, pageClamped === 0)}>{'<'}</button>
            <button onClick={()=>setPage(p => Math.min(totalPages - 1, p + 1))} disabled={pageClamped >= totalPages - 1} style={pageBtn(M, pageClamped >= totalPages - 1)}>{'>'}</button>
            <button onClick={()=>setPage(totalPages - 1)} disabled={pageClamped >= totalPages - 1} style={pageBtn(M, pageClamped >= totalPages - 1)}>{'>>'}</button>
          </div>
        </div>
      </AdminCard>
    </div>
  );
}

function pageBtn(M, disabled) {
  return {
    background: disabled ? 'transparent' : M.bg,
    color: disabled ? M.ink2 : M.green,
    border:`1px solid ${disabled ? M.ink2 + '44' : M.green + '88'}`,
    padding:'3px 10px', fontFamily:'inherit', fontSize:12, fontWeight:700,
    cursor: disabled ? 'not-allowed' : 'pointer', letterSpacing:'0.05em',
  };
}

// ─── Players view — per-UID aggregation ───────────────────────────
function PlayersView({ palette: M, anonUid }) {
  const { items: plays, err } = useFirebaseList('plays', { orderBy:'t', limit:5000, enabled: !!anonUid });
  const [sortBy, setSortBy] = React.useState('wagered'); // wagered | net | plays | lastSeen

  if (err) return <DataError palette={M} message={err}/>;
  if (plays.length === 0) return <NoPlaysYet palette={M}/>;

  const users = aggregateByUser(plays);
  const sorted = [...users].sort((a, b) => {
    if (sortBy === 'wagered')  return b.wagered  - a.wagered;
    if (sortBy === 'net')      return b.net      - a.net;
    if (sortBy === 'plays')    return b.plays    - a.plays;
    if (sortBy === 'lastSeen') return b.lastSeen - a.lastSeen;
    return 0;
  });

  return (
    <div style={{display:'flex', flexDirection:'column', gap:16}}>
      <ViewHeader palette={M} title="Players" subtitle={`${users.length.toLocaleString()} distinct uids · sorted by ${sortBy}`}/>

      <AdminCard
        title={`All players (${users.length})`}
        accent={M.cyan}
        right={<RadioStrip palette={M} options={[
          {k:'wagered',  l:'wagered'},
          {k:'net',      l:'net'},
          {k:'plays',    l:'plays'},
          {k:'lastSeen', l:'recent'},
        ]} value={sortBy} onChange={setSortBy}/>}
      >
        <div style={{overflowX:'auto'}}>
          <table style={{width:'100%', borderCollapse:'collapse', fontFamily:'JetBrains Mono, monospace', fontSize:12, minWidth:1000}}>
            <thead>
              <tr style={{borderBottom:`2px solid ${M.cyan}55`}}>
                {['#','user','uid','plays','wagered','paid','net','avg bet','fav game','coins','first seen','last seen'].map(h => (
                  <th key={h} style={{
                    textAlign: ['plays','wagered','paid','net','avg bet'].includes(h) ? 'right' : 'left',
                    padding:'6px 8px', color:M.ink2, fontSize:11, fontWeight:700, letterSpacing:'0.06em', textTransform:'uppercase',
                  }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {sorted.map((u, i) => (
                <tr key={u.uid} style={{borderBottom:`1px dashed ${M.ink2}22`}}>
                  <td style={{padding:'5px 8px', color:M.ink2}}>{i + 1}</td>
                  <td style={{padding:'5px 8px', color:M.cyan, fontWeight:700, maxWidth:140, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{u.user}</td>
                  <td style={{padding:'5px 8px', color:M.ink2, fontSize:11, maxWidth:140, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}} title={u.uid}>{u.uid.slice(0, 10)}{u.uid.length > 10 ? '…' : ''}</td>
                  <td style={{padding:'5px 8px', color:M.green, textAlign:'right'}}>{u.plays.toLocaleString()}</td>
                  <td style={{padding:'5px 8px', color:M.cyan, textAlign:'right'}}>{fmtNum(u.wagered)}</td>
                  <td style={{padding:'5px 8px', color:M.yellow, textAlign:'right'}}>{fmtNum(u.paid)}</td>
                  <td style={{padding:'5px 8px', color: u.net >= 0 ? M.green : M.red, textAlign:'right', fontWeight:700}}>{u.net >= 0 ? '+' : ''}{fmtNum(u.net)}</td>
                  <td style={{padding:'5px 8px', color:M.ink, textAlign:'right'}}>{fmtNum(u.avgBet)}</td>
                  <td style={{padding:'5px 8px', color: gameColor(u.favoriteGame, M)}}>{normGameName(u.favoriteGame)}</td>
                  <td style={{padding:'5px 8px', color:M.ink2, fontSize:11}}>{u.coins.join('/') || '—'}</td>
                  <td style={{padding:'5px 8px', color:M.ink2, fontSize:11, whiteSpace:'nowrap'}}>{u.firstSeen ? new Date(u.firstSeen).toLocaleDateString([], {month:'numeric', day:'numeric'}) : '—'}</td>
                  <td style={{padding:'5px 8px', color:M.ink2, fontSize:11, whiteSpace:'nowrap'}}>{u.lastSeen ? new Date(u.lastSeen).toLocaleString([], {month:'numeric', day:'numeric', hour:'2-digit', minute:'2-digit'}) : '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </AdminCard>
    </div>
  );
}

// ─── Realtime view — ops & health ─────────────────────────────────
function RealtimeView({ palette: M, anonUid }) {
  const { items: presence }                 = useFirebaseList('presence', { orderBy:'t', limit:1000, enabled: !!anonUid });
  const { items: errors, err: errorsErr }   = useFirebaseList('errors',   { orderBy:'t', limit:100,  enabled: !!anonUid });
  const { items: plays }                    = useFirebaseList('plays',    { orderBy:'t', limit:50,   enabled: !!anonUid });
  const connected = useFirebaseConnected();

  const byChannel = {};
  for (const p of presence) {
    const c = p.channel || 'general';
    byChannel[c] = (byChannel[c] || 0) + 1;
  }
  const channelRows = Object.entries(byChannel).sort((a, b) => b[1] - a[1]);
  const errorsNewest = [...errors].reverse();
  const recent = [...plays].reverse();

  return (
    <div style={{display:'flex', flexDirection:'column', gap:16}}>
      <ViewHeader palette={M} title="Realtime" subtitle="presence · errors · live plays"/>

      <div style={{display:'grid', gridTemplateColumns:'minmax(220px, 1fr) minmax(220px, 1fr) minmax(220px, 1fr)', gap:16}}>
        <AdminCard title="Live online" accent={M.green}>
          <div className="display" style={{fontSize:56, color:M.green, lineHeight:1, fontFamily:'VT323, monospace'}}>{presence.length}</div>
          <div style={{color:M.ink2, fontSize:11, marginTop:4}}>active sessions · ~5s drop detection</div>
        </AdminCard>

        <AdminCard title="Firebase RTDB" accent={connected ? M.green : M.red}>
          <div style={{display:'flex', alignItems:'center', gap:10}}>
            <span style={{ width:14, height:14, borderRadius:'50%', background: connected ? M.green : M.red, boxShadow:`0 0 8px ${connected ? M.green : M.red}` }}/>
            <span className="display" style={{fontSize:24, color: connected ? M.green : M.red, fontFamily:'VT323, monospace', letterSpacing:'0.05em'}}>{connected ? 'ONLINE' : 'RECONNECTING'}</span>
          </div>
          <div style={{marginTop:8, fontSize:11, color:M.ink2, lineHeight:1.5}}>
            <div>auth uid: <span style={{color: anonUid ? M.green : M.red}}>{anonUid ? 'signed in' : 'signing in…'}</span></div>
            <div>presence cap: 1000 read · errors cap: 100 stored</div>
          </div>
        </AdminCard>

        <AdminCard title={`Presence by channel (${channelRows.length})`} accent={M.cyan}>
          {channelRows.length === 0 ? (
            <div style={{color:M.ink2, fontStyle:'italic', fontSize:13}}>— no active sessions —</div>
          ) : (
            <BarChart
              palette={M} height={Math.max(80, channelRows.length * 22)}
              data={channelRows.map(([c, n]) => ({ label: '#' + c, value: n, color: M.cyan }))}
              valueFmt={v => String(v)}
            />
          )}
        </AdminCard>
      </div>

      <div style={{display:'grid', gridTemplateColumns:'1.4fr 1fr', gap:16}}>
        <AdminCard title={`Client errors (${errors.length}/100)`} accent={errors.length > 0 ? M.yellow : M.green}>
          {errorsErr && <div style={{color:M.red, fontSize:12, marginBottom:8}}>⚠ {errorsErr} (rules may not be published yet)</div>}
          {errorsNewest.length === 0 ? (
            <div style={{color:M.ink2, fontStyle:'italic', fontSize:13}}>— no errors logged —</div>
          ) : (
            <div style={{maxHeight:360, overflowY:'auto', fontSize:12, fontFamily:'JetBrains Mono, monospace', lineHeight:1.45}}>
              {errorsNewest.map(e => (
                <div key={e.id} style={{padding:'6px 0', borderBottom:`1px dashed ${M.ink2}33`}}>
                  <div style={{color:M.ink2, fontSize:11, display:'flex', justifyContent:'space-between', gap:6}}>
                    <span>{e.t ? new Date(e.t).toLocaleString() : '—'}</span>
                    <span style={{color:M.cyan}}>{e.page || '?'}</span>
                  </div>
                  <div style={{color:M.red, wordBreak:'break-word', marginTop:2}}>{e.msg}</div>
                  {e.ua && <div style={{color:M.ink2, fontSize:10, marginTop:2, opacity:0.7, wordBreak:'break-word'}}>{e.ua}</div>}
                </div>
              ))}
            </div>
          )}
        </AdminCard>

        <AdminCard title="Live plays · last 50" accent={M.green}>
          {recent.length === 0 ? (
            <div style={{color:M.ink2, fontStyle:'italic', fontSize:13}}>— no recent plays —</div>
          ) : (
            <div style={{maxHeight:360, overflowY:'auto', fontSize:12, fontFamily:'JetBrains Mono, monospace'}}>
              {recent.map(p => {
                const profit = (Number(p.payout) || 0) - (Number(p.bet) || 0);
                return (
                  <div key={p.id} style={{display:'grid', gridTemplateColumns:'auto 1fr auto', columnGap:8, padding:'4px 0', borderBottom:`1px dashed ${M.ink2}22`}}>
                    <span style={{color:M.ink2, fontSize:10}}>{p.t ? new Date(p.t).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit', second:'2-digit'}) : '—'}</span>
                    <span style={{color: gameColor(p.game, M), overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{normGameName(p.game)} · {p.user || 'anon'} · {fmtSol(Number(p.bet) || 0)}</span>
                    <span style={{color: profit >= 0 ? M.green : M.red, textAlign:'right'}}>{profit >= 0 ? '+' : ''}{profit.toFixed(3)}</span>
                  </div>
                );
              })}
            </div>
          )}
        </AdminCard>
      </div>
    </div>
  );
}

// ─── Shared bits ──────────────────────────────────────────────────
function ViewHeader({ palette: M, title, subtitle }) {
  return (
    <div style={{display:'flex', alignItems:'baseline', justifyContent:'space-between', marginBottom:6}}>
      <div>
        <h1 className="display" style={{margin:0, fontSize:30, color:M.green, fontFamily:'VT323, monospace', lineHeight:1, letterSpacing:'-0.02em'}}>{title}</h1>
        {subtitle && <div style={{color:M.ink2, fontSize:12, marginTop:2, letterSpacing:'0.04em'}}>{subtitle}</div>}
      </div>
    </div>
  );
}

function RadioStrip({ palette: M, options, value, onChange }) {
  return (
    <div style={{display:'inline-flex', border:`1px solid ${M.green}55`}}>
      {options.map(o => {
        const active = value === o.k;
        return (
          <button key={o.k} onClick={()=>onChange(o.k)} style={{
            background: active ? M.green : 'transparent',
            color: active ? M.bg : M.green,
            border:0, padding:'4px 10px',
            fontFamily:'inherit', fontSize:11, fontWeight:700, cursor:'pointer',
            letterSpacing:'0.06em', textTransform:'uppercase',
          }}>{o.l}</button>
        );
      })}
    </div>
  );
}

function DataError({ palette: M, message }) {
  return (
    <AdminCard title="Data · unavailable" accent={M.red}>
      <div style={{color:M.red, fontSize:14, marginBottom:8}}>⚠ {message}</div>
      <div style={{color:M.ink2, fontSize:13, lineHeight:1.5}}>
        this almost certainly means the Firebase rules haven't been published yet, or your admin uid
        isn't in the read allowlist. paste <code style={{color:M.cyan}}>admin/firebase-rules-step2.json</code> into the Firebase
        console, then refresh.
      </div>
    </AdminCard>
  );
}

function NoPlaysYet({ palette: M }) {
  return (
    <AdminCard title="Waiting on plays" accent={M.yellow}>
      <div style={{color:M.ink2, fontSize:13, lineHeight:1.5}}>
        no plays recorded yet. play a few rounds on any game (or have a test user do so) and they'll
        start appearing here within ~1 second of each round resolving.
      </div>
    </AdminCard>
  );
}

function LeaderBoard({ rows, M, kind }) {
  if (!rows || rows.length === 0) {
    return <div style={{color:M.ink2, fontStyle:'italic', fontSize:13}}>— no rounds yet —</div>;
  }
  return (
    <div style={{display:'grid', gridTemplateColumns:'28px 1.2fr 1fr 1fr 1fr', columnGap:10, rowGap:5, fontSize:13, fontFamily:'JetBrains Mono, monospace', alignItems:'center'}}>
      <span style={{color:M.ink2, fontSize:11}}>#</span>
      <span style={{color:M.ink2, fontSize:11}}>user</span>
      <span style={{color:M.ink2, fontSize:11}}>game</span>
      <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>{kind === 'win' ? 'profit' : 'loss'}</span>
      <span style={{color:M.ink2, fontSize:11, textAlign:'right'}}>when</span>
      {rows.map((r, i) => (
        <React.Fragment key={r.id || i}>
          <span style={{color:M.ink2}}>{i + 1}</span>
          <span style={{color:M.cyan, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap'}}>{r.user || 'anon'}</span>
          <span style={{color: gameColor(r.game, M)}}>{normGameName(r.game)}</span>
          <span style={{color: kind === 'win' ? M.green : M.red, textAlign:'right'}}>
            {kind === 'win' ? '+' : ''}{(r.profit || 0).toFixed(3)} {r.coin || 'SOL'}
          </span>
          <span style={{color:M.ink2, textAlign:'right', fontSize:11}}>{r.t ? new Date(r.t).toLocaleString() : '—'}</span>
        </React.Fragment>
      ))}
    </div>
  );
}

Object.assign(window, { HomePage, HowPage, FairnessPage, LeaderboardPage, ProfilePage, WalletPage, AdminPage, GameCard, PixelCoin });
