// game-mines.jsx — Mines · 5x5 grid · 99% RTP / 1% house edge
// Player picks a mine count (1..24), bets, then reveals tiles one at a time.
// Each safe pick multiplies the prize by 0.99 × C(25, k) / C(25 - M, k) — the
// fair conditional multiplier given M mines, k safe picks taken, scaled by
// 1% house edge. Cashing out at any point pays bet × current multiplier;
// clicking a mine ends the round at 0.
//
// §5.65 — server-resolved. Mine positions are derived server-side via the
// Worker's chained-hash walk (server/src/games/mines.js); the browser only
// sees a roundId + commitment hash on /play/mines/start. Each reveal POSTs
// the tileIdx to /play/mines/reveal; server returns safe-with-mult or
// bust-with-full-layout. /play/mines/cashout reveals the layout + writes
// /plays. Local `minesMultiplier` mirrors the server formula for the
// during-play "current mult" display.

const { useState: useStateMN, useEffect: useEffectMN, useRef: useRefMN } = React;

const MN_TILES = 25;
const MN_HOUSE_EDGE = 0.01;
const MN_PRESETS = [3, 5, 10, 15, 24]; // quick-pick mine counts

// Cumulative multiplier after `safePicks` safe reveals with `mineCount` mines.
// Returns 1.0 when no picks have been taken yet (no cash-out value). Equivalent
// to (0.99) × product_{i=0..safePicks-1} (25 - i) / (25 - mineCount - i).
function minesMultiplier(safePicks, mineCount) {
  if (safePicks === 0) return 1;
  let m = 1;
  for (let i = 0; i < safePicks; i++) {
    m *= (MN_TILES - i) / (MN_TILES - mineCount - i);
  }
  return m * (1 - MN_HOUSE_EDGE);
}

function MinesPage({ palette: M, wallet, fairness, onConnect }) {
  // Phases: idle = pre-deal config; playing = grid live, awaiting picks;
  // busted = mine hit, all tiles revealed; cashed = player cashed out clean.
  const [phase, setPhase] = useStateMN('idle');
  const [mineCount, setMineCount] = useStateMN(3);
  const [mines, setMines] = useStateMN(new Set());
  const [revealed, setRevealed] = useStateMN(new Set());
  const [last, setLast] = useStateMN(null);
  const [error, setError] = useBetError();
  const { bet, setBet, betStr, setBetStr } = useBetInput(0.1);
  const busyRef = useRefMN(false);
  // §5.65: round identity. Server holds the mine layout in memory keyed by
  // (uid, roundId); browser only knows the id. roundHexRef is populated from
  // the server's response on bust/cashout — during play the round hex is
  // server-side secret.
  const roundIdRef = useRefMN(null);
  const roundHexRef = useRefMN(null);

  const MIN_BET = coinMinBet(wallet.activeCoin);
  // Per-coin max — keep lower than coin flip because the max-mult ceiling at
  // 24 mines / 1 safe pick is ~24.75×, but 10 mines fully cleared (15 picks)
  // pays ~3268× so a tight bet cap on the input prevents accidental jackpots.
  const MAX_BET_SOL = 5;
  const maxBet = maxBetForCoin(MAX_BET_SOL, wallet.activeCoin);

  useEffectMN(() => {
    if (bet < MIN_BET) setBet(MIN_BET);
    else if (bet > maxBet) setBet(maxBet);
  }, [wallet.activeCoin]); // eslint-disable-line

  const safePicks = revealed.size;
  const currentMult = minesMultiplier(safePicks, mineCount);
  const nextMult = minesMultiplier(safePicks + 1, mineCount);
  const cashValue = bet * currentMult;
  const canDeal = phase === 'idle' || phase === 'busted' || phase === 'cashed';
  const canPlay = phase === 'playing';
  const safeTilesRemaining = MN_TILES - mineCount - safePicks;

  function clampMineCount(n) {
    return Math.max(1, Math.min(24, Math.floor(Number(n) || 1)));
  }

  async function deal() {
    if (busyRef.current) return;
    if (!canDeal) return;
    if (!wallet.canPlay) { onConnect(); return; }
    const bag = wallet.balance[wallet.activeCoin] || 0;
    if (bet < MIN_BET) { setError(`minimum bet is ${MIN_BET} ${wallet.activeCoin}`); return; }
    if (bet > maxBet)  { setError(`max bet on mines is ${maxBet.toFixed(2)} ${wallet.activeCoin}`); return; }
    if (bet > bag) {
      setError(`bet ${bet.toFixed(3)} ${wallet.activeCoin} exceeds bag (${bag.toFixed(3)} ${wallet.activeCoin} available)`);
      return;
    }
    busyRef.current = true;
    setError(null);
    setLast(null);
    if (!wallet.debit(wallet.activeCoin, bet)) {
      busyRef.current = false;
      setError(`bag changed under the bet -- try again`);
      return;
    }

    // §5.65: server-resolved start. Server holds the layout; browser only
    // gets a roundId + commitment hash back. Local `mines` set stays empty
    // until bust/cashout reveals it.
    let server;
    try {
      server = await callServerGame('/play/mines/start', {
        bet,
        mineCount,
        clientSeed: fairness.clientSeed,
        coin: wallet.activeCoin,
        user: wallet.username,
      });
    } catch (err) {
      wallet.credit(wallet.activeCoin, bet, 'mines-refund-' + Date.now());
      busyRef.current = false;
      setError('server error: ' + (err && err.message ? err.message : String(err)));
      return;
    }

    roundIdRef.current = server.roundId;
    roundHexRef.current = server.serverHash || ('mines-' + server.roundId);
    setMines(new Set());          // layout not known until end of round
    setRevealed(new Set());
    setPhase('playing');
    busyRef.current = false;
    playHitSound();
  }

  async function reveal(idx) {
    if (busyRef.current) return;
    if (!canPlay) return;
    if (revealed.has(idx)) return;
    if (!roundIdRef.current) return;
    busyRef.current = true;

    let server;
    try {
      server = await callServerGame('/play/mines/reveal', {
        roundId: roundIdRef.current,
        tileIdx: idx,
      });
    } catch (err) {
      busyRef.current = false;
      const msg = err && err.message ? err.message : String(err);
      // If the round was lost (isolate rotation, etc.), refund + reset.
      if (msg.includes('round not found')) {
        wallet.credit(wallet.activeCoin, bet, 'mines-refund-' + Date.now());
        roundIdRef.current = null;
        setPhase('idle');
        setError('round lost (server reset); bet refunded');
      } else {
        setError('server error: ' + msg);
      }
      return;
    }

    // Mark the clicked tile revealed regardless of outcome.
    const newRevealed = new Set(revealed);
    newRevealed.add(idx);
    setRevealed(newRevealed);

    if (server.isMine) {
      // Bust — server reveals the layout.
      setMines(new Set(Array.isArray(server.layout) ? server.layout : []));
      const result = {
        outcome: 'bust',
        mult: 0,
        payout: 0,
        bet,
        safePicks: Number(server.safePicks) || 0,
        mineCount: Number(server.mineCount) || mineCount,
        hex: String(server.hex || '').slice(0, 12),
      };
      setLast(result);
      wallet.logPlay({
        game:'Mines', bet, payout: 0, win: false,
        coin: wallet.activeCoin, hex: String(server.hex || '').slice(0, 16),
        mult: 0, outcome: 'bust', mineCount: result.mineCount, safePicks: result.safePicks,
      });
      // NOTE: server wrote /plays already (rules step 4 blocks browser writes).
      roundIdRef.current = null;
      setPhase('busted');
      busyRef.current = false;
      playOutcomeSound(false);
      return;
    }

    if (server.autoCashout) {
      // Full clear — server auto-cashed.
      setMines(new Set(Array.isArray(server.layout) ? server.layout : []));
      const mult = Number(server.mult) || 0;
      const payout = Number(server.payout) || 0;
      const safe = Number(server.safePicks) || 0;
      const mc = Number(server.mineCount) || mineCount;
      if (payout > 0) wallet.credit(wallet.activeCoin, payout, String(server.hex || ('mines-cash-' + Date.now())));
      const result = {
        outcome: 'cashed', mult, payout, bet,
        safePicks: safe, mineCount: mc,
        hex: String(server.hex || '').slice(0, 12),
      };
      setLast(result);
      wallet.logPlay({
        game:'Mines', bet, payout, win: payout > bet,
        coin: wallet.activeCoin, hex: String(server.hex || '').slice(0, 16),
        mult, outcome: 'cashed', mineCount: mc, safePicks: safe,
      });
      roundIdRef.current = null;
      setPhase('cashed');
      busyRef.current = false;
      if (safe === MN_TILES - mc) playJackpotSound();
      else if (mult >= 10)        playStreakSound();
      else                        playOutcomeSound(true);
      return;
    }

    // Plain safe pick — continue playing.
    busyRef.current = false;
    playHitSound();
  }

  async function cashOut() {
    if (busyRef.current) return;
    if (!canPlay) return;
    if (safePicks === 0) return;
    if (!roundIdRef.current) return;
    busyRef.current = true;

    let server;
    try {
      server = await callServerGame('/play/mines/cashout', {
        roundId: roundIdRef.current,
      });
    } catch (err) {
      busyRef.current = false;
      setError('server error: ' + (err && err.message ? err.message : String(err)));
      return;
    }

    setMines(new Set(Array.isArray(server.layout) ? server.layout : []));
    const mult = Number(server.mult) || 0;
    const payout = Number(server.payout) || 0;
    const safe = Number(server.safePicks) || 0;
    const mc = Number(server.mineCount) || mineCount;
    if (payout > 0) wallet.credit(wallet.activeCoin, payout, String(server.hex || ('mines-cash-' + Date.now())));
    const result = {
      outcome: 'cashed', mult, payout, bet,
      safePicks: safe, mineCount: mc,
      hex: String(server.hex || '').slice(0, 12),
    };
    setLast(result);
    wallet.logPlay({
      game:'Mines', bet, payout, win: payout > bet,
      coin: wallet.activeCoin, hex: String(server.hex || '').slice(0, 16),
      mult, outcome: 'cashed', mineCount: mc, safePicks: safe,
    });
    roundIdRef.current = null;
    setPhase('cashed');
    busyRef.current = false;
    if (safe === MN_TILES - mc) playJackpotSound();
    else if (mult >= 10)        playStreakSound();
    else                        playOutcomeSound(true);
  }

  function fmtMult(m) {
    if (m >= 1000) return `${(m / 1000).toFixed(2)}k×`;
    if (m >= 100)  return `${m.toFixed(1)}×`;
    return `${m.toFixed(2)}×`;
  }

  return (
    <div className="rc-page" style={{flex:1, padding:'18px 26px 40px', overflowY:'auto'}}>
      <h1 className="rc-h-page" style={{fontSize:48, color:M.hot, margin:'4px 0', textShadow:`3px 3px 0 ${M.green}`}}>MINES</h1>
      <div className="rc-h-sub" style={{color:M.ink2, fontSize:18, marginBottom:14}}>
        <span style={{color:M.green}}>99% RTP</span> · <span style={{color:M.hot}}>1% edge</span> · 5×5 grid · pick safely, cash out before a mine · max bet {maxBet.toFixed(2)} {wallet.activeCoin}
      </div>

      <div className="rc-mines-playmode">
        <PlayModeBar palette={M} wallet={wallet} onConnect={onConnect}/>
      </div>
      <BetErrorToast error={error} onClose={()=>setError(null)} palette={M}/>

      <div className="rc-game-grid" style={{display:'grid', gridTemplateColumns:'320px 1fr', gap:18, alignItems:'start'}}>
        <Frame title="bet" accent={M.yellow} className="rc-mines-bet">
         <div className="rc-bet-panel">
          <div className="rc-mn-greyout rc-bet-inputs" style={{opacity: canDeal ? 1 : 0.4, pointerEvents: canDeal ? 'auto' : 'none', transition:'opacity 0.15s'}}>
            <div style={{fontSize:14, color:M.ink2, marginBottom:4}}>mines</div>
            <div style={{display:'grid', gridTemplateColumns:'repeat(5, 1fr)', gap:4, marginBottom:6}}>
              {MN_PRESETS.map(n => (
                <button key={n} onClick={()=>setMineCount(n)} disabled={!canDeal} style={{
                  background: mineCount === n ? M.hot : M.bg,
                  color:      mineCount === n ? M.bg  : M.hot,
                  border:`2px solid ${M.hot}`, fontFamily:'inherit', fontSize:14, padding:'5px 0',
                  cursor: canDeal ? 'pointer' : 'not-allowed', fontWeight:700,
                  boxShadow: mineCount === n ? `2px 2px 0 ${M.bg}` : 'none',
                }}>{n}</button>
              ))}
            </div>
            <input type="number" min={1} max={24} step={1} value={mineCount}
              onChange={e=>setMineCount(clampMineCount(e.target.value))}
              disabled={!canDeal} style={{
                width:'100%', background:M.bg, border:`2px solid ${M.hot}`, color:M.hot,
                padding:'6px 8px', fontFamily:'inherit', fontSize:16, outline:'none',
              }}/>
            <div style={{fontSize:11, color:M.ink2, marginTop:4, marginBottom:10}}>
              {25 - mineCount} safe · {mineCount} mine{mineCount===1?'':'s'} · next mult {fmtMult(minesMultiplier(1, mineCount))}
            </div>

            <div style={{fontSize:14, color:M.ink2, marginBottom:4}}>bet ({wallet.activeCoin})</div>
            <input type="number" step={0.01} max={maxBet} value={betStr} onChange={e=>setBetStr(e.target.value)} disabled={!canDeal} style={{
              width:'100%', background:M.bg, border:`2px solid ${M.green}`, color:M.green, padding:'8px 10px', fontFamily:'inherit', fontSize:22, outline:'none',
            }}/>
            <div style={{marginTop:6}}>
              <QuickBetButtons current={bet} onPick={v=>setBet(v)} palette={M} disabled={!canDeal}/>
            </div>
            <div style={{display:'flex', gap:4, marginTop:6}}>
              <button onClick={()=>setBet(b=>Math.max(MIN_BET, +(b/2).toFixed(6)))} disabled={!canDeal} style={mnBtnSec(M)}>½</button>
              <button onClick={()=>setBet(b=>+Math.min(b*2, maxBet).toFixed(6))} disabled={!canDeal} style={mnBtnSec(M)}>2×</button>
              <button onClick={()=>setBet(+(Math.min(wallet.balance[wallet.activeCoin] || 0, maxBet)).toFixed(6))} disabled={!canDeal} title={`max bet: ${maxBet.toFixed(2)} ${wallet.activeCoin}`} style={mnBtnSec(M)}>max</button>
            </div>
            {/* PLAY — desktop twin, lives inside the greyout (original structure).
                Hidden on mobile via rc-hide-mobile; the mobile twin below takes over. */}
            <button className="rc-hide-mobile" onClick={deal} disabled={!canDeal} style={{
              marginTop:14, width:'100%', background:M.hot, color:M.bg, border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:22, padding:'10px 0', fontWeight:700, cursor:canDeal?'pointer':'wait', boxShadow:`5px 5px 0 ${M.green}`,
            }}>{phase === 'idle' ? '► PLAY' : '► PLAY AGAIN'}</button>
          </div>
          {/* PLAY — mobile twin, lives outside the greyout so it can sit at the
              top of the bet panel via flex order. Self-contained opacity since
              it's no longer inheriting the greyout parent's 0.4. Hidden on
              desktop via rc-mobile-only. */}
          <button className="rc-mobile-only rc-bet-action" onClick={deal} disabled={!canDeal} style={{
            marginTop:14, width:'100%', background:M.hot, color:M.bg, border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:22, padding:'10px 0', fontWeight:700,
            cursor:canDeal?'pointer':'wait', boxShadow:`5px 5px 0 ${M.green}`,
            opacity: canDeal ? 1 : 0.4, transition: 'opacity 0.15s',
          }}>{phase === 'idle' ? '► PLAY' : '► PLAY AGAIN'}</button>
          {phase === 'playing' && (
            <button className="rc-mn-cashout" onClick={cashOut} disabled={safePicks === 0} style={{
              marginTop:14, width:'100%',
              background: safePicks === 0 ? M.ink2 : M.green,
              color: M.bg,
              border:`3px solid ${M.bg}`, fontFamily:'inherit', fontSize:22, padding:'10px 0',
              fontWeight:700, cursor: safePicks === 0 ? 'not-allowed' : 'pointer',
              boxShadow:`5px 5px 0 ${M.bg}`, letterSpacing:'0.05em',
            }}>
              {safePicks === 0
                ? 'PICK A TILE FIRST'
                : `✓ CASH OUT · ${cashValue.toFixed(wallet.activeCoin==='SOL'?3:2)} ${wallet.activeCoin}`}
            </button>
          )}
         </div>
        </Frame>

        <Frame title="the field" accent={M.green} style={{minHeight:380, display:'flex', flexDirection:'column', alignItems:'center', justifyContent:'center', gap:14, padding:'20px 16px'}}>
          {/* Status / result slot — fixed height so the grid below never
              shifts between idle, playing, busted, and cashed phases. */}
          <div className="rc-resolve-slot-row" style={{minHeight:78, display:'flex', alignItems:'flex-start', justifyContent:'center', width:'100%'}}>
            {phase === 'idle' && (
              <div style={{color:M.ink2, fontSize:14, textAlign:'center'}}>pick a mine count, place a bet, and click PLAY to start.</div>
            )}
            {phase === 'playing' && (
              <div style={{display:'flex', gap:14, alignItems:'baseline', flexWrap:'wrap', justifyContent:'center'}}>
                <span style={{fontSize:13, color:M.ink2, letterSpacing:'0.05em'}}>CURRENT</span>
                <span style={{fontSize:28, color:M.yellow, fontWeight:700, fontFamily:'JetBrains Mono, monospace'}}>{fmtMult(currentMult)}</span>
                <span style={{fontSize:13, color:M.ink2}}>next safe pick → {fmtMult(nextMult)}</span>
              </div>
            )}
            {phase === 'busted' && last && (
              <div style={{textAlign:'center'}}>
                <div style={{fontSize:30, color:M.red, fontWeight:700, textShadow:`2px 2px 0 ${M.bg}`, letterSpacing:'0.05em'}}>✗ MINE — BUST</div>
                <div style={{fontSize:16, color:M.red, marginTop:2, fontWeight:700}}>
                  −{last.bet.toFixed(wallet.activeCoin==='SOL'?3:2)} {wallet.activeCoin} · {last.safePicks} safe pick{last.safePicks===1?'':'s'} before the bomb
                </div>
                <div style={{fontSize:12, color:M.ink2, marginTop:2, fontFamily:'JetBrains Mono, monospace'}}>hash: {last.hex}…</div>
              </div>
            )}
            {phase === 'cashed' && last && (
              <div style={{textAlign:'center'}}>
                <div style={{fontSize:30, color:M.green, fontWeight:700, textShadow:`2px 2px 0 ${M.bg}`, letterSpacing:'0.05em'}}>
                  ✓ CASHED · {fmtMult(last.mult)}
                </div>
                <div style={{fontSize:16, color:M.green, marginTop:2, fontWeight:700}}>
                  +{last.payout.toFixed(wallet.activeCoin==='SOL'?3:2)} {wallet.activeCoin}
                  <span style={{color:M.ink2, marginLeft:6, fontWeight:400}}>· {last.safePicks}/{25-last.mineCount} safe picks</span>
                </div>
                <div style={{fontSize:12, color:M.ink2, marginTop:2, fontFamily:'JetBrains Mono, monospace'}}>hash: {last.hex}…</div>
              </div>
            )}
          </div>

          {/* Grid is ALWAYS rendered — during idle, every tile shows as a
              covered placeholder, so the grid never appears/disappears as
              phases change. */}
          <MinesGrid
            M={M}
            mines={mines}
            revealed={revealed}
            phase={phase}
            onReveal={reveal}
          />
        </Frame>
      </div>

      <MinesStatsPanel palette={M} wallet={wallet}/>
      <HashPanel palette={M} fairness={fairness}/>
    </div>
  );
}

function mnBtnSec(M){ return { flex:1, background:M.bg, color:M.green, border:`1px solid ${M.green}`, fontFamily:'inherit', fontSize:14, padding:'4px 0', cursor:'pointer' }; }

function MinesGrid({ M, mines, revealed, phase, onReveal }) {
  return (
    <div style={{display:'grid', gridTemplateColumns:'repeat(5, 1fr)', gap:6, padding:8, background:M.bg, border:`3px solid ${M.green}`, boxShadow:`5px 5px 0 ${M.hot}`}}>
      {Array.from({length: MN_TILES}, (_, i) => (
        <MinesTile key={i} idx={i} M={M} mines={mines} revealed={revealed} phase={phase} onReveal={onReveal}/>
      ))}
    </div>
  );
}

function MinesTile({ idx, M, mines, revealed, phase, onReveal }) {
  const isRevealed = revealed.has(idx);
  const isMine = mines.has(idx);
  // After bust or cash-out, every mine is exposed so the player sees the
  // layout. Hidden non-mines stay covered to keep the "what would've been
  // safe" cells distinguishable from the bomb cells.
  const showAsRevealed = isRevealed || ((phase === 'busted' || phase === 'cashed') && isMine);
  const clickable = phase === 'playing' && !isRevealed;
  // Color scheme: covered = panel gray; safe = green tile with gem; mine =
  // red tile with bomb. Post-round mines render with reduced opacity to
  // distinguish "you hit this" (full red) from "you avoided this" (dim red).
  let bg = M.panel;
  let content = null;
  let opacity = 1;
  if (showAsRevealed && isMine) {
    bg = M.red;
    content = <span style={{fontSize:24, color:M.bg, fontWeight:700, lineHeight:1}}>✦</span>;
    if (!isRevealed) opacity = 0.55; // unhit mine after cash-out / bust
  } else if (showAsRevealed && !isMine) {
    bg = M.green;
    content = <span style={{fontSize:22, color:M.bg, fontWeight:700, lineHeight:1}}>◆</span>;
  }
  return (
    <button
      onClick={() => clickable && onReveal(idx)}
      disabled={!clickable}
      style={{
        width: 60, height: 60, background: bg, border: `2px solid ${M.green}33`,
        cursor: clickable ? 'pointer' : 'default',
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: 0, opacity, transition: 'background 0.15s, opacity 0.15s',
      }}
    >{content}</button>
  );
}

function MinesStatsPanel({ palette: M, wallet }) {
  const coin = wallet.activeCoin;
  const rounds = (wallet.history || []).filter(h => h.game === 'Mines' && h.coin === coin);
  const total = rounds.length;
  const cashed = rounds.filter(r => r.outcome === 'cashed').length;
  const winRate = total ? (cashed / total) * 100 : 0;
  const wagered = rounds.reduce((s, r) => s + (r.bet || 0), 0);
  const netPL   = rounds.reduce((s, r) => s + ((r.payout || 0) - (r.bet || 0)), 0);
  const bestWin = rounds.reduce((m, r) => Math.max(m, (r.payout || 0) - (r.bet || 0)), 0);
  const bestMult = rounds.reduce((m, r) => Math.max(m, r.mult || 0), 0);
  const dp = coin === 'SOL' ? 3 : 2;
  const fmt    = n => `${n>=0?'+':''}${n.toFixed(dp)} ${coin}`;
  const fmtAbs = n => `${n.toFixed(dp)} ${coin}`;
  const tile = (label, value, color) => (
    <div style={{textAlign:'center', padding:'4px 0'}}>
      <div style={{fontSize:11, color:M.ink2, textTransform:'uppercase', letterSpacing:'0.05em'}}>{label}</div>
      <div style={{fontSize:22, color, fontWeight:700, lineHeight:1.1, marginTop:4, fontFamily:'JetBrains Mono, monospace'}}>{value}</div>
    </div>
  );
  return (
    <Frame title="mines stats" accent={M.cyan} style={{marginTop:18}}>
      {total === 0 ? (
        <div style={{color:M.ink2, fontSize:14, padding:'4px 0'}}>{'>'} no rounds yet in {coin}. drop a chip.</div>
      ) : (
        <div className="rc-stat-6" style={{display:'grid', gridTemplateColumns:'repeat(6, 1fr)', gap:14}}>
          {tile('rounds',        total,                              M.green)}
          {tile('cash-out rate', `${winRate.toFixed(1)}%`,           M.cyan)}
          {tile('total wagered', fmtAbs(wagered),                    M.yellow)}
          {tile('net P/L',       fmt(netPL),                         netPL >= 0 ? M.green : M.red)}
          {tile('best win',      `+${bestWin.toFixed(dp)} ${coin}`,  M.green)}
          {tile('best mult',     bestMult >= 1000 ? `${(bestMult/1000).toFixed(2)}k×` : `${bestMult.toFixed(2)}×`, M.hot)}
        </div>
      )}
    </Frame>
  );
}

Object.assign(window, { MinesPage });
