const { useState, useEffect, useCallback, useMemo } = React;

/* ── i18n ──
   Strings live in locales/<lang>.json (served statically alongside this
   file under /dice/locales/). Active locale is chosen from, in priority:
     1. ?lang= query param (operator can append it to the launch URL;
        the backend also forwards the /init `language` here)
     2. localStorage "dice_lang" (sticky across refresh / token strip)
     3. "en" default
   Both the active locale and English (the fallback) are fetched once at
   boot, before the first render, so t() is synchronous in the component
   tree. Missing keys fall back to English, then to the raw key. */
const SUPPORTED_LANGS = ["en", "ka"];
// Bump together with index.html's app.jsx?v= so locale edits bust the CDN cache.
const LOCALE_VERSION = "505";
let _messages = {};   // active-locale dict
let _fallback = {};   // always English

function detectLang() {
  const q = new URLSearchParams(window.location.search).get("lang");
  if (q && SUPPORTED_LANGS.includes(q)) {
    try { localStorage.setItem("dice_lang", q); } catch (e) {}
    return q;
  }
  try {
    const stored = localStorage.getItem("dice_lang");
    if (stored && SUPPORTED_LANGS.includes(stored)) return stored;
  } catch (e) {}
  return "en";
}

async function loadLocale() {
  const lang = detectLang();
  // Cache-bust with the same version the page is pinned to so a locale
  // edit ships without a stale CDN copy. (?v matches index.html's app.jsx.)
  const fetchJson = async (l) => {
    try {
      const r = await fetch(`locales/${l}.json?v=${LOCALE_VERSION}`);
      return r.ok ? await r.json() : {};
    } catch (e) { return {}; }
  };
  _fallback = await fetchJson("en");
  _messages = lang === "en" ? _fallback : await fetchJson(lang);
}

function t(key, vars) {
  let s = _messages[key];
  if (s == null) s = _fallback[key];
  if (s == null) s = key;
  if (vars) {
    for (const k in vars) s = s.split("{" + k + "}").join(String(vars[k]));
  }
  return s;
}

/* ── Mock API (no backend needed) ── */
const MOCK_MODE = !window.DICE_API_URL;
const _mock = {
  balance: 10000.00,
  clientSeed: "demo_" + Math.random().toString(36).slice(2, 10),
  seedHash: Array.from(crypto.getRandomValues(new Uint8Array(32)), b => b.toString(16).padStart(2,"0")).join(""),
  history: [],
  spinNum: 0,
  round: null,
  odds: { "1": { multiplier: "9.67", win_prob: 0.0998 }, "2": { multiplier: "1.92", win_prob: 0.5023 }, "3": { multiplier: "1.15", win_prob: 0.8380 } },
  config: { min_bet: "0.10", max_bet: "100.00" },
  rollDice: (n) => Array.from({length:n}, () => Math.floor(Math.random()*6)+1),
  freeRounds: (() => { const p = new URLSearchParams(window.location.search).get("freeRounds"); return p ? { total: +p, left: +p, bet: "10.00" } : null; })(),
};

function _mockRoll(dc) {
  const dd = _mock.rollDice(2), pd = _mock.rollDice(dc);
  const dt = dd.reduce((a,b)=>a+b,0), pt = pd.reduce((a,b)=>a+b,0);
  return { dealer_dice: dd, player_dice: pd, dealer_total: dt, player_total: pt,
    roll_outcome: pt>dt ? "win" : pt===dt ? "tie" : "lose" };
}
function _mockHistory(outcome, cost, payout, dc, mult, step) {
  _mock.spinNum++;
  _mock.history.unshift({
    spin_number: _mock.spinNum, round_id: "mock-"+_mock.spinNum, outcome,
    total_cost: cost.toFixed(2), total_payout: payout.toFixed(2), profit: (payout-cost).toFixed(2),
    server_seed: Array.from(crypto.getRandomValues(new Uint8Array(16)), b=>b.toString(16).padStart(2,"0")).join(""),
    seed_hash: _mock.seedHash, client_seed: _mock.clientSeed,
    game_data: { dice_count: dc, multiplier: String(mult), step: step || 1 },
  });
}

function _mockApi(path, opts = {}) {
  const body = opts.body && typeof opts.body === "object" ? opts.body : (opts.body ? JSON.parse(opts.body) : {});
  if (path === "/api/v1/state") {
    return { balance: _mock.balance.toFixed(2), currency: "USD", game_data: { odds: _mock.odds }, odds: _mock.odds, config: _mock.config, next_seed_hash: _mock.seedHash, client_seed: _mock.clientSeed };
  }
  if (path === "/api/v1/bet") {
    // Sequential: the opening duel. Win/tie open the round; lose is terminal.
    const dc = body.dice_count || 1;
    const amount = +(body.amount || 10);
    _mock.balance = +(_mock.balance - amount).toFixed(2);
    _mock.seedHash = Array.from(crypto.getRandomValues(new Uint8Array(32)), b=>b.toString(16).padStart(2,"0")).join("");
    const roll = _mockRoll(dc);
    const mult = +(_mock.odds[String(dc)]?.multiplier || 1);
    const roundId = "mock-round-" + (_mock.spinNum + 1);
    if (roll.roll_outcome === "lose") {
      if (_mock.freeRounds && _mock.freeRounds.left > 0) _mock.freeRounds.left--;
      _mockHistory("lose", amount, 0, dc, mult, 1);
      const resp = { round_id: roundId, finished: true, outcome: "lose", balance: _mock.balance.toFixed(2),
        next_seed_hash: _mock.seedHash,
        game_data: { ...roll, status:"lose", pot:"0.00", payout:"0.00", multiplier:String(mult), dice_count:dc, step:1, finished:true } };
      return resp;
    }
    const isTie = roll.roll_outcome === "tie";
    const pot = roll.roll_outcome === "win" ? +(amount*mult).toFixed(2) : amount;
    _mock.round = { id: roundId, dc, bet: amount, pot, mult, step: 1 };
    // outcome/status are "tie" on an opening tie (matches backend post-fix),
    // "active" on a win. The roll itself is in game_data.roll_outcome.
    return { round_id: roundId, finished: false, outcome: isTie ? "tie" : "active", balance: _mock.balance.toFixed(2),
      next_seed_hash: _mock.seedHash,
      game_data: { ...roll, status: isTie ? "tie" : "active", pot:pot.toFixed(2), multiplier:String(mult), dice_count:dc, step:1, finished:false, max_rounds:({1:4,2:13,3:30}[dc]||30) } };
  }
  const actionMatch = path.match(/^\/api\/v1\/round\/[^/]+\/action$/);
  if (actionMatch) {
    const r = _mock.round;
    if (!r) return { error_code:"INVALID_ROUND", error_description:"No active round" };
    const roll = _mockRoll(r.dc);
    r.step++;
    if (roll.roll_outcome === "lose") {
      _mock.round = null; if (_mock.freeRounds && _mock.freeRounds.left > 0) _mock.freeRounds.left--;
      _mockHistory("lose", r.bet, 0, r.dc, r.mult, r.step);
      return { round_id:r.id, finished:true, outcome:"lose", balance:_mock.balance.toFixed(2),
        game_data:{ ...roll, status:"lose", pot:"0.00", payout:"0.00", multiplier:String(r.mult), dice_count:r.dc, step:r.step, finished:true } };
    }
    const MAX_ROUNDS = { 1: 4, 2: 13, 3: 30 };
    if (roll.roll_outcome === "win") {
      let pot = +(r.pot * r.mult).toFixed(2);
      if (r.step >= (MAX_ROUNDS[r.dc] || 30)) {
        _mock.balance = +(_mock.balance + pot).toFixed(2);
        _mock.round = null; if (_mock.freeRounds && _mock.freeRounds.left > 0) _mock.freeRounds.left--;
        _mockHistory("cashout", r.bet, pot, r.dc, r.mult, r.step);
        return { round_id:r.id, finished:true, outcome:"cashout", balance:_mock.balance.toFixed(2),
          game_data:{ ...roll, status:"cashout", pot:pot.toFixed(2), payout:pot.toFixed(2), multiplier:String(r.mult), dice_count:r.dc, step:r.step, finished:true } };
      }
      r.pot = pot;
    }
    return { round_id:r.id, finished:false, outcome:"active", balance:_mock.balance.toFixed(2),
      game_data:{ ...roll, status:"active", pot:r.pot.toFixed(2), multiplier:String(r.mult), dice_count:r.dc, step:r.step, finished:false, max_rounds:(MAX_ROUNDS[r.dc]||30) } };
  }
  const cashoutMatch = path.match(/^\/api\/v1\/round\/[^/]+\/cashout$/);
  if (cashoutMatch) {
    const r = _mock.round;
    if (!r) return { error_code:"INVALID_ROUND", error_description:"No active round" };
    _mock.balance = +(_mock.balance + r.pot).toFixed(2);
    _mock.round = null; if (_mock.freeRounds && _mock.freeRounds.left > 0) _mock.freeRounds.left--;
    const isPush = r.pot <= r.bet;
    _mockHistory(isPush ? "tie" : "cashout", r.bet, r.pot, r.dc, r.mult, r.step);
    return { round_id:r.id, finished:true, outcome:"cashout", balance:_mock.balance.toFixed(2),
      game_data:{ status:"cashout", pot:r.pot.toFixed(2), payout:r.pot.toFixed(2), multiplier:String(r.mult), dice_count:r.dc, step:r.step, finished:true } };
  }
  if (path.startsWith("/api/v1/history")) return { rounds: _mock.history.slice(0, 50) };
  if (path === "/api/v1/client-seed") { if (body.client_seed) _mock.clientSeed = body.client_seed; return {}; }
  if (path === "/api/v1/refresh-seed") { _mock.seedHash = Array.from(crypto.getRandomValues(new Uint8Array(32)), b => b.toString(16).padStart(2,"0")).join(""); return {}; }
  if (path === "/api/v1/verify") { const dd = _mock.rollDice(2), pd = _mock.rollDice(body.dice_count||1); return { dealer_dice: dd, player_dice: pd, dealer_total: dd.reduce((a,b)=>a+b,0), player_total: pd.reduce((a,b)=>a+b,0) }; }
  if (path === "/api/v1/freerounds") {
    const fr = _mock.freeRounds;
    if (!fr || fr.left <= 0) return { grants: [] };
    return { grants: [{ rounds_total: fr.total, rounds_used: fr.total - fr.left, bet_amount: fr.bet, currency: "USD", winnings_total: "0" }] };
  }
  return {};
}

/* ── API Client ── */
const API_BASE = window.DICE_API_URL || "http://localhost:8080";
const DEMO_KEY = window.DICE_DEMO_KEY || "74205b7502590744595896acd837935ccf5686c68b3a59d4c25ae61f664504a9";

let _sessionToken = new URLSearchParams(window.location.search).get("token") || "";
function getToken() { return _sessionToken; }

async function bootstrapDemo() {
  if (MOCK_MODE) { _sessionToken = "mock-token"; return true; }
  if (_sessionToken && _sessionToken !== DEMO_KEY) return false;
  const res = await fetch(`${API_BASE}/api/v1/init-demo`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ game_uuid: "" })
  });
  const data = await res.json();
  if (data.error_code) throw new Error(data.error_description || data.error_code);
  const url = new URL(data.url, window.location.origin);
  const realToken = url.searchParams.get("token");
  if (realToken) _sessionToken = realToken;
  window.history.replaceState({}, "", window.location.pathname);
  return true;
}

async function api(path, opts = {}) {
  if (MOCK_MODE) return _mockApi(path, opts);
  const headers = { ...(opts.headers || {}) };
  const token = getToken();
  if (token) headers["Authorization"] = `Bearer ${token}`;
  if (opts.body && typeof opts.body === "object") {
    headers["Content-Type"] = "application/json";
    opts.body = JSON.stringify(opts.body);
  }
  const res = await fetch(`${API_BASE}${path}`, { ...opts, headers });
  const text = await res.text();
  let data;
  try { data = JSON.parse(text); } catch(e) { throw new Error("API unavailable"); }
  if (data.error_code) throw new Error(data.error_description || data.error_code);
  return data;
}

/* ── Free rounds ── */
// The player's free rounds come from the backend grant ledger — the only
// authoritative source. Returns aggregate totals across every active grant
// for this session's game. A spoofed URL can no longer fake free rounds,
// and the count survives ties (the engine releases the slot on a tie).
async function fetchFreeRounds() {
  try {
    const d = await api("/api/v1/freerounds");
    const grants = d.grants || [];
    let total = 0, used = 0, winnings = 0, betAmount = "", currency = "";
    for (const g of grants) {
      total += g.rounds_total || 0;
      used  += g.rounds_used  || 0;
      winnings += +(g.winnings_total || 0);
      if (!betAmount && g.bet_amount) { betAmount = g.bet_amount; currency = g.currency || ""; }
    }
    return { total, used, left: total - used, winnings, betAmount, currency };
  } catch (e) {
    return { total: 0, used: 0, left: 0, winnings: 0, betAmount: "", currency: "" };
  }
}

// Resume support — pull the persisted state of an in-progress sequential
// round so the UI can be restored after a refresh / reconnect / device
// switch. Returns null on any error rather than blocking init.
async function fetchRoundState(roundId) {
  try { return await api(`/api/v1/round/${roundId}/state`); }
  catch (e) { return null; }
}

/* ── Constants ── */
const DOTS = { 1:[[50,50]], 2:[[27,27],[73,73]], 3:[[27,27],[50,50],[73,73]], 4:[[27,27],[73,27],[27,73],[73,73]], 5:[[27,27],[73,27],[50,50],[27,73],[73,73]], 6:[[27,22],[73,22],[27,50],[73,50],[27,78],[73,78]] };

/* ── Sound ── */
const SFX = (() => {
  let ctx, gain, _muted = localStorage.getItem("sfx_muted")==="true";
  const getCtx = () => { if(!ctx){ctx=new(window.AudioContext||window.webkitAudioContext)();gain=ctx.createGain();gain.gain.value=_muted?0:1;gain.connect(ctx.destination)} if(ctx.state==="suspended")ctx.resume(); return ctx; };
  const tone = (f,d,t="sine",v=.15) => { const c=getCtx(),o=c.createOscillator(),g=c.createGain();o.type=t;o.frequency.value=f;g.gain.setValueAtTime(v,c.currentTime);g.gain.exponentialRampToValueAtTime(.001,c.currentTime+d);o.connect(g).connect(gain);o.start();o.stop(c.currentTime+d); };
  const noiseBurst = (c,dur,decay) => { const len=Math.floor(c.sampleRate*dur),buf=c.createBuffer(1,len,c.sampleRate),d=buf.getChannelData(0); for(let i=0;i<len;i++)d[i]=(Math.random()*2-1)*Math.exp(-i/(c.sampleRate*decay)); return buf; };
  return {
    click:()=>tone(800,.06,"square",.08),
    roll:()=>{
      const c=getCtx(),t=c.currentTime;
      [0.55,0.68,0.78,0.86,0.92,0.96,0.99].forEach((off,idx)=>{
        const vol=0.4*Math.pow(0.7,idx),freq=1800+Math.random()*2500;
        const src=c.createBufferSource();src.buffer=noiseBurst(c,0.015,0.003);
        const hp=c.createBiquadFilter();hp.type="highpass";hp.frequency.value=freq;
        const g=c.createGain();g.gain.value=vol;
        src.connect(hp).connect(g).connect(gain);src.start(t+off);src.stop(t+off+0.015);
        const o=c.createOscillator(),tg=c.createGain();o.type="sine";
        o.frequency.setValueAtTime(200+Math.random()*100,t+off);o.frequency.exponentialRampToValueAtTime(60,t+off+0.06);
        tg.gain.setValueAtTime(vol*0.6,t+off);tg.gain.exponentialRampToValueAtTime(0.001,t+off+0.07);
        o.connect(tg).connect(gain);o.start(t+off);o.stop(t+off+0.07);
      });
    },
    win:()=>{tone(523,.15,"sine",.12);setTimeout(()=>tone(659,.15,"sine",.12),100);setTimeout(()=>tone(784,.25,"sine",.15),200)},
    lose:()=>{tone(350,.2,"sawtooth",.08);setTimeout(()=>tone(280,.35,"sawtooth",.06),150)},
    tie:()=>{tone(440,.15,"triangle",.1);setTimeout(()=>tone(440,.2,"triangle",.08),160)},
    setMuted:(m)=>{_muted=m;localStorage.setItem("sfx_muted",m);if(gain)gain.gain.value=m?0:1},
    isMuted:()=>_muted
  };
})();

/* ── Helpers ── */
const rc = v => Math.round(v*100)/100;

// Currency code → display symbol. Session currency arrives from the API;
// anything not listed falls back to the code itself (see currSymbol).
const CURRENCY_SYMBOLS = {
  USD: "$", EUR: "€", GBP: "£", GEL: "₾", RUB: "₽", TRY: "₺",
  UAH: "₴", JPY: "¥", CNY: "¥", INR: "₹", BRL: "R$", KZT: "₸",
  BTC: "₿", ETH: "Ξ", USDT: "₮", USDC: "$",
};

/* ── SVG Icons ── */
function IconInfo() {
  return (
    <svg viewBox="0 0 16 16" fill="#D4D4D4">
      <path d="M8 1.33A6.67 6.67 0 1014.67 8 6.67 6.67 0 008 1.33zm-.67 4a.67.67 0 111.34 0 .67.67 0 01-1.34 0zM8.67 11.33h-1.34V7.33h1.34v4z"/>
    </svg>
  );
}

function IconVolume({ muted }) {
  return (
    <svg viewBox="0 0 16 16" fill="#D4D4D4">
      {muted ? (<>
        <path d="M7.3 2.7L4.6 5.3H1.3v5.4h3.3l2.7 2.6V2.7z"/>
        <path d="M14 5.5l-4 5M10 5.5l4 5" fill="none" stroke="#D4D4D4" strokeWidth="1.5" strokeLinecap="round"/>
      </>) : (<>
        <path d="M7.3 2.7L4.6 5.3H1.3v5.4h3.3l2.7 2.6V2.7z"/>
        <path d="M10.1 5.4a3.3 3.3 0 010 5.2" fill="none" stroke="#D4D4D4" strokeWidth="1.5" strokeLinecap="round"/>
        <path d="M12 3.5a6.6 6.6 0 010 9" fill="none" stroke="#D4D4D4" strokeWidth="1.5" strokeLinecap="round"/>
      </>)}
    </svg>
  );
}

function IconShield() {
  return (
    <svg viewBox="0 0 16 16" fill="#D4D4D4">
      <path d="M8 1.3L2.7 3.3v4.5c0 4 5.3 6.7 5.3 6.7s5.3-2.7 5.3-6.7V3.3L8 1.3zM7.3 10.5L5.5 8.7l.9-.9 1 1 2.3-2.3.9.9L7.3 10.5z"/>
    </svg>
  );
}

function IconHistory() {
  return (
    <svg viewBox="0 0 16 16" fill="#D4D4D4">
      <path d="M8 1.33A6.67 6.67 0 1014.67 8 6.67 6.67 0 008 1.33zM8.67 8L11 9.2l-.5.9L7.67 8.5V4.67h1V8z"/>
    </svg>
  );
}

function IconDice({ disabled }) {
  const bg = disabled ? "#525252" : "#171717";
  const dot = disabled ? "#404040" : "#FFD106";
  return (
    <svg viewBox="0 0 24 24" fill="none">
      <rect x="2" y="2" width="20" height="20" rx="4" fill={bg}/>
      <circle cx="7.5" cy="7.5" r="1.8" fill={dot}/>
      <circle cx="16.5" cy="7.5" r="1.8" fill={dot}/>
      <circle cx="7.5" cy="16.5" r="1.8" fill={dot}/>
      <circle cx="16.5" cy="16.5" r="1.8" fill={dot}/>
      <circle cx="12" cy="12" r="1.8" fill={dot}/>
    </svg>
  );
}

function IconDiceCount({ count }) {
  // Small 16x16 dice icons for risk tabs — shows 1, 2, or 3 dice
  if (count === 1) return (
    <svg viewBox="0 0 16 16" width="16" height="16" fill="none">
      <rect x="1" y="1" width="14" height="14" rx="3" fill="currentColor" opacity="0.25"/>
      <circle cx="8" cy="8" r="1.5" fill="currentColor"/>
    </svg>
  );
  if (count === 2) return (
    <svg viewBox="0 0 16 16" width="16" height="16" fill="none">
      <rect x="0" y="2" width="10" height="10" rx="2.5" fill="currentColor" opacity="0.25"/>
      <circle cx="5" cy="7" r="1.2" fill="currentColor"/>
      <rect x="6" y="4" width="10" height="10" rx="2.5" fill="currentColor" opacity="0.25"/>
      <circle cx="11" cy="9" r="1.2" fill="currentColor"/>
    </svg>
  );
  return (
    <svg viewBox="0 0 16 16" width="16" height="16" fill="none">
      <rect x="0" y="3" width="8" height="8" rx="2" fill="currentColor" opacity="0.25"/>
      <circle cx="4" cy="7" r="1" fill="currentColor"/>
      <rect x="4" y="1" width="8" height="8" rx="2" fill="currentColor" opacity="0.25"/>
      <circle cx="8" cy="5" r="1" fill="currentColor"/>
      <rect x="8" y="5" width="8" height="8" rx="2" fill="currentColor" opacity="0.25"/>
      <circle cx="12" cy="9" r="1" fill="currentColor"/>
    </svg>
  );
}

function IconStepForward() {
  return (
    <svg viewBox="0 0 16 16" width="16" height="16" fill="none">
      <path d="M4 3l6 5-6 5V3z" fill="currentColor"/>
      <rect x="11" y="3" width="2" height="10" rx="1" fill="currentColor"/>
    </svg>
  );
}

function IconChevronUp() {
  return (
    <svg viewBox="0 0 16 16" fill="none" width="12" height="12">
      <path d="M4 10l4-4 4 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
    </svg>
  );
}

function IconMinus() {
  return (
    <svg viewBox="0 0 16 16" fill="none" width="14" height="14">
      <line x1="4" y1="8" x2="12" y2="8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

function IconPlus() {
  return (
    <svg viewBox="0 0 16 16" fill="none" width="14" height="14">
      <line x1="8" y1="4" x2="8" y2="12" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
      <line x1="4" y1="8" x2="12" y2="8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
    </svg>
  );
}

/* ── Zdog Dice Constants ── */
const ZDOG_TAU = Zdog.TAU;
const FACE_COLOR = '#E1E1E1';
const DOT_COLOR = '#0D0D0D';
const MIN_TUMBLE = 300;   // minimum tumble time before landing (ms)
const LAND_DURATION = 300; // landing deceleration duration (ms)
const DROP_DURATION = 200; // drop-in duration at tumble start (ms)
const DROP_HEIGHT = 60;
const TUMBLE_SPEED = 0.012; // radians per ms during tumble
const WIGGLE_START = 0.6;
const WIGGLE_AMP = 0.2;
const WIGGLE_FREQ = 1.5;
const WIGGLE_DECAY = 5;

// Maps a die value (1-6) to the Zdog rotation that brings that face's pip
// group to +z (the viewer). All verified against Zdog's vector.js: rotateX
// is standard right-hand rule (propA='y', propB='z'); rotateY's sin sign
// is FLIPPED relative to standard RHR (propA='x', propB='z' with the
// rotateProperty formula `new_propA = a*cos - b*sin`), so rotateY(+π/2)
// actually maps +x → +z and -x → -z. Each value below was checked by
// hand against the actual rotation Zdog applies — do not "fix" without
// reading vector.js first.
const FACE_ROTATIONS = {
  1: { x: 0, y: 0, z: 0 },
  2: { x: ZDOG_TAU/4, y: 0, z: 0 },
  3: { x: 0, y: ZDOG_TAU/4, z: 0 },
  4: { x: 0, y: -ZDOG_TAU/4, z: 0 },
  5: { x: -ZDOG_TAU/4, y: 0, z: 0 },
  6: { x: 0, y: ZDOG_TAU/2, z: 0 },
};

function buildDieGeometry(parent) {
  const face = new Zdog.Rect({ addTo: new Zdog.Group({ addTo: parent }), stroke: 50, width: 50, height: 50, color: FACE_COLOR, translate: { z: -25 } });
  face.copy({ rotate: { x: ZDOG_TAU/4 }, translate: { y: 25 } });
  face.copy({ rotate: { x: ZDOG_TAU/4 }, translate: { y: -25 } });
  face.copy({ translate: { z: 25 } });

  const dot = new Zdog.Ellipse({ addTo: parent, diameter: 15, stroke: false, fill: true, color: DOT_COLOR, translate: { z: 50 } });

  const two = new Zdog.Group({ addTo: parent, rotate: { x: ZDOG_TAU/4 }, translate: { y: 50 } });
  dot.copy({ addTo: two, translate: { y: 20 } });
  dot.copy({ addTo: two, translate: { y: -20 } });

  const three = new Zdog.Group({ addTo: parent, rotate: { y: ZDOG_TAU/4 }, translate: { x: 50 } });
  dot.copy({ addTo: three, translate: { z: 0 } });
  dot.copy({ addTo: three, translate: { x: 20, y: -20, z: 0 } });
  dot.copy({ addTo: three, translate: { x: -20, y: 20, z: 0 } });

  const four = new Zdog.Group({ addTo: parent, rotate: { y: ZDOG_TAU/4 }, translate: { x: -50 } });
  two.copyGraph({ addTo: four, rotate: { x: 0 }, translate: { x: 20, y: 0 } });
  two.copyGraph({ addTo: four, rotate: { x: 0 }, translate: { x: -20, y: 0 } });

  const five = new Zdog.Group({ addTo: parent, rotate: { x: ZDOG_TAU/4 }, translate: { y: -50 } });
  four.copyGraph({ addTo: five, rotate: { y: 0 }, translate: { x: 0 } });
  dot.copy({ addTo: five, translate: { z: 0 } });

  const six = new Zdog.Group({ addTo: parent, translate: { z: -50 } });
  two.copyGraph({ addTo: six, rotate: { x: 0, z: ZDOG_TAU/4 }, translate: { x: 0, y: 0 } });
  four.copyGraph({ addTo: six, rotate: { y: 0 }, translate: { x: 0 } });
}

/* ── Zdog Dice Group (one canvas for multiple dice) ── */
const DICE_SPACING = 121; // space between dice centers in Zdog units (12px gap at 0.56 zoom)
const MAX_DICE = 3;

function ZdogDiceGroup({ dice, rolling, rollSeq }) {
  const canvasRef = React.useRef(null);
  const zdogRef = React.useRef(null);
  const animRef = React.useRef(null);
  const diceRef = React.useRef([]);
  const visibleRef = React.useRef(0);

  // Build once with MAX_DICE, never rebuild
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas || zdogRef.current) return;

    const illo = new Zdog.Illustration({ element: canvas, zoom: 0.56, rotate: { x: 0, y: 0 } });
    const diceArr = [];
    for (let i = 0; i < MAX_DICE; i++) {
      const anchor = new Zdog.Anchor({ addTo: illo, translate: { x: 0, y: 0 } });
      buildDieGeometry(anchor);
      const rot = FACE_ROTATIONS[1];
      anchor.rotate.x = rot.x; anchor.rotate.y = rot.y; anchor.rotate.z = rot.z;
      diceArr.push({
        anchor,
        phase: "idle",       // "idle" | "tumble" | "land"
        startTime: 0,
        targetValue: 1,
        spinDirX: 1,
        spinDirY: 1,
        landFrom: { x: 0, y: 0, z: 0 },
        landTo: { x: 0, y: 0, z: 0 },
        landStart: 0,
        visible: false,
      });
    }

    zdogRef.current = illo;
    diceRef.current = diceArr;
    const unregister = _registerDiceGroup(diceArr);

    let lastTime = 0;
    function animate(now) {
      const dt = lastTime ? now - lastTime : 16;
      lastTime = now;
      diceArr.forEach(d => {
        if (d.phase === "idle" || now < d.startTime) return;

        if (d.phase === "tumble") {
          // Drop-in easing for the first DROP_DURATION ms
          const elapsed = now - d.startTime;
          const dropT = Math.min(elapsed / DROP_DURATION, 1);
          const dropEased = 1 - Math.pow(1 - dropT, 3);
          d.anchor.translate.y = DROP_HEIGHT * (1 - dropEased);

          // Free spin
          d.anchor.rotate.x += TUMBLE_SPEED * dt * d.spinDirX;
          d.anchor.rotate.y += TUMBLE_SPEED * dt * d.spinDirY;

          // Transition to land phase when target is known and MIN_TUMBLE elapsed
          if (d.targetValue && elapsed >= MIN_TUMBLE) {
            d.landFrom = { x: d.anchor.rotate.x, y: d.anchor.rotate.y, z: d.anchor.rotate.z };
            const target = FACE_ROTATIONS[d.targetValue];
            d.landTo = {
              x: target.x + ZDOG_TAU * (2 + Math.floor(Math.random())) * d.spinDirX,
              y: target.y + ZDOG_TAU * (2 + Math.floor(Math.random())) * d.spinDirY,
              z: 0
            };
            d.landStart = now;
            d.phase = "land";
          }
        }

        if (d.phase === "land") {
          const t = Math.min((now - d.landStart) / LAND_DURATION, 1);
          const eased = 1 - Math.pow(1 - t, 3);
          d.anchor.translate.y = 0;
          d.anchor.rotate.x = d.landFrom.x + (d.landTo.x - d.landFrom.x) * eased;
          d.anchor.rotate.y = d.landFrom.y + (d.landTo.y - d.landFrom.y) * eased;
          d.anchor.rotate.z = d.landFrom.z + (d.landTo.z - d.landFrom.z) * eased;
          if (t > WIGGLE_START) {
            const wp = (t - WIGGLE_START) / (1 - WIGGLE_START);
            const decay = Math.exp(-WIGGLE_DECAY * wp);
            d.anchor.rotate.x += WIGGLE_AMP * decay * Math.sin(wp * Math.PI * WIGGLE_FREQ);
            d.anchor.rotate.y += WIGGLE_AMP * 0.6 * decay * Math.sin(wp * Math.PI * WIGGLE_FREQ * 1.2 + 0.4);
          }
          if (t >= 1) {
            d.phase = "idle";
            d.anchor.translate.y = 0;
            const target = FACE_ROTATIONS[d.targetValue];
            d.anchor.rotate.x = target.x; d.anchor.rotate.y = target.y; d.anchor.rotate.z = target.z;
          }
        }
      });
      illo.updateRenderGraph();
      animRef.current = requestAnimationFrame(animate);
    }
    animRef.current = requestAnimationFrame(animate);

    return () => { if (animRef.current) cancelAnimationFrame(animRef.current); unregister(); };
  }, []);

  // Reposition dice when count changes (no rebuild)
  useEffect(() => {
    const diceArr = diceRef.current;
    if (!diceArr.length) return;
    const count = dice.length;
    const totalWidth = (count - 1) * DICE_SPACING;
    for (let i = 0; i < MAX_DICE; i++) {
      const d = diceArr[i];
      if (i < count) {
        d.anchor.translate.x = -totalWidth/2 + i * DICE_SPACING;
        d.anchor.translate.y = 0;
        d.visible = true;
      } else {
        // Hide by moving off-screen
        d.anchor.translate.x = 9999;
        d.anchor.translate.y = 9999;
        d.visible = false;
      }
    }
    visibleRef.current = count;
  }, [dice.length]);

  // Handle rolling start / stop
  useEffect(() => {
    const diceArr = diceRef.current;
    if (!diceArr.length) return;
    if (rolling) {
      const now = performance.now();
      const count = dice.length;
      for (let i = 0; i < count; i++) {
        const d = diceArr[i];
        if (!d.visible) continue;
        d.targetValue = null;
        d.spinDirX = Math.random() > 0.5 ? 1 : -1;
        d.spinDirY = Math.random() > 0.5 ? 1 : -1;
        d.startTime = now + i * 90;
        d.phase = "tumble";
      }
    } else {
      // Safety net: force-snap any dice still stuck after rolling ends
      diceArr.forEach(d => {
        if (d.phase !== "idle" && d.visible) {
          d.phase = "idle";
          if (d.targetValue) {
            const target = FACE_ROTATIONS[d.targetValue];
            d.anchor.rotate.x = target.x; d.anchor.rotate.y = target.y; d.anchor.rotate.z = target.z;
          }
          d.anchor.translate.y = 0;
        }
      });
    }
  }, [rolling]);

  // Handle values arriving — set targetValue so the animate loop
  // transitions from tumble → land once MIN_TUMBLE has elapsed.
  useEffect(() => {
    const diceArr = diceRef.current;
    if (!diceArr.length) return;
    dice.forEach((val, i) => {
      if (i >= MAX_DICE || !val) return;
      const d = diceArr[i];
      if (d.phase === "tumble") {
        // Mid-tumble: the animate loop will pick up targetValue and
        // begin the landing phase once MIN_TUMBLE has elapsed.
        d.targetValue = val;
      } else if (d.phase === "idle") {
        // Not animating (initial mount, resume restore) — snap directly.
        d.targetValue = val;
        const target = FACE_ROTATIONS[val];
        d.anchor.rotate.x = target.x; d.anchor.rotate.y = target.y; d.anchor.rotate.z = target.z;
        d.anchor.translate.y = 0;
      }
    });
  }, [rollSeq, dice[0], dice[1], dice[2]]);

  return <canvas ref={canvasRef} className="zdog-dice-canvas" width="300" height="220"/>;
}

// Module-level registry: each ZdogDiceGroup registers its diceRef so
// waitForAllDiceLanded() can poll across both house + player groups.
const _diceGroups = [];
function _registerDiceGroup(diceArr) {
  _diceGroups.push(diceArr);
  return () => { const idx = _diceGroups.indexOf(diceArr); if (idx >= 0) _diceGroups.splice(idx, 1); };
}
function waitForAllDiceLanded(timeout = 3000) {
  return new Promise(resolve => {
    const start = Date.now();
    function check() {
      const allIdle = _diceGroups.every(arr =>
        arr.every(d => !d.visible || d.phase === "idle")
      );
      if (allIdle) { resolve(); return; }
      if (Date.now() - start > timeout) {
        // Force-snap any stuck dice on timeout
        _diceGroups.forEach(arr => arr.forEach(d => {
          if (d.visible && d.phase !== "idle") {
            d.phase = "idle";
            if (d.targetValue) {
              const target = FACE_ROTATIONS[d.targetValue];
              d.anchor.rotate.x = target.x; d.anchor.rotate.y = target.y; d.anchor.rotate.z = target.z;
            }
            d.anchor.translate.y = 0;
          }
        }));
        resolve();
        return;
      }
      setTimeout(check, 50);
    }
    check();
  });
}

/* ── Dice Group (wrapper) ── */
function DiceGroup({ dice, color, rolling, rollSeq }) {
  return <div className="dice-row"><ZdogDiceGroup dice={dice} rolling={rolling} rollSeq={rollSeq}/></div>;
}

/* ── Arena Layout Component ── */
function ArenaLayout({ phase, result, dDice, pDice, dc, rolling, rollSeq, doubleMode, pot, insufficientToast, roundToast, currSymbol, originalBet, cashoutAlert, setCashoutAlert }) {
  const houseOn = (phase==="result"||result)&&dDice[0];
  const houseTotal = dDice[0]+dDice[1];
  const houseChipClass = houseOn ? (result==="win"?"chip--negative":result==="lose"?"chip--positive":"chip--primary") : "chip--neutral";

  const playerOn = (phase==="result"||result)&&result;
  const playerTotal = pDice.reduce((a,b)=>a+b,0);
  const playerChipClass = playerOn ? (result==="win"?"chip--positive":result==="lose"?"chip--negative":"chip--primary") : "chip--neutral";

  return (
    <div className="arena-layout">
      {/* House dice */}
      <div className="arena-dice arena-dice--top">
        <DiceGroup dice={dDice} color="white" rolling={rolling} rollSeq={rollSeq}/>
      </div>

      {/* Table with labels on edges */}
      <div className="arena-table">
        <img className="arena-table-bg" src="table.svg" alt=""/>
        <img className="arena-table-logo" src="watermark.svg" alt="SETANTAbet"/>
        {insufficientToast && (
          <div className="insufficient-toast">
            <div className="m-bet-currency">{currSymbol}</div>
            <span>{t("toast.insufficient_balance")}</span>
          </div>
        )}
        {roundToast && (
          <div className="round-toast">
            <span className={`round-toast-chip round-toast-chip--${roundToast.type}`}>
              {roundToast.type==="win"?t("toast.win"):roundToast.type==="lose"?t("toast.lost"):t("toast.push")}
            </span>
            <span className="round-toast-detail">{roundToast.text}</span>
          </div>
        )}
        {doubleMode && phase==="result" && result==="win" && (
          <div className="round-toast">
            <span className="round-toast-chip round-toast-chip--win">{t("toast.win")}</span>
            <span className="round-toast-detail">{t("toast.pot_raised", {amount: currSymbol+pot.toFixed(2)})}</span>
          </div>
        )}
        {doubleMode && phase==="result" && result==="tie" && (
          <div className="round-toast">
            <span className="round-toast-chip round-toast-chip--push">{t("toast.push")}</span>
            <span className="round-toast-detail">{t("toast.pot_kept", {amount: currSymbol+pot.toFixed(2)})}</span>
          </div>
        )}
        {cashoutAlert && (
          <div className="cashout-alert-backdrop" onClick={()=>setCashoutAlert(null)}>
            <div className={`cashout-alert${(cashoutAlert.mult >= 10 || cashoutAlert.big) ? " cashout-alert--big" : ""}`} onClick={e=>e.stopPropagation()}>
              {(cashoutAlert.mult >= 10 || cashoutAlert.big) && (
                <div style={{display:"flex",flexDirection:"column",alignItems:"center",gap:1}}>
                  <svg width="24" height="24" viewBox="0 0 24 24" fill="none"><g clipPath="url(#bwi)"><path d="M15.527 16.255c-.667 0-1.15-.483-1.15-1.15v-3.842h-3.131c-.667 0-1.15-.482-1.15-1.149 0-.245.106-.456.193-.622l3.193-4.404H6.369c-2.088 0-3.728 1.64-3.728 3.728v7.456c0 2.088 1.64 3.728 3.728 3.728h7.456c2.088 0 3.728-1.64 3.728-3.728v-2l-1.105 1.517c-.202.202-.518.465-.921.465zM7.114 16.64a1.114 1.114 0 110-2.237 1.114 1.114 0 010 2.237zm0-5.956a1.123 1.123 0 110-2.246 1.123 1.123 0 010 2.246zm5.965 5.956a1.114 1.114 0 110-2.237 1.114 1.114 0 010 2.237zm1.439-11.482L11.035 9.948s-.061.122-.061.166c0 .184.088.272.272.272h1.088a1.114 1.114 0 011.746 0h1.175v4.72c0 .184.088.271.272.271.035 0 .14-.044.263-.158l1.763-2.43V8.816c0-1.851-1.29-3.351-3.035-3.658z" fill="#D9D9D9"/><path d="M21.501 7.263c0 .044-.061.158-.061.167l-3.886 5.36-1.763 2.43c-.123.113-.228.157-.263.157-.184 0-.272-.088-.272-.272v-4.72h-4.01c-.184 0-.271-.087-.271-.271 0-.044.061-.158.061-.167l3.483-4.79 2.21-3.052A.265.265 0 0117.01 2c.026 0 .044.009.062.018a.285.285 0 01.149.254v4.72h4.009c.184 0 .272.087.272.271z" fill="#FFD106"/></g><defs><clipPath id="bwi"><rect width="18.86" height="18" fill="white" transform="translate(2.64 2)"/></clipPath></defs></svg>
                  <span className="cashout-alert-label">{t("alert.big_win")}</span>
                </div>
              )}
              <div className="cashout-alert-mult">{cashoutAlert.amount.toFixed(2)} {currSymbol}</div>
              <div className="cashout-alert-chip">{cashoutAlert.mult}X</div>
            </div>
          </div>
        )}
        <div className="arena-edge-label arena-edge-label--top">
          <span className="chip chip--xs chip--neutral">{t("arena.house")}</span>
          <span className={`chip chip--sm ${houseChipClass}`}>{houseOn ? houseTotal : 0}</span>
        </div>
        <div className="arena-edge-label arena-edge-label--bottom">
          <span className="chip chip--xs chip--neutral">{t("arena.you")}</span>
          <span className={`chip chip--sm ${playerChipClass}`}>{playerOn ? playerTotal : 0}</span>
        </div>
      </div>

      {/* Player dice */}
      <div className="arena-dice arena-dice--bottom">
        <DiceGroup dice={phase==="result" ? pDice : Array.from({length:dc},(_,i)=>pDice[i])} color="white" rolling={rolling} rollSeq={rollSeq}/>
      </div>
    </div>
  );
}

/* ── Drawer (bottom sheet) ── */
function Drawer({ title, onClose, children }) {
  return (
    <div className="drawer-backdrop" onClick={onClose}>
      <div className="drawer" onClick={e=>e.stopPropagation()}>
        <div className="drawer-header">
          <span className="drawer-title">{title}</span>
          <button className="drawer-close" onClick={onClose}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none">
              <path d="M5 5L15 15M15 5L5 15" stroke="#FFFFFF" strokeWidth="2" strokeLinecap="round"/>
            </svg>
          </button>
        </div>
        <div className="drawer-content">
          {children}
        </div>
      </div>
    </div>
  );
}

/* ── Legacy Modal (desktop fallback) ── */
function Modal({ title, onClose, children }) {
  return (
    <div className="overlay" onClick={onClose}>
      <div className="modal" style={{textAlign:"left"}} onClick={e=>e.stopPropagation()}>
        <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:16}}>
          <h2 style={{fontSize:16,fontWeight:900}}>{title}</h2>
          <button className="icon-btn" onClick={onClose} style={{background:"var(--surface-default)"}}>
            <svg viewBox="0 0 16 16" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round">
              <line x1="4" y1="4" x2="12" y2="12"/><line x1="12" y1="4" x2="4" y2="12"/>
            </svg>
          </button>
        </div>
        {children}
      </div>
    </div>
  );
}

/* ── PF Icons (inline SVG) ── */
function PfIconPadlock() { return <svg viewBox="0 0 20 20" fill="none"><path d="M14.166 7.5V5.833c0-2.333-1.833-4.166-4.166-4.166S5.833 3.5 5.833 5.833V7.5C4.417 7.5 3.333 8.583 3.333 10v5.833c0 1.417 1.084 2.5 2.5 2.5h8.334c1.416 0 2.5-1.083 2.5-2.5V10c0-1.417-1.084-2.5-2.5-2.5zM7.5 5.833c0-1.416 1.083-2.5 2.5-2.5s2.5 1.084 2.5 2.5V7.5h-5V5.833zm3.417 7.084l-.084.083V14.167c0 .5-.333.833-.833.833s-.833-.333-.833-.833V13c-.5-.5-.584-1.25-.084-1.75s1.25-.584 1.75-.084c.5.417.584 1.25.084 1.75z" fill="#171717"/></svg>; }
function PfIconBolt() { return <svg viewBox="0 0 20 20" fill="none"><path d="M15.833 7.5H11.666V2.5c0-.25-.166-.5-.333-.667-.333-.25-.917-.167-1.167.167L3.5 11.166c-.084.167-.167.334-.167.5 0 .5.334.834.834.834h4.166v5c0 .5.334.833.834.833.25 0 .5-.166.666-.333l6.667-9.167c.083-.166.166-.333.166-.5 0-.5-.333-.833-.833-.833z" fill="#171717"/></svg>; }
function PfIconEye() { return <svg viewBox="0 0 20 20" fill="none"><path d="M10.417 7.5c-1.417 0-2.5 1.083-2.5 2.5s1.083 2.5 2.5 2.5 2.5-1.083 2.5-2.5-1.083-2.5-2.5-2.5zm8.25 2.167C17 5.75 13.834 3.333 10.417 3.333S3.834 5.75 2.167 9.667c-.083.25-.083.416 0 .666 1.667 3.917 4.833 6.334 8.25 6.334s6.583-2.417 8.25-6.334c.083-.25.083-.416 0-.666zM10.417 14.167c-2.334 0-4.167-1.834-4.167-4.167s1.833-4.167 4.167-4.167 4.166 1.834 4.166 4.167-1.833 4.167-4.166 4.167z" fill="#171717"/></svg>; }
function PfIconRefresh() { return <svg viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round"><path d="M4 12a8 8 0 018-8v4l4-4-4-4v4a8 8 0 100 16v-4l-4 4 4 4v-4a8 8 0 010-16z"/></svg>; }
function PfIconDice() { return <svg viewBox="0 0 16 16" fill="none"><path d="M11.333 1.333H4.666C2.8 1.333 1.333 2.8 1.333 4.666v6.667c0 1.867 1.467 3.334 3.333 3.334h6.667c1.867 0 3.334-1.467 3.334-3.334V4.666c0-1.866-1.467-3.333-3.334-3.333zM5.666 12.333c-.533 0-1-.467-1-1s.467-1 1-1 1 .467 1 1-.467 1-1 1zm0-3.333c-.533 0-1-.467-1-1s.467-1 1-1 1 .467 1 1-.467 1-1 1zm0-3.334c-.533 0-1-.466-1-1 0-.533.467-1 1-1s1 .467 1 1c0 .534-.467 1-1 1zm4.667 6.667c-.533 0-1-.467-1-1s.467-1 1-1 1 .467 1 1-.467 1-1 1zm0-3.333c-.533 0-1-.467-1-1s.467-1 1-1 1 .467 1 1-.467 1-1 1zm0-3.334c-.533 0-1-.466-1-1 0-.533.467-1 1-1s1 .467 1 1c0 .534-.467 1-1 1z" fill="white"/></svg>; }
function PfIconShield() { return <svg viewBox="0 0 20 20" fill="none"><path d="M6.945 10L9.167 12.222 13.333 8.056M10 1.667L3.056 4.445v5c0 4 2.889 7.333 6.944 8.611 4.056-1.278 6.945-4.611 6.945-8.611v-5L10 1.667z" stroke="#4ADE80" strokeWidth="1.778" strokeLinecap="round" strokeLinejoin="round"/></svg>; }
function PfIconCopy() { return <svg viewBox="0 0 16 16" fill="none"><path d="M10.667 5.333H13.6L10 1.733v2.934c0 .4.267.666.667.666zm0 1.334C9.533 6.667 8.667 5.8 8.667 4.667V1.333H6.667c-1.134 0-2 .867-2 2V4H4c-1.133 0-2 .867-2 2v6.667c0 1.133.867 2 2 2h5.333c1.134 0 2-.867 2-2V12H12c1.133 0 2-.867 2-2V6.667h-3.333zM10 12.667c0 .4-.267.666-.667.666H4c-.4 0-.667-.266-.667-.666V6c0-.4.267-.667.667-.667h.667V10c0 1.133.866 2 2 2H10v.667z" fill="white"/></svg>; }

function VerifyTab() {
  const [serverSeed, setServerSeed] = useState("");
  const [cSeed, setCSeed] = useState("");
  const [diceCount, setDiceCount] = useState(1);
  const [result, setResult] = useState(null);
  const [loading, setLoading] = useState(false);
  const [copied, setCopied] = useState(false);

  const doVerify = async () => {
    if (!serverSeed || !cSeed) return;
    setLoading(true);
    try {
      const r = await api("/api/v1/verify", {method:"POST", body:{server_seed:serverSeed, client_seed:cSeed, nonce:0, dice_count:diceCount}});
      setResult(r);
    } catch(e) { setResult({error:t("verify.failed")}); }
    setLoading(false);
  };

  const doCopy = (text) => {
    navigator.clipboard.writeText(text).then(() => { setCopied(true); setTimeout(()=>setCopied(false), 1500); });
  };

  return (
    <div className="pf-content">
      <div className="pf-section">
        <div className="pf-title">{t("verify.title")}</div>
        <div className="pf-desc" style={{fontSize:14}}>{t("verify.desc")}</div>
      </div>
      <div style={{display:"flex",flexDirection:"column",gap:12}}>
        <div className="pf-input">
          <div className="pf-input-text">
            <input className="pf-input-value" value={serverSeed} onChange={e=>setServerSeed(e.target.value)} placeholder={t("verify.ph_server_seed")}/>
          </div>
        </div>
        <div className="pf-input">
          <div className="pf-input-text">
            <input className="pf-input-value" value={cSeed} onChange={e=>setCSeed(e.target.value)} placeholder={t("verify.ph_client_seed")}/>
          </div>
        </div>
      </div>
      <div className="pf-section">
        <div className="pf-title">{t("verify.dice_count")}</div>
        <div className="pf-desc" style={{fontSize:14}}>{t("verify.dice_count_desc")}</div>
      </div>
      <div style={{display:"flex",flexDirection:"column",gap:12}}>
        <div className="pf-dice-chips">
          {[1,2,3].map(n=>(
            <button key={n} className={"pf-dice-chip"+(diceCount===n?" pf-dice-chip--active":"")} onClick={()=>setDiceCount(n)}>
              <PfIconDice/> {t("risk.dice", {n})}
            </button>
          ))}
        </div>
        <button className="pf-btn" onClick={doVerify} disabled={loading||!serverSeed||!cSeed}>
          {loading?t("verify.verifying"):t("verify.verify_round")}
        </button>
      </div>
      {result && !result.error && (
        <div className="pf-result">
          <div className="pf-result-header">
            <div className="pf-result-status">
              <div className="pf-result-title">{t("verify.verified_title")}</div>
              <div className="pf-result-sub">{t("verify.verified_sub")}</div>
            </div>
            <div className="pf-result-shield"><PfIconShield/></div>
          </div>
          <div className="pf-result-hash">
            <div className="pf-result-hash-text">
              <span className="pf-result-hash-label">{t("verify.hash")}</span>
              <span className="pf-result-hash-value">{serverSeed.slice(0,6)}...{serverSeed.slice(-6)}</span>
            </div>
            <div className="pf-result-copy" onClick={()=>doCopy(serverSeed)}><PfIconCopy/></div>
          </div>
          <div className="pf-result-body">
            <div className="pf-result-row">
              <div className="pf-result-row-left">
                <span className="pf-result-label">{t("verify.dealer")}</span>
                <span className="pf-result-value">[{result.dealer_dice.join(", ")}]</span>
              </div>
              <span className="pf-result-value">= {result.dealer_total}</span>
            </div>
            <div className="pf-result-row">
              <div className="pf-result-row-left">
                <span className="pf-result-label">{t("verify.player")}</span>
                <span className="pf-result-value">[{result.player_dice.join(", ")}]</span>
              </div>
              <span className="pf-result-value">= {result.player_total}</span>
            </div>
          </div>
        </div>
      )}
      {result && result.error && (
        <div className="pf-result pf-result--error">
          <div className="pf-result-header">
            <div className="pf-result-status">
              <div className="pf-result-title">{result.error}</div>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

function FairDrawer({ onClose, seedHash, clientSeed, onSetSeed, onRefreshSeed, useNonce, onToggleNonce, clientNonce }) {
  const [tab, setTab] = useState("overview");
  const [newSeed, setNewSeed] = useState(clientSeed);
  const doSetSeed = async () => { try { await api("/api/v1/client-seed",{method:"POST",body:{client_seed:newSeed}}); onSetSeed(newSeed); } catch(e){} };
  const doRandomize = () => { const arr=new Uint8Array(16);crypto.getRandomValues(arr);setNewSeed(Array.from(arr,b=>b.toString(16).padStart(2,"0")).join("")); };

  return (
    <Drawer title={t("drawer.provably_fair")} onClose={onClose}>
      <div className="drawer-tabs">
        {[{id:"overview",label:t("fair.tab_overview")},{id:"seeds",label:t("fair.tab_seeds")},{id:"verify",label:t("fair.tab_verify")}].map(tb=>(
          <button key={tb.id} className={"drawer-tab"+(tab===tb.id?" drawer-tab--active":"")} onClick={()=>setTab(tb.id)}>{tb.label}</button>
        ))}
      </div>

      {tab==="overview" && (
        <div className="pf-content">
          <div className="pf-section">
            <div className="pf-title">{t("fair.how_title")}</div>
            <div className="pf-desc">{t("fair.how_desc")}</div>
          </div>
          <div className="pf-steps">
            <div className="pf-step">
              <div className="pf-step-icon"><PfIconPadlock/></div>
              <div className="pf-step-text">{t("fair.step1")}</div>
            </div>
            <div className="pf-step-arrow"><div className="pf-step-arrow-line"/></div>
            <div className="pf-step">
              <div className="pf-step-icon"><PfIconBolt/></div>
              <div className="pf-step-text">{t("fair.step2")}</div>
            </div>
            <div className="pf-step-arrow"><div className="pf-step-arrow-line"/></div>
            <div className="pf-step">
              <div className="pf-step-icon"><PfIconEye/></div>
              <div className="pf-step-text">{t("fair.step3")}</div>
            </div>
          </div>
        </div>
      )}

      {tab==="seeds" && (
        <div className="pf-content">
          <div className="pf-section">
            <div className="pf-title">{t("fair.how_title")}</div>
            <div className="pf-desc">{t("fair.how_desc")}</div>
          </div>
          <div style={{display:"flex",flexDirection:"column",gap:12}}>
            <div className="pf-input">
              <div className="pf-input-text">
                <span className="pf-input-label">{t("fair.active_seed")}</span>
                <input className="pf-input-value pf-input-value--mono" value={useNonce?newSeed+"-"+clientNonce:newSeed} onChange={e=>{if(!useNonce)setNewSeed(e.target.value)}} readOnly={useNonce}/>
              </div>
              <button className="pf-input-btn" onClick={doRandomize}>
                <svg viewBox="0 0 16 16" fill="none"><path d="M11.333 2.2C8.733.733 5.533 1.2 3.4 3.133V2c0-.4-.267-.667-.667-.667S2.067 1.6 2.067 2v3c0 .4.266.667.666.667h3c.4 0 .667-.267.667-.667s-.267-.667-.667-.667H4.133C5.133 3.267 6.533 2.667 8 2.667c2.933 0 5.333 2.4 5.333 5.333 0 .4.267.667.667.667s.667-.267.667-.667c0-2.4-1.267-4.6-3.334-5.8zM13.266 10.333h-3c-.4 0-.666.267-.666.667s.266.667.666.667h1.6c-1 1.066-2.4 1.666-3.866 1.666-2.934 0-5.334-2.4-5.334-5.333 0-.4-.266-.667-.666-.667s-.667.267-.667.667C1.333 11.667 4.333 14.667 8 14.667c1.733 0 3.333-.667 4.6-1.867V14c0 .4.266.667.666.667s.667-.267.667-.667v-3c0-.4-.334-.667-.667-.667z" fill="white"/></svg>
              </button>
            </div>
            <div className="pf-input">
              <div className="pf-input-text">
                <span className="pf-input-label">{t("fair.seed_hash")}</span>
                <div className="pf-input-value pf-input-value--green pf-input-value--mono" style={{wordBreak:"break-all",fontSize:12}}>{seedHash||t("common.loading")}</div>
              </div>
            </div>
            <button className="pf-btn" onClick={doSetSeed}>{t("fair.save")}</button>
          </div>
        </div>
      )}

      {tab==="verify" && <VerifyTab/>}
    </Drawer>
  );
}


function HistoryDrawer({ onClose, clientSeed }) {
  const [rounds, setRounds] = useState([]);
  const [loading, setLoading] = useState(true);
  const [expanded, setExpanded] = useState(null);
  const [verifyResults, setVerifyResults] = useState({});
  useEffect(()=>{api("/api/v1/history?limit=50").then(d=>{setRounds(d.rounds||[]);setLoading(false)}).catch(()=>setLoading(false))},[]);

  const doVerify = async (r, i) => {
    // /history nests dice_count under game_data; r.dice_count itself is
    // always undefined, so sending it directly produced a silent 400 in
    // the backend and the Verified panel rendered with stale dice from
    // whatever verify previously succeeded. Read the right field, and
    // surface a real error instead of swallowing the rejection.
    const diceCount = (r.game_data && r.game_data.dice_count) || r.dice_count || 1;
    const clientSeedUsed = r.client_seed || clientSeed;
    try {
      const result = await api("/api/v1/verify", {method:"POST", body:{
        server_seed: r.server_seed, client_seed: clientSeedUsed, nonce: 0, dice_count: diceCount
      }});
      setVerifyResults(prev => ({...prev, [i]: result}));
    } catch(e) {
      setVerifyResults(prev => ({...prev, [i]: { error: e.message || t("verify.failed") }}));
    }
  };

  const HistDice = ({win,push}) => <svg viewBox="0 0 24 24" fill="none"><path d="M18 0H6C2.64 0 0 2.64 0 6v12c0 3.36 2.64 6 6 6h12c3.36 0 6-2.64 6-6V6c0-3.36-2.64-6-6-6zM7.2 18.6c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8zM7.2 9c-.96 0-1.8-.84-1.8-1.8S6.24 5.4 7.2 5.4 9 6.24 9 7.2 8.16 9 7.2 9zm4.8 4.8c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8zm5.8 4.8c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8zm0-9.6c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8z" fill={push?"#FFD106":win?"#22c55e":"#f43f5e"}/></svg>;
  const ChevDown = () => <svg viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="#d4d4d4" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>;
  const ChevUp = () => <svg viewBox="0 0 16 16" fill="none"><path d="M4 10l4-4 4 4" stroke="#d4d4d4" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/></svg>;

  return (
    <Drawer title={t("drawer.bet_history")} onClose={onClose}>
      {loading ? (
        <div className="hist-empty">{t("common.loading")}</div>
      ) : rounds.length === 0 ? (
        <div className="hist-empty">{t("history.empty")}</div>
      ) : (
        <div className="hist-list">
          {rounds.map((r, i) => {
            const isWin = r.outcome === "win" || r.outcome === "cashout";
            const isPush = r.outcome === "tie";
            const isExp = expanded === i;
            const vr = verifyResults[i];
            const gd = r.game_data || {};
            const dc = r.dice_count || gd.dice_count || 1;
            const rolls = r.rolls || gd.step || 1;
            const mult = r.multiplier || gd.multiplier || "0.00";
            const profit = +(r.profit || 0);
            const cost = +(r.total_cost || 0);
            return (
              <div key={i} className={"hist-item"+(isExp?" hist-item--expanded":"")} onClick={()=>setExpanded(isExp?null:i)}>
                <div className="hist-header">
                  <div className="hist-left">
                    <div className="hist-icon"><HistDice win={isWin} push={isPush}/></div>
                    <div className="hist-info">
                      <span className="hist-info-title">{dc} {dc===1?t("history.die"):t("history.dice")}</span>
                      <span className="hist-info-sub">${cost.toFixed(2)} - {rolls} {rolls>1?t("history.rolls"):t("history.roll")}</span>
                    </div>
                  </div>
                  <div className="hist-right">
                    <div className="hist-chips">
                      <span className="hist-mult-chip">{mult} X</span>
                      <span className={"hist-payout-chip "+(isPush?"hist-payout-chip--push":isWin?"hist-payout-chip--win":"hist-payout-chip--lose")}>
                        {isPush ? t("history.push") : isWin ? "$ "+profit.toFixed(2) : "- $ "+Math.abs(profit).toFixed(2)}
                      </span>
                    </div>
                    <div className="hist-chevron">{isExp ? <ChevUp/> : <ChevDown/>}</div>
                  </div>
                </div>
                {isExp && (
                  <div className="hist-details" onClick={e=>e.stopPropagation()}>
                    <div className="hist-details-inner">
                      <div className="hist-seeds">
                        <div className="hist-seed-group">
                          <span className="hist-seed-label">{t("history.server_seed")}</span>
                          <span className="hist-seed-value">{r.server_seed || "—"}</span>
                        </div>
                        <div className="hist-seed-group">
                          <span className="hist-seed-label">{t("fair.seed_hash")}</span>
                          <span className="hist-seed-value">{r.seed_hash || "—"}</span>
                        </div>
                      </div>
                      {!vr ? (
                        <button className="hist-verify-btn" onClick={()=>doVerify(r,i)}>
                          {t("verify.verify_round")}
                        </button>
                      ) : vr.error ? (
                        <button className="hist-verify-btn" onClick={()=>doVerify(r,i)} style={{color:"var(--red)"}}>
                          {t("history.verify_failed", {error: vr.error})}
                        </button>
                      ) : (
                        <>
                          <button className="hist-verify-btn" disabled>
                            <PfIconShield/> {t("history.verified")}
                          </button>
                          <div className="pf-result">
                            <div className="pf-result-header">
                              <div className="pf-result-status">
                                <div className="pf-result-title">{t("verify.verified_title")}</div>
                                <div className="pf-result-sub">{t("verify.verified_sub")}</div>
                              </div>
                            </div>
                            <div className="pf-result-body">
                              <div className="pf-result-row">
                                <div className="pf-result-row-left">
                                  <span className="pf-result-label">{t("verify.dealer")}</span>
                                  <span className="pf-result-value">[{(vr.dealer_dice||[]).join(", ")}]</span>
                                </div>
                                <span className="pf-result-value">= {vr.dealer_total}</span>
                              </div>
                              <div className="pf-result-row">
                                <div className="pf-result-row-left">
                                  <span className="pf-result-label">{t("verify.player")}</span>
                                  <span className="pf-result-value">[{(vr.player_dice||[]).join(", ")}]</span>
                                </div>
                                <span className="pf-result-value">= {vr.player_total}</span>
                              </div>
                            </div>
                          </div>
                        </>
                      )}
                    </div>
                  </div>
                )}
              </div>
            );
          })}
        </div>
      )}
    </Drawer>
  );
}

function InfoDrawer({ onClose, odds, config }) {
  const o1=odds["1"]||{},o2=odds["2"]||{},o3=odds["3"]||{};
  const DiceIcon = () => <svg viewBox="0 0 24 24" fill="none"><path d="M18 0H6C2.64 0 0 2.64 0 6v12c0 3.36 2.64 6 6 6h12c3.36 0 6-2.64 6-6V6c0-3.36-2.64-6-6-6zM7.2 18.6c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8zM7.2 9c-.96 0-1.8-.84-1.8-1.8S6.24 5.4 7.2 5.4 9 6.24 9 7.2 8.16 9 7.2 9zm4.8 4.8c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8zm5.8 4.8c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8zm0-9.6c-.96 0-1.8-.84-1.8-1.8s.84-1.8 1.8-1.8 1.8.84 1.8 1.8-.84 1.8-1.8 1.8z" fill="#d4d4d4"/></svg>;
  const RiskLow = () => <svg viewBox="0 0 16 16" fill="none"><rect x="2" y="10" width="3" height="4" rx="1.5" fill="#22C55E"/><rect x="6.5" y="6" width="3" height="8" rx="1.5" fill="#404040"/><rect x="11" y="2" width="3" height="12" rx="1.5" fill="#404040"/></svg>;
  const RiskMed = () => <svg viewBox="0 0 16 16" fill="none"><rect x="2" y="10" width="3" height="4" rx="1.5" fill="#FFD106"/><rect x="6.5" y="6" width="3" height="8" rx="1.5" fill="#FFD106"/><rect x="11" y="2" width="3" height="12" rx="1.5" fill="#404040"/></svg>;
  const RiskHigh = () => <svg viewBox="0 0 16 16" fill="none"><rect x="2" y="10" width="3" height="4" rx="1.5" fill="#F43F5E"/><rect x="6.5" y="6" width="3" height="8" rx="1.5" fill="#F43F5E"/><rect x="11" y="2" width="3" height="12" rx="1.5" fill="#F43F5E"/></svg>;

  return (
    <Drawer title={t("drawer.how_to_play")} onClose={onClose}>
      <div className="htp-content">
        {/* Intro */}
        <div className="htp-intro">
          {t("htp.intro")}
        </div>

        {/* Place Your Bet and Roll */}
        <div className="htp-card">
          <div className="htp-card-title">{t("htp.card1_title")}</div>

          <div style={{display:"flex",flexDirection:"column",gap:16}}>
            <div className="htp-previews">
              {/* Cashout preview */}
              <div className="htp-preview">
                <div className="htp-preview-img">
                  <div className="htp-preview-table">
                    <img src="table.svg" alt="" className="htp-preview-table-bg"/>
                    <img src="watermark.svg" alt="" className="htp-preview-table-logo"/>
                  </div>
                  <div className="htp-preview-btn htp-preview-btn--green">
                    <DiceIcon/> {t("htp.preview_cashout_btn")}
                  </div>
                </div>
                <div className="htp-preview-info">
                  <div className="htp-preview-texts">
                    <span className="htp-preview-label htp-preview-label--green">{t("htp.cashout")}</span>
                    <span className="htp-preview-desc">{t("htp.cashout_desc")}</span>
                  </div>
                  <span className="htp-chip htp-chip--green">{t("htp.payout_chip", {mult: o3.multiplier||"1.15"})}</span>
                </div>
              </div>

              {/* Double preview */}
              <div className="htp-preview">
                <div className="htp-preview-img">
                  <div className="htp-preview-table">
                    <img src="table.svg" alt="" className="htp-preview-table-bg"/>
                    <img src="watermark.svg" alt="" className="htp-preview-table-logo"/>
                  </div>
                  <div className="htp-preview-btn htp-preview-btn--yellow">
                    <DiceIcon/> {t("htp.preview_double_btn")}
                  </div>
                </div>
                <div className="htp-preview-info">
                  <div className="htp-preview-texts">
                    <span className="htp-preview-label htp-preview-label--yellow">{t("htp.double")}</span>
                    <span className="htp-preview-desc">{t("htp.double_desc")}</span>
                  </div>
                  <span className="htp-chip htp-chip--yellow">{t("htp.roll_again")}</span>
                </div>
              </div>
            </div>

            {/* Stats table */}
            <div className="htp-stats">
              <div className="htp-stat-row">
                <span className="htp-stat-label">{t("htp.min_bet")}</span>
                <span className="htp-stat-value">$ {config.min_bet||"0.10"}</span>
              </div>
              <div className="htp-stat-row">
                <span className="htp-stat-label">{t("htp.max_bet")}</span>
                <span className="htp-stat-value">$ {(+config.max_bet||100).toFixed(2)}</span>
              </div>
              <div className="htp-stat-row">
                <span className="htp-stat-label">{t("htp.rtp")}</span>
                <span className="htp-stat-value htp-stat-value--green">96.5 %</span>
              </div>
            </div>
          </div>

          {/* Warning */}
          <div className="htp-warning">
            {t("htp.warning_pre")}<span className="htp-warning-red">{t("htp.warning_red")}</span>{t("htp.warning_post")}
          </div>
        </div>

        {/* Risk Levels */}
        <div className="htp-card" style={{padding:12,gap:12}}>
          <div className="htp-card-title">{t("htp.risk_levels")}</div>

          <div className="htp-risk-cards">
            {/* Low risk - 3 dice */}
            <div className="htp-risk-card">
              <div className="htp-risk-dice">
                <DiceIcon/><DiceIcon/><DiceIcon/>
              </div>
              <div className="htp-risk-chip">
                <div className="htp-risk-chip-icon"><RiskLow/></div>
                <div className="htp-risk-chip-text">
                  <span className="htp-risk-chip-mult">{o3.multiplier||"1.15"}x</span>
                  <span className="htp-risk-chip-label">{t("htp.low")}</span>
                </div>
              </div>
            </div>

            {/* Medium risk - 2 dice */}
            <div className="htp-risk-card">
              <div className="htp-risk-dice">
                <DiceIcon/><DiceIcon/>
              </div>
              <div className="htp-risk-chip">
                <div className="htp-risk-chip-icon"><RiskMed/></div>
                <div className="htp-risk-chip-text">
                  <span className="htp-risk-chip-mult">{o2.multiplier||"1.92"}x</span>
                  <span className="htp-risk-chip-label">{t("htp.medium")}</span>
                </div>
              </div>
            </div>

            {/* High risk - 1 die */}
            <div className="htp-risk-card">
              <div className="htp-risk-dice">
                <DiceIcon/>
              </div>
              <div className="htp-risk-chip">
                <div className="htp-risk-chip-icon"><RiskHigh/></div>
                <div className="htp-risk-chip-text">
                  <span className="htp-risk-chip-mult">{o1.multiplier||"9.67"}x</span>
                  <span className="htp-risk-chip-label">{t("htp.high")}</span>
                </div>
              </div>
            </div>
          </div>
        </div>

        {/* Provably Fair footer */}
        <div className="htp-footer">
          <div className="htp-footer-icon"><PfIconShield/></div>
          <div className="htp-footer-text">
            {t("htp.footer_pre")}<strong>{t("drawer.provably_fair")}</strong>{t("htp.footer_post")}
          </div>
        </div>
      </div>
    </Drawer>
  );
}



/* ── Free Round Modals ── */
function FreeRoundWelcomeModal({ total, betAmount, currencySymbol, onStart }) {
  return (
    <div className="overlay free-round-welcome" onClick={e=>e.stopPropagation()}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div style={{textAlign:"center",padding:"24px 16px"}}>
          <img src="loading-logo.svg" alt="" style={{width:80,height:70,marginBottom:16}}/>
          <h2 style={{fontSize:22,fontWeight:900,marginBottom:8}}>{t("fr.welcome_title")}</h2>
          <p style={{fontSize:16,color:"var(--tx2)",marginBottom:6}}>{t("fr.welcome_received_pre")}<strong style={{color:"#FFD106"}}>{t("fr.free_rounds_n", {n: total})}</strong></p>
          <p style={{fontSize:14,color:"var(--tx3)",marginBottom:24}}>{t("fr.bet_amount_pre")}<strong style={{color:"var(--tx)"}}>{currencySymbol}{betAmount || "—"}</strong>{t("fr.per_round")}</p>
          <button onClick={onStart}
            style={{width:"100%",height:48,borderRadius:12,background:"#FFD106",color:"#171717",border:"none",fontWeight:800,fontSize:16,cursor:"pointer",fontFamily:"var(--font-primary)",boxShadow:"0 4px 16px rgba(255,209,6,0.25)"}}>
            {t("fr.start")}
          </button>
        </div>
      </div>
    </div>
  );
}

function FreeRoundCompleteModal({ total, winnings, currencySymbol, onContinue }) {
  const isPositive = winnings >= 0;
  return (
    <div className="overlay free-round-complete" onClick={e=>e.stopPropagation()}>
      <div className="modal" onClick={e=>e.stopPropagation()}>
        <div style={{textAlign:"center",padding:"24px 16px"}}>
          <img src="loading-logo.svg" alt="" style={{width:80,height:70,marginBottom:16}}/>
          <h2 style={{fontSize:22,fontWeight:900,marginBottom:8}}>{t("fr.complete_title")}</h2>
          <p style={{fontSize:14,color:"var(--tx2)",marginBottom:6}}>{t("fr.played_pre")}<strong style={{color:"var(--tx)"}}>{t("fr.rounds_n", {n: total})}</strong></p>
          <p style={{fontSize:24,fontWeight:900,marginBottom:24,color:isPositive?"var(--green)":"var(--red)"}}>
            {t("fr.total_winnings")} {isPositive?"+":""}{currencySymbol}{Math.abs(winnings).toFixed(2)}
          </p>
          <button onClick={onContinue}
            style={{width:"100%",height:48,borderRadius:12,background:"#FFD106",color:"#171717",border:"none",fontWeight:800,fontSize:16,cursor:"pointer",fontFamily:"var(--font-primary)",boxShadow:"0 4px 16px rgba(255,209,6,0.25)"}}>
            {t("fr.continue")}
          </button>
        </div>
      </div>
    </div>
  );
}

function FreeRoundCounter({ used, total, winnings }) {
  return (
    <div className="free-round-counter">
      <span className="free-round-counter-text">{t("fr.counter", {used, total})}</span>
      <span style={{flex:1}}/>
      <span className="free-round-winnings">{t("fr.total_win_label")} ${winnings.toFixed(2)}</span>
    </div>
  );
}

/* ── Main Game ── */
function Game() {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [balance, setBalance] = useState("0.00");
  const [currency, setCurrency] = useState("USD");
  // Currency symbol — the session currency comes from the API (/state
  // d.currency) and can be any ISO code or crypto ticker the operator
  // runs. Map the common ones to a glyph; fall back to the code itself
  // with a trailing space (e.g. "USDT 10.00") so an unmapped currency
  // still renders sensibly rather than guessing a wrong symbol.
  // Declared up here (not in the render body) so the bet/double handlers
  // below can build toast strings with it.
  const currSymbol = CURRENCY_SYMBOLS[currency] || (currency ? currency + " " : "$");
  const [odds, setOdds] = useState({});
  const [config, setConfig] = useState({});
  const [seedHash, setSeedHash] = useState("");
  const [clientSeed, setClientSeed] = useState("");
  const [useNonce, setUseNonce] = useState(false);
  const [clientNonce, setClientNonce] = useState(0);

  // Bet + dice-count persist in localStorage so they survive refresh /
  // tab close. Clamped against the merchant's live min_bet/max_bet once
  // /state lands (the persisted value might be outside a new merchant's
  // range). Free-round entry still resets to 10 (campaign bet is fixed
  // server-side anyway; that path overrides what the player chose).
  const [bet, setBet] = useState(() => {
    if (typeof localStorage === "undefined") return 1;
    try {
      const saved = JSON.parse(localStorage.getItem("dice_bet_data"));
      if (saved && saved.bet > 0) return saved.bet;
    } catch(e) {}
    return 1;
  });
  const [dc, setDc] = useState(() => {
    if (typeof localStorage === "undefined") return 2;
    const v = +localStorage.getItem("dice_dc");
    return v >= 1 && v <= 3 ? v : 2;
  });
  useEffect(() => { try { localStorage.setItem("dice_bet_data", JSON.stringify({ bet })); } catch(e){} }, [bet]);
  useEffect(() => { try { localStorage.setItem("dice_dc",  String(dc));  } catch(e){} }, [dc]);

  // Keep pDice's length in lockstep with dc (handles the dc selector
  // changing mid-session). The setPDice has its own dep on prev so it
  // no-ops when length already matches and never thrashes the dice
  // graphics for a "same length" change.

  // Pre-size pDice to match dc so the ZdogDiceGroup never sees its
  // dice.length grow mid-roll. If we let pDice start [1] (length 1) and
  // grow to dc on the first /bet response, then setRolling(true) fires
  // ROLLING with the OLD length, only marks die 0 as rolling, and the
  // values-arriving effect can't assign a target to dice >= 1 (their
  // d.phase stays "idle"; the React `rolling` is still true so the
  // snap branch is also gated). Those slots silently freeze at face 1.
  const [rolling, setRolling] = useState(false);
  const [rollSeq, setRollSeq] = useState(0);
  const [phase, setPhase] = useState("idle");
  const [dDice, setDDice] = useState([1,1]);
  // Initial length must match the persisted dc — read localStorage here
  // (not via the useState init for dc above, which already resolved that)
  // so the very first render already has the right number of player dice.
  const [pDice, setPDice] = useState(() => {
    if (typeof localStorage === "undefined") return [1, 1];
    const v = +localStorage.getItem("dice_dc");
    const n = v >= 1 && v <= 3 ? v : 2;
    return new Array(n).fill(1);
  });
  const [result, setResult] = useState(null);
  const resultTimerRef = React.useRef(null);
  const clearResultLater = () => { if(resultTimerRef.current) clearTimeout(resultTimerRef.current); resultTimerRef.current = setTimeout(()=>{ setResult(null); resultTimerRef.current=null; }, 2000); };
  const [recentResults, setRecentResults] = useState([]);

  // Resize pDice if the dc selector changes between rolls. Same reason
  // as the lazy-init above — ZdogDiceGroup's rolling-start snapshots
  // dice.length, so the array MUST already be sized to dc before the
  // next setRolling(true), or the new slot is left forever at face 1.
  useEffect(() => {
    setPDice(prev => prev.length === dc ? prev : new Array(dc).fill(1));
  }, [dc]);

  /* Sequential round state — the double/cashout chain lives on the backend.
     roundId is the active round; pot is the server-reported pot at stake. */
  const [doubleMode, setDoubleMode] = useState(false);
  const [pot, setPot] = useState(0);
  const [roundId, setRoundId] = useState("");
  const [originalBet, setOriginalBet] = useState(0);
  const [doubleDc, setDoubleDc] = useState(1);
  const [currentStep, setCurrentStep] = useState(0);

  /* Free Rounds state */
  const [freeRoundsTotal, setFreeRoundsTotal] = useState(0);
  const [freeRoundsUsed, setFreeRoundsUsed] = useState(0);
  const [freeRoundsActive, setFreeRoundsActive] = useState(false);
  const [freeRoundsWinnings, setFreeRoundsWinnings] = useState(0);
  const [freeRoundsBet, setFreeRoundsBet] = useState("");
  const [showFreeRoundWelcome, setShowFreeRoundWelcome] = useState(false);
  const [showFreeRoundComplete, setShowFreeRoundComplete] = useState(false);
  const [freeRoundsEnded, setFreeRoundsEnded] = useState(false);

  const [drawerOpen, setDrawerOpen] = useState(false);
  const [drawerTab, setDrawerTab] = useState("fair");
  const [showMuted, setShowMuted] = useState(SFX.isMuted());
  const [showMenu, setShowMenu] = useState(false);
  const [cashoutAlert, setCashoutAlert] = useState(null);
  const [insufficientToast, setInsufficientToast] = useState(false);
  const [roundToast, setRoundToast] = useState(null);

  useEffect(() => {
    (async () => {
      const minLoadTime = new Promise(r => setTimeout(r, 2500));
      try {
        await bootstrapDemo();
        const d = await api("/api/v1/state");
        const gameOdds = (d.game_data && d.game_data.odds) || d.odds || {};
        setBalance(d.balance); setCurrency(d.currency); setOdds(gameOdds); setConfig(d.config);
        setSeedHash(d.next_seed_hash); setClientSeed(d.client_seed);

        // Clamp the localStorage-restored bet to this merchant's range —
        // if it persisted from a different merchant (or admin tightened
        // limits), snap it to the boundary instead of letting the player
        // submit an out-of-range bet that the backend will reject.
        if (d.config) {
          const mn = +d.config.min_bet || 0.1;
          const mx = +d.config.max_bet || 100;
          setBet(b => (b < mn ? mn : b > mx ? mx : b));
        }

        // Seed the recent-results dot strip from server-side history so
        // a refresh / new tab doesn't make the strip look empty. Strip
        // shows up to 5 most recent rounds, newest first. Best-effort:
        // if /history fails, we just start with [] and add live rolls
        // as the player plays.
        try {
          const h = await api("/api/v1/history?limit=5");
          const rs = (h && h.rounds) || [];
          // Newest-first; outcome derived from game_data.roll_outcome
          // when present, falling back to profit (positive=win,
          // negative=lose, zero=tie/push) so sequential cashouts that
          // surface as "cashout" still colour the dot green.
          const outcomes = rs.map(r => {
            const ro = r.game_data && r.game_data.roll_outcome;
            if (ro === "win" || ro === "tie" || ro === "lose") return ro;
            const p = +r.profit;
            if (p > 0) return "win";
            if (p < 0) return "lose";
            return "tie";
          }).slice(0, 5);
          setRecentResults(outcomes);
        } catch(e) {}

        await minLoadTime;
        setLoading(false);

        // Resume an in-progress round if the backend reports one for this
        // player + game. Without this a player who closed the browser
        // mid-chain has no way to reach their pot — it sits invisible
        // until the active round expires and gets forfeited. A new bet
        // can't start while a round is open anyway, so do this before
        // the free-round welcome (which would offer to start one).
        if (d.active_round_id) {
          const rs = await fetchRoundState(d.active_round_id);
          if (rs && rs.game_data) {
            const gd = rs.game_data;
            const rolls = gd.rolls || [];
            const last = rolls.length ? rolls[rolls.length - 1] : null;
            // Opening-tie on resume: don't drop the player into the
            // cashout-button UI for their own stake. Auto-settle and
            // continue session init like the round never existed. Only
            // do this for paid rounds — free-round resumes still need
            // the badge to keep counting down.
            if (last && last.outcome === "tie" && rolls.length === 1 && !d.active_round_is_free) {
              try {
                const c = await api(`/api/v1/round/${d.active_round_id}/cashout`, {method:"POST", body:{}});
                if (c.balance) setBalance(c.balance);
                // Round closed cleanly — fall through to the rest of
                // session init (free-round welcome, etc.).
              } catch(e) {
                // Auto-cashout failed; drop the player into the manual
                // cashout UI rather than losing their stake silently.
                if (last) {
                  setDDice(last.dealer_dice || [1,1]);
                  setPDice(last.player_dice || [1]);
                  setResult(last.outcome);
                }
                const restoredDc = +(gd.dice_count || 1);
                setRoundId(d.active_round_id);
                setPot(+(gd.pot || 0));
                setOriginalBet(+(gd.bet_amount || 0));
                setDoubleDc(restoredDc);
                setDc(restoredDc);
                setDoubleMode(true);
                setPhase("result");
                return;
              }
              // Skip the rest of resume; let session init continue.
            } else {
            if (last) {
              setDDice(last.dealer_dice || [1,1]);
              setPDice(last.player_dice || [1]);
              setResult(last.outcome);
            }
            const restoredDc = +(gd.dice_count || 1);
            setRoundId(d.active_round_id);
            setPot(+(gd.pot || 0));
            setOriginalBet(+(gd.bet_amount || 0));
            setDoubleDc(restoredDc);
            setDc(restoredDc);
            setDoubleMode(true);
            setPhase("result");
            // If the open round was acquired from a free-round grant, keep
            // the FREE ROUND counter/indicators alive on resume — the
            // money flow already routes correctly, this is the UI badge.
            // Only flip into free-round mode if grants actually remain
            // playable; a leaked active_round whose grant is already
            // fully consumed would otherwise wedge us into free-round
            // mode and pop a bogus 0/0 completion modal on the next bet.
            if (d.active_round_is_free) {
              const fr = await fetchFreeRounds();
              setFreeRoundsTotal(fr.total);
              setFreeRoundsUsed(fr.used);
              setFreeRoundsBet(fr.betAmount);
              if (fr.left > 0) setFreeRoundsActive(true);
            }
            return; // skip the free-round welcome; the open round must be settled first
            }
          }
        }

        // Free rounds come from the backend grant ledger, not a URL param.
        // If the player has active grants, offer them — on every viewport
        // (the old innerWidth gate left mobile players unable to start).
        const fr = await fetchFreeRounds();
        if (fr.left > 0) {
          setFreeRoundsTotal(fr.total);
          setFreeRoundsUsed(fr.used);
          setFreeRoundsBet(fr.betAmount);
          setShowFreeRoundWelcome(true);
        }
      } catch(e) {
        _sessionToken = "";
        window.history.replaceState({}, "", window.location.pathname);
        try {
          await bootstrapDemo();
          const d = await api("/api/v1/state");
          const gameOdds = (d.game_data && d.game_data.odds) || d.odds || {};
          setBalance(d.balance); setCurrency(d.currency); setOdds(gameOdds); setConfig(d.config);
          setSeedHash(d.next_seed_hash); setClientSeed(d.client_seed);
          await minLoadTime;
          setLoading(false);
        } catch(e2) { setError(e2.message); setLoading(false); }
      }
    })();
  }, []);

  const minBet=+(config.min_bet||.1), maxBet=+(config.max_bet||100);
  const betSteps = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1,2,3,4,5,6,7,8,9,10,15,20,25,30,35,40,45,50,100].filter(v=>v>=minBet&&v<=maxBet);
  const stepBet = (dir) => {
    const idx = betSteps.findIndex(s => s >= bet);
    if (dir > 0) {
      const next = idx < 0 ? betSteps.length-1 : Math.min(idx + 1, betSteps.length - 1);
      setBet(betSteps[next]);
    } else {
      const prev = idx <= 0 ? 0 : idx - (betSteps[idx] === bet ? 1 : 0);
      setBet(betSteps[Math.max(0, prev)]);
    }
  };
  const [editingBet, setEditingBet] = useState(false);
  // dicedouble has no side bet; total == bet. Kept as a name so the
  // canRoll/insufficientBalance/optimistic-debit sites stay readable.
  const total = bet;
  const canRoll = (+balance>=total || freeRoundsActive) && total>0 && phase==="idle" && !showFreeRoundComplete && !freeRoundsEnded;
  const insufficientBalance = +balance < total && !freeRoundsActive && phase==="idle";

  const stepChips = useMemo(() => {
    if (!doubleMode || !doubleDc) return [];
    const baseMult = +(odds[String(doubleDc)]?.multiplier || 1);
    const maxRounds = { 1: 4, 2: 13, 3: 30 }[doubleDc] || 30;
    const chips = [];
    for (let s = 1; s <= maxRounds; s++) {
      chips.push({ step: s, mult: Math.pow(baseMult, s) });
    }
    return chips;
  }, [doubleMode, doubleDc, odds]);

  const stepTrackRef = React.useRef(null);
  useEffect(() => {
    if (!stepTrackRef.current || !currentStep) return;
    const el = stepTrackRef.current.children[currentStep - 1];
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
  }, [currentStep]);

  // Mouse drag-to-scroll for step tracker (desktop)
  useEffect(() => {
    const el = stepTrackRef.current;
    if (!el) return;
    let isDown = false, startX, scrollLeft;
    const onDown = (e) => { isDown = true; el.style.cursor = 'grabbing'; startX = e.pageX - el.offsetLeft; scrollLeft = el.scrollLeft; };
    const onUp = () => { isDown = false; el.style.cursor = 'grab'; };
    const onMove = (e) => { if (!isDown) return; e.preventDefault(); el.scrollLeft = scrollLeft - (e.pageX - el.offsetLeft - startX); };
    el.addEventListener('mousedown', onDown);
    window.addEventListener('mouseup', onUp);
    window.addEventListener('mousemove', onMove);
    return () => { el.removeEventListener('mousedown', onDown); window.removeEventListener('mouseup', onUp); window.removeEventListener('mousemove', onMove); };
  }, [doubleMode]);

  const toastTimerRef = React.useRef(null);
  const showRoundToast = useCallback((type, text) => {
    if(toastTimerRef.current) clearTimeout(toastTimerRef.current);
    setRoundToast({ type, text });
    toastTimerRef.current = setTimeout(() => { setRoundToast(null); toastTimerRef.current=null; }, 2000);
  }, []);

  const handleRoll = useCallback(async()=>{
    if(!canRoll) return;
    SFX.roll();

    if(resultTimerRef.current){clearTimeout(resultTimerRef.current);resultTimerRef.current=null;} if(toastTimerRef.current){clearTimeout(toastTimerRef.current);toastTimerRef.current=null;} setRolling(true); setPhase("rolling"); setResult(null); setRoundToast(null); setCashoutAlert(null);
    // Optimistic debit for snappy feedback — a free round costs nothing.
    if (!freeRoundsActive) setBalance(b => (+(+b - total).toFixed(2)).toString());
    try {
      if (useNonce) {
        await api("/api/v1/client-seed", {method:"POST", body:{client_seed: clientSeed + "-" + clientNonce}});
        setClientNonce(n => n + 1);
      }
      // Dice Duel is sequential: /bet plays the opening duel. A loss is
      // terminal; a win or tie leaves the round open for double/cashout.
      const d = await api("/api/v1/bet",{method:"POST",body:{ dice_count:dc, amount:bet.toFixed(2) }});
      const gd = d.game_data || {};
      setDDice(gd.dealer_dice||[1,1]); setPDice(gd.player_dice||[1]); setRollSeq(s => s + 1);
      await waitForAllDiceLanded();
      setRolling(false);
      const rollOutcome = gd.roll_outcome || d.outcome;
      setPhase("result"); setResult(rollOutcome);
      if(d.next_seed_hash) setSeedHash(d.next_seed_hash);
      if(rollOutcome==="win")SFX.win(); else if(rollOutcome==="tie")SFX.tie(); else SFX.lose();
      setRecentResults(prev => [rollOutcome, ...prev].slice(0, 5));
      setBalance(d.balance);

      /* Free rounds play as plain single duels — settle the round at
         once, no double/cashout chain. The engine auto-settles free-
         round opens server-side, so /bet usually arrives finished
         with the cashout's payout already in game_data. The fallback
         /cashout call covers the case where the backend left the
         round open (e.g. a tie release returns the slot and stays
         non-terminal). */
      if (freeRoundsActive) {
        let won = 0;
        const freeBet = +freeRoundsBet || 0;
        if (d.finished) {
          // gd.payout is the gross (includes stake); net win is
          // payout - bet. Matches the engine's net-mode credit math.
          const payout = +((gd && gd.payout) || 0);
          won = Math.max(0, payout - freeBet);
        } else if (d.round_id) {
          try {
            const c = await api(`/api/v1/round/${d.round_id}/cashout`, {method:"POST", body:{}});
            if (c.balance) setBalance(c.balance);
            const payout = +((c.game_data && c.game_data.payout) || 0);
            won = Math.max(0, payout - freeBet);
          } catch(e) {}
        }
        setFreeRoundsWinnings(w => w + won);
        fetchFreeRounds().then(fr => {
          setFreeRoundsUsed(Math.max(0, freeRoundsTotal - fr.left));
          if (fr.left <= 0) {
            setFreeRoundsEnded(true);
            if (freeRoundsTotal > 0) {
              // Real completion — celebrate.
              setTimeout(()=>{ setShowFreeRoundComplete(true); }, 1500);
            } else {
              // Stale free-round mode (resumed a leaked active_round
              // whose grant is already consumed). Drop the flag
              // silently — no "0 rounds / $0.00" modal that obscures
              // the rest of the UI.
              setFreeRoundsActive(false);
              setFreeRoundsBet("");
            }
          }
        });
        // Phase idle immediately so Roll button is active — result stays visible briefly
        setPhase("idle");
        clearResultLater();
        return;
      }

      /* Real money — opening tie auto-cashes out (push). The player isn't
         offered a "Cashout $X.XX (push)" button to return their own stake;
         we settle the round server-side and bring them back to a fresh
         Roll. Subsequent ties (mid-chain, after at least one win) still
         leave the round open since there's a real pot to defend. */
      if (!d.finished && d.round_id && rollOutcome === "tie") {
        try {
          const c = await api(`/api/v1/round/${d.round_id}/cashout`, {method:"POST", body:{}});
          if (c.balance) setBalance(c.balance);
        } catch(err) {
          // Auto-cashout failed — fall back to leaving the round open
          // so the player can retry manually rather than losing the
          // stake to a frontend network blip.
          setRoundId(d.round_id);
          setPot(+(gd.pot||0));
          setOriginalBet(bet);
          setDoubleDc(dc);
          setCurrentStep(gd.step || 1);
          setDoubleMode(true);
          return;
        }
        showRoundToast("tie", t("toast.pot_kept", {amount: currSymbol+bet.toFixed(2)}));
        setPhase("idle");
        clearResultLater();
        return;
      }

      /* Real money: a win leaves the round open — the player can double
         (re-stake the whole pot) or cash out. */
      if (!d.finished && d.round_id) {
        setRoundId(d.round_id);
        setPot(+(gd.pot||0));
        setOriginalBet(bet);
        setDoubleDc(dc);
        setCurrentStep(gd.step || 1);
        setDoubleMode(true);
        return;
      }

      /* Terminal on the opening roll — a loss, or a rare roll-1 cap win. */
      if (rollOutcome === "lose") showRoundToast("lose", t("toast.pot_lost", {amount: currSymbol+bet.toFixed(2)}));
      else showRoundToast("win", t("toast.pot_raised", {amount: currSymbol+(+(gd.payout||0)).toFixed(2)}));
      setPhase("idle");
      clearResultLater();
    } catch(e) {
      setRolling(false); setPhase("idle"); alert(e.message);
    }
  },[canRoll,dc,bet,total,freeRoundsActive,freeRoundsTotal,useNonce,clientSeed,clientNonce]);

  // Declared after handleRoll so the useCallback dep [..., handleRoll] is
  // evaluated against the real value, not a TDZ/hoisted-undefined.
  const tryRoll = useCallback(() => {
    if (canRoll) { handleRoll(); return; }
    if (insufficientBalance) {
      setInsufficientToast(true);
      setTimeout(() => setInsufficientToast(false), 2000);
    }
  }, [canRoll, insufficientBalance, handleRoll]);

  const handleDouble = useCallback(async () => {
    if (!doubleMode || phase !== "result" || !roundId) return;
    // Doubling requires at least one win in the chain — an opening tie is
    // a push, not a foothold to gamble from. The backend enforces this
    // too; the client gate keeps the keyboard shortcut and any stray
    // click from triggering a guaranteed-failing /action.
    if (pot <= originalBet) return;
    SFX.roll();
    if(resultTimerRef.current){clearTimeout(resultTimerRef.current);resultTimerRef.current=null;} if(toastTimerRef.current){clearTimeout(toastTimerRef.current);toastTimerRef.current=null;} setRolling(true); setPhase("rolling"); setResult(null); setRoundToast(null); setCashoutAlert(null);
    try {
      // A "double" is one Action on the open round — another duel staking
      // the whole pot. The backend caps the pot at 1000x the opening bet
      // and auto-cashes out if a win would exceed it.
      const d = await api(`/api/v1/round/${roundId}/action`, { method:"POST", body:{} });
      const gd = d.game_data || {};
      setDDice(gd.dealer_dice||[1,1]); setPDice(gd.player_dice||[1]); setRollSeq(s => s + 1);
      await waitForAllDiceLanded();
      setRolling(false);
      const rollOutcome = gd.roll_outcome || d.outcome;
      setResult(rollOutcome);
      setRecentResults(prev => [rollOutcome, ...prev].slice(0, 5));
      if (rollOutcome === "win") SFX.win(); else if (rollOutcome === "tie") SFX.tie(); else SFX.lose();

      if (!d.finished) {
        // Win or tie — the pot rides, the round stays open.
        setPot(+(gd.pot||0));
        setCurrentStep(gd.step || currentStep + 1);
        setPhase("result");
        return;
      }
      // Terminal: a loss (pot gone) or the 1000x cap auto-cashout.
      setBalance(d.balance);
      if (rollOutcome === "lose") {
        showRoundToast("lose", t("toast.pot_lost", {amount: currSymbol+originalBet.toFixed(2)}));
      } else {
        const payout = +(gd.payout||0);
        setCashoutAlert({ amount: payout, mult: originalBet>0 ? +(payout/originalBet).toFixed(2) : 0, streak: (gd.step||1)-1, big: true });
      }
      setRoundId(""); setPot(0); setDoubleMode(false); setOriginalBet(0); setCurrentStep(0);
      setPhase("idle");
      clearResultLater();
    } catch(e) {
      // The round is untouched on the backend — the winnings remain in the
      // open round. Surface the error and stay in double mode.
      setRolling(false); setPhase("result"); alert(e.message);
    }
  }, [doubleMode, phase, roundId, originalBet, pot, currentStep]);

  const handleCashout = useCallback(async () => {
    if (!doubleMode || phase !== "result" || !roundId) return;
    // Defer the SFX until we know whether this cashout is an actual win
    // or a push (opening-tie → pot == bet). A push playing the win sound
    // and showing "WIN! 1.00x $bet" is the long-standing UX bug — the
    // celebration UI used to fire on every cashout regardless of payout.
    try {
      const d = await api(`/api/v1/round/${roundId}/cashout`, { method:"POST", body:{} });
      const gd = d.game_data || {};
      const payout = +(gd.payout || pot || 0);
      // The pot is paid by the backend — the balance already reflects it.
      setBalance(d.balance);
      const isPush = originalBet > 0 && payout <= originalBet;
      if (isPush) {
        SFX.tie();
        showRoundToast("tie", t("toast.push_plain"));
      } else {
        SFX.win();
        setCashoutAlert({ amount: payout, mult: originalBet > 0 ? +(payout/originalBet).toFixed(2) : 0, streak: (gd.step||1)-1 });
      }
      setRoundId(""); setPot(0); setDoubleMode(false); setOriginalBet(0); setCurrentStep(0);
      setPhase("idle"); setResult(null);
    } catch(e) {
      // Cashout failed — the round is still open on the backend. Stay in
      // double mode so the player can retry; do NOT discard roundId/pot,
      // or the active round becomes invisible and the pot gets forfeited
      // when it expires.
      alert(e.message);
    }
  }, [doubleMode, phase, roundId, pot, originalBet]);

  useEffect(()=>{
    const h=e=>{
      if(e.repeat) return;
      if(e.code==="Space"){
        e.preventDefault();
        if(doubleMode && phase==="result") handleDouble();
        else if(phase==="idle") tryRoll();
      }
      if(e.code==="KeyC" && doubleMode && phase==="result"){
        e.preventDefault();
        handleCashout();
      }
    };
    window.addEventListener("keydown",h);
    return()=>window.removeEventListener("keydown",h);
  },[phase,tryRoll,handleDouble,handleCashout,doubleMode]);


  if(loading) return (
    <div className="loading-screen">
      <div className="loading-content">
        <img className="loading-logo" src="loading-logo.svg" alt="Dice Duel"/>
        <div className="loading-bar-wrap">
          <div className="loading-bar"/>
        </div>
        <p className="loading-text">{t("loading.text")}</p>
      </div>
      <div className="loading-watermark">
        <img src="watermark.svg" alt="SETANTAbet"/>
      </div>
    </div>
  );
  if(error) return <div style={{textAlign:"center",padding:40,color:"var(--status-error)"}}>{t("common.error", {msg: error})}</div>;

  const idle = phase==="idle";

  return (<>
    {/* ── Header ── */}
    <div className="m-header">
      {/* Desktop: visible buttons left */}
      <div className="m-header-actions desktop-only">
        <button className="m-header-btn" onClick={()=>{setShowMuted(!showMuted);SFX.setMuted(!showMuted);if(showMuted)SFX.click()}}>
          <IconVolume muted={showMuted}/>
        </button>
        <button className="m-header-btn" onClick={()=>{setDrawerTab("fair");setDrawerOpen(true)}}>
          <IconShield/>
        </button>
        <button className="m-header-btn" onClick={()=>{setDrawerTab("info");setDrawerOpen(true)}}>
          <IconInfo/>
        </button>
      </div>
      {/* Mobile: history button left */}
      <button className="m-header-btn m-header-btn--lg mobile-only" onClick={()=>{setDrawerTab("history");setDrawerOpen(true)}}>
        <IconHistory/>
      </button>
      <div className="m-header-results">
        {recentResults.slice(0, 5).map((r, i) => {
          const fill = r === "lose" ? "#F43F5E" : r === "tie" ? "#FFD106" : "#4ADE80";
          const dotClass = r === "lose" ? " m-header-dot--loss" : r === "tie" ? " m-header-dot--push" : "";
          return (
            <button key={i} className={"m-header-dot"+dotClass} onClick={()=>{setDrawerTab("history");setDrawerOpen(true)}}>
              <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                <path d="M8.5 1H3.5C2.1 1 1 2.1 1 3.5V8.5C1 9.9 2.1 11 3.5 11H8.5C9.9 11 11 9.9 11 8.5V3.5C11 2.1 9.9 1 8.5 1ZM4 8.75C3.6 8.75 3.25 8.4 3.25 8C3.25 7.6 3.6 7.25 4 7.25C4.4 7.25 4.75 7.6 4.75 8C4.75 8.4 4.4 8.75 4 8.75ZM4 4.75C3.6 4.75 3.25 4.4 3.25 4C3.25 3.6 3.6 3.25 4 3.25C4.4 3.25 4.75 3.6 4.75 4C4.75 4.4 4.4 4.75 4 4.75ZM6 6.75C5.6 6.75 5.25 6.4 5.25 6C5.25 5.6 5.6 5.25 6 5.25C6.4 5.25 6.75 5.6 6.75 6C6.75 6.4 6.4 6.75 6 6.75ZM8 8.75C7.6 8.75 7.25 8.4 7.25 8C7.25 7.6 7.6 7.25 8 7.25C8.4 7.25 8.75 7.6 8.75 8C8.75 8.4 8.4 8.75 8 8.75ZM8 4.75C7.6 4.75 7.25 4.4 7.25 4C7.25 3.6 7.6 3.25 8 3.25C8.4 3.25 8.75 3.6 8.75 4C8.75 4.4 8.4 4.75 8 4.75Z" fill={fill}/>
              </svg>
            </button>
          );
        })}
      </div>
      {/* Desktop: history button right */}
      <button className="m-header-history-btn desktop-only" onClick={()=>{setDrawerTab("history");setDrawerOpen(true)}}>
        <IconHistory/>
        <span>{t("header.history")}</span>
      </button>
      {/* Mobile: 3-dot menu */}
      <div className="m-header-menu-wrap mobile-only" style={{position:"relative"}}>
        <button className="m-header-btn m-header-btn--lg" onClick={()=>setShowMenu(!showMenu)}>
          <svg viewBox="0 0 16 16" fill="none" width="16" height="16">
            <circle cx="4" cy="8" r="1.5" fill="#D4D4D4"/>
            <circle cx="8" cy="8" r="1.5" fill="#D4D4D4"/>
            <circle cx="12" cy="8" r="1.5" fill="#D4D4D4"/>
          </svg>
        </button>
        {showMenu && (<>
          <div className="m-menu-backdrop" onClick={()=>setShowMenu(false)}/>
          <div className="m-menu">
            <button className="m-menu-item" onClick={()=>{setShowMuted(!showMuted);SFX.setMuted(!showMuted);if(showMuted)SFX.click();setShowMenu(false)}}>
              <IconVolume muted={showMuted}/>
              <span>{showMuted?t("menu.sound_off"):t("menu.sound_on")}</span>
            </button>
            <button className="m-menu-item" onClick={()=>{setDrawerTab("fair");setDrawerOpen(true);setShowMenu(false)}}>
              <IconShield/>
              <span>{t("drawer.provably_fair")}</span>
            </button>
            <button className="m-menu-item" onClick={()=>{setDrawerTab("info");setDrawerOpen(true);setShowMenu(false)}}>
              <IconInfo/>
              <span>{t("drawer.how_to_play")}</span>
            </button>
          </div>
        </>)}
      </div>
    </div>

    {/* ── Mobile Brand Logo (hidden on desktop) ── */}

    {/* ── Arena (all sizes) ── */}
    <div className="game-arena-center" style={{position:"relative"}}>
      <ArenaLayout phase={phase} result={result} dDice={dDice} pDice={pDice} dc={dc} rolling={rolling} rollSeq={rollSeq} doubleMode={doubleMode} pot={pot} insufficientToast={insufficientToast} roundToast={roundToast} currSymbol={currSymbol} originalBet={originalBet} cashoutAlert={cashoutAlert} setCashoutAlert={setCashoutAlert}/>
    </div>

    {/* ── Mobile Free Round Counter ── */}
    {freeRoundsActive && (
      <div className="mobile-only" style={{padding:"8px 14px 0"}}>
        <FreeRoundCounter used={freeRoundsUsed} total={freeRoundsTotal} winnings={freeRoundsWinnings}/>
      </div>
    )}

    {/* ── Action Footer (unified card) ── */}
    <div className="m-controls">
      {/* Action buttons */}
      <div className="m-action-row">
        {doubleMode ? (<>
          <button className="m-btn-cashout-new" onClick={handleCashout} disabled={phase !== "result"}>
            <svg width="20" height="20" viewBox="0 0 20 20" fill="none"><g clipPath="url(#co2)"><path d="M9.088 17l-2.48.016c-.2.002-.261.27-.082.36l2.98 1.475c.467.233.937.38 1.398.446 1.737.249 3.35-.65 4.267-2.49l2.725-5.503c.577-1.152.702-2.366.35-3.415a4.31 4.31 0 00-2.148-2.344l-1.283-.635c-.098-.05-.213.025-.213.135l.038 6.146c.022 3.396-2.26 5.785-5.552 5.81z" fill="#4ade80"/><path d="M9.24.836c2.496-.005 4.177 1.731 4.194 4.332l.038 6.146c.017 2.601-1.649 4.362-4.143 4.379l-4.263.03c-.25 0-.491-.016-.724-.05-2.088-.299-3.454-1.943-3.471-4.283L.834 5.245C.817 2.645 2.481.885 4.976.866L9.236.837h.003zM7.111 5.867a.614.614 0 00-.614.615v2.897l-.702-.702a.439.439 0 00-.878 0 .439.439 0 000 .878l1.755 1.756a.439.439 0 00.878 0l1.756-1.756a.439.439 0 000-.878.439.439 0 00-.878 0l-.702.702V6.482a.614.614 0 00-.615-.615z" fill="#4ade80"/></g><defs><clipPath id="co2"><rect width="20" height="20" fill="white"/></clipPath></defs></svg>
            <span>{t("btn.cashout_amount", {amount: currSymbol+pot.toFixed(2)})}</span>
          </button>
          {(() => {
            const canDouble = pot > originalBet;
            const nextPot = canDouble
              ? (pot * +(odds[String(doubleDc)]?.multiplier || 1)).toFixed(2)
              : "0";
            return (
              <button className="m-btn-roll" onClick={canDouble ? handleDouble : undefined}
                disabled={!canDouble}
                style={!canDouble ? {opacity:0.4, cursor:"not-allowed"} : undefined}>
                <IconDice disabled={!canDouble}/>
                <span>{canDouble ? t("btn.roll_win", {amount: currSymbol+nextPot}) : t("btn.push_cashout")}</span>
              </button>
            );
          })()}
        </>) : (
          <button className="m-btn-betroll" onClick={tryRoll} disabled={phase!=="idle" || freeRoundsEnded || showFreeRoundComplete}>
            <IconDice disabled={false}/>
            <span>{t("btn.bet_roll", {amount: currSymbol+bet.toFixed(2)})}</span>
          </button>
        )}
      </div>
      {/* Middle section: risk tabs (idle) or step tracker (in-game) */}
      <div className="m-tab-group">
        {doubleMode ? (
          <div className="m-step-track" ref={stepTrackRef}>
            {stepChips.map(chip => {
              const isDone = chip.step < currentStep;
              const isActive = chip.step === currentStep;
              const isNext = chip.step === currentStep + 1;
              return (
                <button key={chip.step}
                  className={"m-step-chip"+(isActive ? " m-step-chip--active" : isDone ? " m-step-chip--done" : "")}>
                  {isDone && <svg viewBox="0 0 16 16" width="16" height="16" fill="none"><rect x="1" y="1" width="14" height="14" rx="3" fill="#22C55E"/><circle cx="5" cy="5" r="1.3" fill="#171717"/><circle cx="11" cy="5" r="1.3" fill="#171717"/><circle cx="8" cy="8" r="1.3" fill="#171717"/><circle cx="5" cy="11" r="1.3" fill="#171717"/><circle cx="11" cy="11" r="1.3" fill="#171717"/></svg>}
                  {isActive && <svg viewBox="0 0 16 16" width="16" height="16" fill="none"><rect x="1" y="1" width="14" height="14" rx="3" fill="white"/><circle cx="5" cy="5" r="1.3" fill="#171717"/><circle cx="11" cy="5" r="1.3" fill="#171717"/><circle cx="8" cy="8" r="1.3" fill="#171717"/><circle cx="5" cy="11" r="1.3" fill="#171717"/><circle cx="11" cy="11" r="1.3" fill="#171717"/></svg>}
                  {isNext && <IconStepForward/>}
                  {!isDone && <span>{chip.mult.toFixed(2)}x</span>}
                </button>
              );
            })}
          </div>
        ) : (
          [
            {d:3, mult:(odds["3"]||{}).multiplier||"1.69", bars:[{c:"#22C55E"},{c:"#404040"},{c:"#404040"}]},
            {d:2, mult:(odds["2"]||{}).multiplier||"2.60", bars:[{c:"#FFD106"},{c:"#FFD106"},{c:"#404040"}]},
            {d:1, mult:(odds["1"]||{}).multiplier||"10.50", bars:[{c:"#F43F5E"},{c:"#F43F5E"},{c:"#F43F5E"}]},
          ].map(opt => (
            <button key={opt.d} className={"m-risk-chip"+(dc===opt.d?" m-risk-chip--active":"")}
              onClick={()=>{SFX.click();setDc(opt.d)}} disabled={!idle||freeRoundsActive}>
              <div className="m-risk-bars">
                {opt.bars.map((b,i)=><span key={i} className="m-risk-bar" style={{height:[4,7,11][i],background:b.c}}/>)}
              </div>
              <div className="m-risk-text">
                <span className="m-risk-mult">{opt.mult}x</span>
                <span className="m-risk-label">{t("risk.dice", {n: opt.d})}</span>
              </div>
            </button>
          ))
        )}
      </div>
      {/* Desktop Free Round Counter — above bet bar */}
      {freeRoundsActive && (
        <div className="desktop-free-round desktop-only">
          <FreeRoundCounter used={freeRoundsUsed} total={freeRoundsTotal} winnings={freeRoundsWinnings}/>
        </div>
      )}
      {/* Bet input bar */}
      <div className="m-bet-bar">
        <div className="m-bet-left">
          <div className="m-bet-currency">{currSymbol}</div>
          <span className="m-bet-balance">{(+balance).toLocaleString("en-US",{minimumFractionDigits:2})}</span>
        </div>
        <div className="m-bet-divider"/>
        {freeRoundsActive ? (
          <span className="free-round-bet-label" style={{flex:1,textAlign:"center"}}>{t("label.free_round")}</span>
        ) : (<>
          <div className="m-bet-center">
            <button className="m-bet-adj" onClick={()=>{SFX.click();stepBet(-1)}} disabled={!idle||doubleMode}>-</button>
            <span className="m-bet-label">{t("label.bet")}</span>
            {editingBet ? (
              <input className="m-bet-input" type="number" min={minBet} max={maxBet} step="0.1"
                autoFocus defaultValue={bet}
                onBlur={(e)=>{const v=+e.target.value;if(!isNaN(v)&&v>=minBet&&v<=maxBet)setBet(rc(v));else if(v>maxBet)setBet(maxBet);else setBet(minBet);setEditingBet(false)}}
                onKeyDown={(e)=>{if(e.key==="Enter")e.target.blur()}}/>
            ) : (
              <span className={"m-bet-value"+((!idle||doubleMode)?" m-bet-value--disabled":"")} onClick={()=>{if(idle&&!doubleMode)setEditingBet(true)}}>${bet.toFixed(2)}</span>
            )}
            <button className="m-bet-adj" onClick={()=>{SFX.click();stepBet(1)}} disabled={!idle||doubleMode}>+</button>
          </div>
          <div className="m-bet-right">
            <button className="m-bet-chip" onClick={()=>{SFX.click();setBet(b=>Math.max(minBet,rc(b/2)))}} disabled={!idle||doubleMode}>1/2</button>
            <button className="m-bet-chip" onClick={()=>{SFX.click();setBet(b=>Math.min(rc(b*2),maxBet,+balance))}} disabled={!idle||doubleMode}>2x</button>
          </div>
        </>)}
      </div>
    </div>

    {/* ── Cashout Alert ── */}

    {/* ── Drawers ── */}
    {drawerOpen && drawerTab==="fair" && <FairDrawer onClose={()=>setDrawerOpen(false)} seedHash={seedHash} clientSeed={clientSeed}
      onSetSeed={s=>setClientSeed(s)} onRefreshSeed={h=>setSeedHash(h)}
      useNonce={useNonce} onToggleNonce={v=>{setUseNonce(v);if(v)setClientNonce(0)}} clientNonce={clientNonce}/>}
    {drawerOpen && drawerTab==="history" && <HistoryDrawer onClose={()=>setDrawerOpen(false)} clientSeed={clientSeed}/>}
    {drawerOpen && drawerTab==="info" && <InfoDrawer onClose={()=>setDrawerOpen(false)} odds={odds} config={config}/>}

    {/* Free Round Modals */}
    {showFreeRoundWelcome && (
      <FreeRoundWelcomeModal total={freeRoundsTotal} betAmount={freeRoundsBet} currencySymbol={currSymbol} onStart={()=>{
        setShowFreeRoundWelcome(false);
        setFreeRoundsActive(true);
        setBet(10);
        setDc(2);
      }}/>
    )}
    {showFreeRoundComplete && (
      <FreeRoundCompleteModal total={freeRoundsTotal} winnings={freeRoundsWinnings} currencySymbol={currSymbol} onContinue={()=>{
        setShowFreeRoundComplete(false);
        setFreeRoundsEnded(false);
        setFreeRoundsActive(false);
        setFreeRoundsTotal(0);
        setFreeRoundsUsed(0);
        setFreeRoundsWinnings(0);
        setFreeRoundsBet("");
      }}/>
    )}
  </>);
}

// Load the active locale (and the English fallback) before first render so
// t() is synchronous everywhere in the tree.
loadLocale().finally(() => {
  ReactDOM.render(React.createElement(Game), document.getElementById("root"));
});
