/*
 * Dark mode is permanent and intentional — not the default-with-a-light-override it used
 * to be. Colors are hardcoded here rather than sourced via var(--tg-bg-color, ...) so that
 * a player's own Telegram client theme (which could be light) can never override it; see
 * telegram.js, which no longer syncs Telegram's theme params into CSS variables at all.
 *
 * === Design system pass (Phase 1 of docs/GRAPHIC_ASSETS_SPEC.md's "UI chrome" tier) ===
 * Adds a token layer (radii, shadows, a brass/gold secondary accent) and a real button
 * system (.btn-primary/.btn-secondary/.btn-ghost) on top of the existing palette and
 * cell/animation rules below, which are unchanged — the "Subterranean vault" mood, the
 * cell color-per-type rules, and every keyframe here were already working and are left as
 * they were. Nothing in this file touches game logic; it's presentation only.
 */
:root {
  color-scheme: dark;
  --bg: #0f1115;
  --fg: #f2f2f2;
  --hint: #9fa0a3;
  --accent: #4caf50;
  --accent-fg: #ffffff;
  --cell-bg: #1e222b;
  --cell-border: #2c313d;
  --positive: #4caf50;
  --negative: #e05c5c;
  --modal-card-bg: #1a1d24;

  /* --- New tokens (Phase 1 design pass) --- */
  /* Aged-brass secondary accent per GRAPHIC_ASSETS_SPEC's art direction ("aged brass &
     rivets, torchlight glow") — used for high-value moments (jackpot, fairness badge,
     chest) so they read as distinct from the everyday green/red win/lose pair. */
  --brass: #d9a441;
  --brass-bright: #f2c766;
  --surface-1: #161a22;      /* panel background, one step up from --bg */
  --surface-2: #1e2430;      /* raised control background (inputs, chips) */
  --border-soft: rgba(255, 255, 255, 0.08);
  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 18px;
  --radius-pill: 999px;
  --shadow-1: 0 2px 10px rgba(0, 0, 0, 0.35);
  --shadow-glow-positive: 0 4px 18px rgba(76, 175, 80, 0.35);
  --shadow-glow-negative: 0 4px 18px rgba(224, 92, 92, 0.3);
  --shadow-glow-brass: 0 4px 20px rgba(217, 164, 65, 0.35);
  /* Georgia/Iowan/Noto Serif is the *real* fallback, not just a loading-flash placeholder:
     this codebase ships no build step, so if fonts/cinzel-600.woff2 (see @font-face below)
     is ever missing, every visit renders on this stack permanently, not just briefly. It's
     deliberately a proper serif, not the sans-serif --font-body stack, so the title/money
     figures keep a distinct "display" feel either way. */
  --font-display: "Cinzel", Georgia, "Iowan Old Style", "Noto Serif", "Times New Roman", serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

/* Self-hosted — no Google Fonts / CDN request at runtime (matches this codebase's "static
   files, no bundler" model; see README.md). Only one weight is actually used anywhere below
   (600 — h1, balance/pool figures, buttons already default to 600 via the `button` rule).
   The .woff2 binary itself isn't in the repo yet — this sandbox's network doesn't reach
   fonts.google.com to fetch it (see apps/web/public/fonts/README.txt for exactly what to
   drop in). Until that file exists this 404s harmlessly and every browser silently falls
   back to the --font-display stack above; font-display:swap avoids any invisible-text wait
   either way. */
@font-face {
  font-family: "Cinzel";
  src: url("fonts/cinzel-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}

* {
  box-sizing: border-box;
}

/* html+body pinned to exactly the viewport height with overflow:hidden — a real mobile game
   shell, not a scrollable document (Ivan's explicit request, 2026-08-13: "the game is
   scrollable and zoomable - i want to avoid that"). This only works because the Cashier and
   Dev Tools moved out of the normal content flow into modals (see #wallet-modal, the
   .dev-toggle-btn's position:fixed) and the bet stepper moved above the action button — the
   remaining main-screen content is sized to actually fit a real phone viewport without
   scrolling; verified via Playwright at the constrained height Telegram's own Mini App nav
   bar leaves available, not just a bare device viewport. */
html {
  height: 100%;
  overflow: hidden;
}

body {
  margin: 0;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  overscroll-behavior: none;
  /* Removes the double-tap-to-zoom gesture and the ~300ms tap-delay some WebViews still add
     for taps that might become a double-tap — single taps/drags still work normally. Pinch-
     zoom itself is disabled via index.html's viewport meta (user-scalable=no); this is the
     CSS-level backstop for WebViews that don't fully honor that meta tag. */
  touch-action: manipulation;
  /* cover (not contain) — contain left visible --bg letterbox bars on any viewport whose
     aspect ratio doesn't match the source art's (1080x1934), which reads as a broken/
     unfinished background rather than a seam, especially on the wider aspect ratios a
     desktop browser (the public demo's other real audience, see INVESTOR_DEMO_SPEC.md) or a
     tablet actually has — Ivan's screenshot caught this. cover always fills the full
     viewport at the cost of cropping some of the image outside its center; center center
     keeps that crop symmetric. fixed keeps it pinned to the viewport while the page content
     scrolls, per request — note this is a known soft spot on some older iOS WKWebView
     versions (it can silently behave like `scroll` there); flag it if it doesn't stay put on
     an actual device. */
  background: var(--bg) url("img/bg-app.jpg") center center / cover no-repeat fixed;
  color: var(--fg);
  font-family: var(--font-body);
  /* Centers #app (the only real child — modals inside it are absolutely positioned, not
     part of flow) against the background's own centered composition. display:flex here is
     only to make #app's own margin:auto (below) resolve as 2-axis centering — NOT paired
     with align-items/justify-content:center on this element, which was tried first and is a
     real bug: when #app is taller than its container, align-items:center pushes its top edge
     above y=0 with no way to scroll up far enough to reach it (scrollY can't go negative) —
     verified via Playwright, the page title became permanently unreachable. margin:auto
     doesn't have that failure mode: it resolves toward 0 once there's no free space to
     distribute, falling back to normal top-anchored flow instead of clipping upward. */
  display: flex;
}

#app {
  max-width: 480px;
  width: 100%;
  max-height: 100%;
  margin: auto;
  padding: 16px;
  padding-bottom: max(16px, env(safe-area-inset-bottom));
  /* Content is sized to fit within body's fixed height (see the html/body rules above) —
     this is a backstop, not the primary mechanism, so an unexpectedly-short viewport clips
     rather than silently breaking the no-scroll layout with a stray scrollbar. */
  overflow: hidden;
  /* Positioning context for .tutorial-corner-btn — pinned to #app's own corner, not
     header's, so it stays the single most top-right point on screen regardless of the
     header's own content height. */
  position: relative;
}

/* Two pieces stacked: the centered hero wordmark (top-aligned, 80% width — Ivan's request)
   and a bottom row holding paytable + balance right-aligned — replacing the old single-row
   "icon+text title on the left, icons+pill on the right" layout. The tutorial corner icon
   is no longer inside header at all (see index.html) — it's pinned to #app's own corner so
   it stays the true top-right corner of the screen regardless of the logo's height. */
header {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 10px;
}

.brand-hero {
  display: flex;
  justify-content: center;
  align-items: flex-start;
}

.brand-logo {
  width: 80%;
  height: auto;
  object-fit: contain;
}

/* Deliberately tiny and low-opacity ("barely visible", Ivan's exact words) — the tutorial
   is one-time learning content (see localStorage-gated auto-open in app.js's boot()), not
   something a returning player needs at header-icon prominence every session. Pinned to
   #app's corner (see index.html), not header's, so it's the single most top-right point on
   the whole screen. The button's own hit area (32px) stays comfortably tappable even
   though the visible icon inside it (14px, faded) reads as much smaller — an invisible
   hit-slop rather than shrinking the actual tap target to match the icon. */
.tutorial-corner-btn {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 5;
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  background: none;
  border-radius: var(--radius-pill);
  align-items: center;
  justify-content: center;
  opacity: 0.4;
}

/* :not([hidden]) — an author-origin rule beats the UA stylesheet's [hidden]{display:none}
   regardless of relative specificity (origin outranks specificity in the cascade), so a
   bare `display: inline-flex` here silently kept this button visible before boot() ever
   reveals it — including on the bare "No Telegram session found" screen, before auth even
   starts. Same bug, same fix, as #cashier-btn's own :not([hidden]) rule below; caught while
   verifying the demo channel hides its own UI correctly, not by inspection. */
.tutorial-corner-btn:not([hidden]) {
  display: inline-flex;
}

.tutorial-corner-btn:hover,
.tutorial-corner-btn:active {
  opacity: 0.85;
}

.tutorial-corner-btn img {
  width: 14px;
  height: 14px;
  object-fit: contain;
}

.header-bottom-row {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
}

/* Icon-only — was a text "Paytable" button, which combined with the full "Balance: N TON"
   pill genuinely didn't fit next to the title on a 390px-wide phone (that overflow was the
   actual cause of the header wrap/overlap flagged in the UI audit, not just a font-size
   issue). Same #paytable-btn id/click handler in app.js; only the visible label changed. */
#paytable-btn {
  width: 44px;
  height: 44px;
  padding: 0;
  border-radius: var(--radius-pill);
  font-size: 1rem;
  line-height: 1;
  align-items: center;
  justify-content: center;
}

/* :not([hidden]) — same cascade-origin bug/fix as .tutorial-corner-btn and #cashier-btn
   above: a bare `display` here beats the UA stylesheet's [hidden]{display:none} regardless
   of specificity, so this button was visible before boot() ever reveals it. */
#paytable-btn:not([hidden]) {
  display: inline-flex;
}

/* #balance-pill is the actual pill (background/border/radius) — #balance inside it is just
   the number text, kept separate so the "+" buy button can share the same pill without
   inheriting text styles. max-width+min-width:0 is a hard backstop: no matter how large a
   balance gets (repeated dev-grants, simulated purchases, or someday a genuinely high-volume
   player), the pill can never force the header to wrap the way a real device screenshot
   caught it doing (see Ivan's iPhone 12 Pro Max feedback, 2026-08-13) — #balance itself
   shrinks its font in two steps first (.balance-compact/.balance-tiny, set by app.js's
   render()) and only truncates with an ellipsis as the very last resort. */
#balance-pill {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--surface-1);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-pill);
  padding: 6px 6px 6px 12px;
  max-width: 52vw;
  min-width: 0;
}

#balance {
  font-family: var(--font-display);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  font-size: 0.95rem;
  overflow: hidden;
  text-overflow: ellipsis;
  min-width: 0;
}

#balance.balance-compact {
  font-size: 0.74rem;
}

#balance.balance-tiny {
  font-size: 0.6rem;
}

#cashier-btn {
  flex: none;
  width: 40px;
  height: 40px;
  padding: 0;
  border-radius: var(--radius-pill);
  background: var(--positive);
  color: #fff;
  border: none;
  font-size: 1.3rem;
  font-weight: 700;
  line-height: 1;
  align-items: center;
  justify-content: center;
}

/* :not([hidden]) — same reasoning as #auth-panel's own :not([hidden]) rule above: an ID
   selector's `display` outranks the browser's default `[hidden] { display: none }`
   (attribute selector), so a bare `display: inline-flex` here silently kept this button
   visible regardless of its `hidden` attribute — including for the demo channel, which
   deliberately never un-hides it (INVESTOR_DEMO_SPEC.md §2: no wallet UI of its own).
   Caught by playtesting the demo channel, not by inspection. */
#cashier-btn:not([hidden]) {
  display: inline-flex;
}

/* === Action bar (bet / start-cashout / win) ===
   One wrapped console instead of a full-width button with the bet row stacked above it
   (Ivan's request) — a decorative brass-bordered panel holding all three controls in a
   single row. Three EQUAL columns (Ivan's correction: "2 cells width" each) via CSS grid,
   same 6px gap as #grid below, so the row reads as the grid's own 6 columns grouped into
   three symmetrical pairs rather than three differently-sized controls. */
#action-bar {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: stretch;
  gap: 6px;
  padding: 7px;
  margin-bottom: 12px;
  background: var(--surface-1);
  border: 1px solid rgba(217, 164, 65, 0.35);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-1), 0 4px 16px rgba(217, 164, 65, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.04);
}

#bet-stepper {
  display: flex;
  align-items: stretch;
  width: 100%;
  min-width: 0;
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-md);
  overflow: hidden;
}

#bet-decrement-btn,
#bet-increment-btn {
  flex: none;
  width: 30px;
  height: 40px;
  padding: 0;
  border-radius: 0;
  background: transparent;
  color: var(--fg);
  border: none;
  font-size: 1.05rem;
  line-height: 1;
}

#bet-decrement-btn:not(:disabled):hover,
#bet-increment-btn:not(:disabled):hover {
  background: rgba(255, 255, 255, 0.06);
}

/* flex:1 (not a fixed width) — fills whatever's left in the stepper's grid column instead
   of leaving dead space next to the +/- buttons. min-width keeps the worst case (5-digit
   "10000", MAX_BET_NANO) from clipping even at the narrowest column width (~105px at
   375px viewport, the tightest of the three "2 grid cells" columns in #action-bar). */
#bet-input {
  flex: 1 1 auto;
  min-width: 2.6rem;
  height: 40px;
  padding: 4px 1px;
  border-radius: 0;
  border: none;
  border-left: 1px solid var(--border-soft);
  border-right: 1px solid var(--border-soft);
  background: transparent;
  color: var(--fg);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 0.78rem;
  text-align: center;
  -moz-appearance: textfield;
}

/* Custom +/-/Max buttons replace the native spinner, which would otherwise duplicate them. */
#bet-input::-webkit-outer-spin-button,
#bet-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* === Button system ===
   Previously every <button> shared one flat-green look, so the primary CTA, "Max", the
   paytable toggle, and the wallet/cashier actions all read as equally important. Three
   explicit variants restore hierarchy: .btn-primary for the one big commitment per screen,
   .btn-secondary for a bordered/lower-emphasis action, .btn-ghost for the quietest actions
   (dev tools, close buttons already styled inline). Existing #id selectors below still work
   unchanged for grid.js/app.js — only the *look* changes per variant class. */
button {
  padding: 10px 16px;
  border-radius: var(--radius-md);
  border: none;
  font-weight: 600;
  font-family: var(--font-body);
  cursor: pointer;
  transition: transform 0.1s ease, opacity 0.15s ease, box-shadow 0.2s ease;
  -webkit-tap-highlight-color: transparent;
}

button:disabled {
  opacity: 0.5;
  cursor: default;
}

button:active:not(:disabled) {
  transform: scale(0.96);
}

.btn-primary {
  background: linear-gradient(180deg, #5fc164 0%, #429a47 100%);
  color: #fff;
  box-shadow: var(--shadow-glow-positive);
  border: 1px solid rgba(255, 255, 255, 0.12);
}

.btn-secondary {
  background: var(--surface-2);
  color: var(--fg);
  border: 1px solid var(--border-soft);
}

.btn-secondary:not(:disabled):hover {
  border-color: rgba(255, 255, 255, 0.2);
}

.btn-ghost {
  background: transparent;
  color: var(--hint);
  border: 1px dashed var(--border-soft);
  font-weight: 500;
}

#grid-wrap {
  /* A couple extra px over the old 12px — the point of compacting the header (item 8 above)
     is to give some of that reclaimed height back to the grid, not just to shrink the header
     for its own sake. */
  margin: 16px 0;
  /* Anchors .bust-overlay below — that overlay is positioned absolute against this element,
     not the page, so it covers exactly the grid and nothing else. */
  position: relative;
}

#grid {
  display: grid;
  gap: 6px;
}

/* === Bust moment (Phase 2) ===
   A scrim + stamp over the grid, not a modal — see index.html's comment on #bust-overlay for
   why (losing is the common outcome; a tap-to-dismiss modal every time was too much
   friction). Shown/hidden purely by app.js toggling [hidden] off round.status === "busted",
   same idempotent per-render pattern as everything else in render(). */
.bust-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(ellipse at center, rgba(74, 31, 31, 0.55) 0%, rgba(15, 17, 21, 0.75) 100%);
  border-radius: var(--radius-md);
  pointer-events: none;
  z-index: 4;
}

.bust-overlay[hidden] {
  display: none;
}

.bust-stamp {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  color: var(--negative);
  text-shadow: 0 0 24px rgba(224, 92, 92, 0.55);
  border: 3px solid var(--negative);
  border-radius: var(--radius-sm);
  padding: 6px 18px;
  transform: rotate(-4deg);
  background: rgba(15, 17, 21, 0.35);
}

@media (prefers-reduced-motion: no-preference) {
  .bust-overlay:not([hidden]) .bust-stamp {
    animation: bustStampIn 0.3s ease-out;
  }
  @keyframes bustStampIn {
    from { transform: rotate(-4deg) scale(1.5); opacity: 0; }
    to { transform: rotate(-4deg) scale(1); opacity: 1; }
  }
}

/* :not([hidden]) on the display:flex, not a bare #auth-panel rule — same reasoning as
   .modal-overlay's own :not([hidden]) below: an ID selector outranks the browser's default
   `[hidden] { display: none }` (attribute selector), so a bare `display: flex` here would
   silently keep #auth-panel visible ("Connecting…" stuck on screen above the loaded game)
   even after app.js sets its `hidden` attribute once auth succeeds. Caught by playtesting
   this exact change, not by inspection. position:fixed+inset:0 makes this a true splash
   screen — it covers the header and game-panel behind it regardless of DOM order, since a
   fixed element paints in its own layer above normally-flowed siblings. */
#auth-panel:not([hidden]) {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  background: var(--bg) url("img/bg-app.jpg") center center / cover no-repeat;
}

.splash-logo {
  width: min(70vw, 280px);
}

#auth-status {
  margin: 0;
  color: var(--hint);
  font-size: 14px;
  text-align: center;
  padding: 0 24px;
}

.spinner {
  width: 14px;
  height: 14px;
  border: 2px solid var(--border-soft);
  border-top-color: var(--brass);
  border-radius: 50%;
  flex: none;
}

.spinner-lg {
  width: 28px;
  height: 28px;
  border-width: 3px;
}

@media (prefers-reduced-motion: no-preference) {
  .spinner {
    animation: spinnerRotate 0.8s linear infinite;
  }
  @keyframes spinnerRotate {
    to { transform: rotate(360deg); }
  }
}

/* The single always-visible action button — the middle of #action-bar's three equal
   columns (was a full-width bar below the grid) — reads "Start Round" (green) while
   idle/between rounds, or "Cash Out" while a round is active, tracking the pool's sign
   (red while behind, green while ahead) via .action-positive. See app.js's render(). */
#action-btn {
  display: block;
  width: 100%;
  min-width: 0;
  margin: 0;
  padding: 12px 3px;
  font-size: 0.76rem;
  font-family: var(--font-display);
  letter-spacing: 0.02em;
  background: linear-gradient(180deg, #ea7373 0%, #cf4f4f 100%);
  color: #fff;
  border: 1px solid rgba(255, 255, 255, 0.12);
  box-shadow: var(--shadow-glow-negative);
  transition: background 0.25s ease, box-shadow 0.25s ease;
}

#action-btn.action-positive {
  background: linear-gradient(180deg, #5fc164 0%, #429a47 100%);
  box-shadow: var(--shadow-glow-positive);
}

.cell {
  position: relative;
  aspect-ratio: 1;
  font-size: 1.25rem;
  background: var(--cell-bg);
  border: 1px solid var(--cell-border);
  border-radius: var(--radius-sm);
  color: var(--fg);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: transform 0.1s ease;
}

.cell:not([disabled]):active {
  transform: scale(0.92);
}

/* position: absolute takes the icon out of the button's normal flow entirely, so it can
   never influence the cell's own size — percentage width/height on a normal-flow <img>
   was creating a circular dependency with the grid's auto row-height (the button's height
   wasn't otherwise pinned down independent of its content), which was inflating revealed
   cells relative to their still-empty neighbors.
   Explicit percentage width/height (not `inset` shorthand + auto) is required here: an
   absolutely-positioned replaced element with width/height left auto falls back to its
   *intrinsic* size (this image's native 128px) rather than computing one from the insets,
   which briefly made every icon render far larger than its cell, overlapping neighbors. */
.cell img {
  position: absolute;
  top: 5%;
  left: 5%;
  width: 90%;
  height: 90%;
  object-fit: contain;
  pointer-events: none;
}

/* The empty-cell icon is deliberately a sparse, small mark within its own canvas (see
   docs/GRAPHIC_ASSETS_SPEC.md) — sized down further so it stays visually subtle rather
   than being blown up to the same prominence as an actual treasure. */
.cell.cell-empty img {
  top: 22.5%;
  left: 22.5%;
  width: 55%;
  height: 55%;
}

.mine-burst {
  z-index: 1;
}

.cell.revealed.cell-mine {
  background: #4a1f1f;
  border-color: #7a2c2c;
}

.cell.revealed.cell-ruby {
  background: #4a2f1f;
  border-color: #a05a1f;
}

.cell.revealed.cell-gem {
  background: #1f3a4a;
  border-color: #2f7aa0;
}

.cell.revealed.cell-diamond {
  background: #1f4a44;
  border-color: #2fa090;
}

.cell.revealed.cell-coin {
  background: #4a4420;
  border-color: #a09030;
}

.cell.revealed.cell-fragment {
  background: #3a2f4a;
  border-color: #7a5aa0;
}

.cell.revealed.cell-fragment.spent-fragment {
  background: transparent;
  border-style: dashed;
  border-color: var(--cell-border);
  opacity: 0.5;
  filter: grayscale(1);
}

.cell.revealed.cell-empty {
  background: transparent;
  border-style: dashed;
}

.cell.revealed.cell-unknown {
  background: var(--cell-bg);
  border-style: dashed;
  color: var(--hint);
}

.cell.revealed.not-picked {
  opacity: 0.55;
}

/* Compact win figure — the right-hand third of #action-bar (was a large standalone line
   above the grid, "Pool: N TON"; moved down here and relabeled "Win" per Ivan's request,
   same round.poolNano value and animatePoolDisplay tween as before). Fixed narrow width so
   a big number shrinks via its own font-size rather than pushing the button off-center. */
#pool {
  width: 100%;
  min-width: 0;
  min-height: 2.2em;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  font-family: var(--font-display);
  font-size: 0.78rem;
  font-weight: 600;
  line-height: 1.15;
  font-variant-numeric: tabular-nums;
  color: var(--hint);
  overflow: hidden;
  transition: color 0.2s ease;
}

.coin-suffix-icon {
  width: 13px;
  height: 13px;
  object-fit: contain;
  margin-left: 3px;
  vertical-align: -2px;
  flex: none;
}

#pool.pool-positive {
  color: var(--positive);
  text-shadow: 0 0 16px rgba(76, 175, 80, 0.35);
}

#pool.pool-negative {
  color: var(--negative);
  text-shadow: 0 0 16px rgba(224, 92, 92, 0.28);
}

/* === Progress strip (merged item-tracker/chest-progress/jackpot-progress) ===
   docs/UX_MAIN_SCREEN_ROADMAP.md item 7 — one consistent chip style for treasures, chest,
   and jackpot instead of chips-plus-two-plain-text-lines. overflow-x:auto + nowrap (not
   flex-wrap:wrap) is the point of this change: height is always exactly one chip tall
   regardless of how many chips are in play, where the old three-independent-rows layout
   could grow taller depending on board composition. */
#progress-strip {
  display: flex;
  gap: 6px;
  flex-wrap: nowrap;
  overflow-x: auto;
  margin-bottom: 8px;
}

/* Padding/gap trimmed (with the strip's own gap 8px->6px above) so all 6 chips fit without
   scrolling on real, common phone widths — iPhone 12 Pro Max (428px) was cut off by ~12px,
   barely more than a rounded corner with no visual hint it was scrollable, which read as
   broken rather than intentional; the standard 390px width (regular, non-Pro iPhones — one
   of the single most common phone widths in use) needed a further trim past the first pass
   to close its own 16px shortfall. Measured empirically at 375/390/414/428px, not assumed:
   390px+ now fits with zero overflow; only ~375px and below (iPhone SE/mini) still needs the
   scroll fallback, which is a real, expected narrow-device need there, not a rounding error. */
.item-badge {
  display: flex;
  align-items: center;
  gap: 3px;
  padding: 4px 6px;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  border: 1px solid var(--border-soft);
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  flex: none;
  white-space: nowrap;
}

.item-badge.item-badge-complete {
  border-color: var(--positive);
  color: var(--positive);
  font-weight: 600;
  box-shadow: 0 0 10px rgba(76, 175, 80, 0.25);
}

/* Always brass-treated, not conditionally like item-badge-complete above — the jackpot is
   correctly called out as the largest number the game shows (see UI audit §5) and keeps its
   distinct color in the merged strip rather than flattening back to neutral. */
.item-badge-jackpot {
  border-color: rgba(217, 164, 65, 0.35);
  background: rgba(217, 164, 65, 0.08);
  color: var(--brass-bright);
  font-weight: 600;
}

.item-badge-icon {
  width: 16px;
  height: 16px;
  object-fit: contain;
}

.item-badge.item-badge-pulse {
  animation: badgePulse 0.35s ease;
}

@keyframes badgePulse {
  0%, 100% { transform: scale(1); }
  40% { transform: scale(1.25); }
}

/* Still used by the paytable modal's inline reference icons (inlineIcon() in app.js) —
   unrelated to the progress strip above despite the similar name/size. */
.progress-icon {
  width: 16px;
  height: 16px;
  object-fit: contain;
  vertical-align: -3px;
}

#status {
  min-height: 1.2em;
  font-size: 0.9rem;
  margin: 4px 0;
}

/* Two-tier text hierarchy (docs/UX_MAIN_SCREEN_ROADMAP.md item 9) — app.js's statusMessage()
   tags every message as one or the other. "primary" is what the player needs to actually
   notice right now (error, bust, jackpot, an actionable balance warning); "secondary" is
   routine ambient instruction or a mild confirmation. #fairness and .modal-note are already
   permanently secondary-styled by nature (muted --hint color, regular weight) and don't need
   their own tier classes — only #status swings between genuinely different message weights
   often enough to need the dynamic distinction. */
#status.text-primary {
  color: var(--fg);
  font-weight: 600;
}

#status.text-secondary {
  color: var(--hint);
  font-weight: 400;
}

/* === Provably-fair badge (docs/UX_MAIN_SCREEN_ROADMAP.md item 10) ===
   Reserves its final 28×28px footprint UNCONDITIONALLY — empty and populated states used to
   be different sizes (empty: min-height:1.2em ≈ 19px; populated: a fixed 28px circle), and
   since #app is vertically centered via body's margin:auto, that ~9px height change on every
   round-end (the moment app.js's renderFairness() actually populates it) shifted #app's
   whole centered position — the entire UI visibly "jumped" up, not just this badge (Ivan's
   real-device report, 2026-08-29). Now the box is always 28×28 with a transparent
   border/background; :not(:empty) only changes color/cursor, never size, so there is nothing
   left to reflow — only the ✓ glyph fades in via its own ::before content existing at all. */
#fairness {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  margin: 4px 0;
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid transparent;
  background: none;
  color: transparent;
  font-size: 0;
  cursor: default;
  position: relative;
}

#fairness:not(:empty) {
  background: rgba(217, 164, 65, 0.08);
  border-color: rgba(217, 164, 65, 0.35);
  cursor: pointer;
}

#fairness:not(:empty)::before {
  content: "✓";
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-size: 0.95rem;
  font-weight: 700;
  color: var(--brass-bright);
}

#fairness:not(:empty):hover {
  border-color: rgba(217, 164, 65, 0.6);
}

/* === Dev tools ===
   Not present in the DOM at all until app.js's boot() decides isDev is true and injects the
   toggle button + modal — see index.html's comment on #dev-tools-slot. This is the mechanism
   behind the "hidden under a dev-style button, absent from a prod build" request: a non-dev
   session never has this markup to find, not just CSS display:none on it.
   position:fixed (not part of normal flow) so it costs zero layout height on the fixed,
   non-scrolling game screen — unlike the old inline toggle+expandable-panel, which pushed
   real content down whenever opened. */
.dev-toggle-btn {
  position: fixed;
  right: 10px;
  bottom: 10px;
  z-index: 40;
  padding: 8px 9px;
  font-size: 0.85rem;
  background: rgba(15, 17, 21, 0.7);
  color: var(--hint);
  border: 1px dashed var(--border-soft);
  border-radius: var(--radius-pill);
  opacity: 0.6;
}
.dev-toggle-btn:hover {
  opacity: 1;
}
.dev-modal-actions {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 4px;
}

/* === Cashier (wallet/deposit/withdraw) ===
   Lives inside #wallet-modal's .modal-card now (see index.html) — opened from the balance
   pill's "+" instead of sitting inline as a permanent panel (mobile-shell rebuild: a
   permanent panel cost too much screen height on a fixed, non-scrolling game screen, see
   Ivan's real-device feedback, 2026-08-13). .modal-card already supplies the card chrome
   (background/border/shadow) — this block only holds cashier-specific content styling. */
.cashier-title {
  font-family: var(--font-display);
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--hint);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0 0 10px;
}

#wallet-address {
  color: var(--hint);
  font-size: 0.85rem;
  margin: 6px 0;
  font-variant-numeric: tabular-nums;
}

#deposit-controls,
#withdraw-controls {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 12px;
}

#deposit-controls label,
#withdraw-controls label {
  font-size: 0.85rem;
  color: var(--hint);
}

#deposit-input,
#withdraw-input {
  width: 5.5rem;
  height: 40px;
  padding: 6px 10px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border-soft);
  background: var(--surface-2);
  color: var(--fg);
  font-family: var(--font-display);
}

#deposit-status,
#withdraw-status {
  min-height: 1.2em;
  color: var(--hint);
  font-size: 0.85rem;
  margin: 6px 0 0;
}

/* === Cashier per-state styling (Phase 2) ===
   Previously every deposit/withdraw state — awaiting confirmation, pending, confirmed,
   failed — rendered as identical muted-gray text, distinguished only by reading the copy
   (see UI audit §6). app.js's cashierStatusClass() maps the existing depositStatus /
   withdrawalStatus values to these three classes; no new states were introduced.
   Selectors repeat the #deposit-status/#withdraw-status ID so they out-specificity the
   plain-gray ID rule above — a bare .cashier-status-pending class lost to that id selector
   on first pass here (id specificity beats class regardless of source order), which quietly
   no-op'd every state color. */
#deposit-status.cashier-status-pending,
#withdraw-status.cashier-status-pending {
  color: var(--brass-bright);
}
#deposit-status.cashier-status-confirmed,
#withdraw-status.cashier-status-confirmed {
  color: var(--positive);
  font-weight: 600;
}
#deposit-status.cashier-status-failed,
#withdraw-status.cashier-status-failed {
  color: var(--negative);
}

.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
  align-items: center;
  justify-content: center;
  padding: 20px;
  z-index: 100;
}

/* `display: flex` must only apply while visible — otherwise it overrides the browser's
   default `[hidden] { display: none }` and a "hidden" modal still intercepts clicks. */
.modal-overlay:not([hidden]) {
  display: flex;
}

.modal-card {
  background: var(--modal-card-bg);
  border: 1px solid var(--border-soft);
  border-radius: var(--radius-lg);
  padding: 24px 22px;
  max-width: 380px;
  width: 100%;
  max-height: 80vh;
  overflow-y: auto;
  text-align: center;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}

.modal-card h2 {
  font-family: var(--font-display);
  margin: 0 0 14px;
  font-size: 1.15rem;
}

.modal-note {
  color: var(--hint);
  font-size: 0.85rem;
  text-align: left;
  margin: 8px 0;
}

/* === How-to-play tutorial ===
   Same .modal-card shell as every other modal, plus prev/next arrows pinned to its own
   left/right edges (position:relative on the card makes that the positioning context, not
   the viewport) — on a phone-width screen there's no usable space to put them outside the
   card itself. */
.tutorial-modal-card {
  position: relative;
  padding-left: 42px;
  padding-right: 42px;
  /* Keeps the card from visibly resizing page-to-page as body copy length varies. */
  min-height: 320px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.tutorial-arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-2);
  color: var(--fg);
  border: 1px solid var(--border-soft);
  font-size: 1.3rem;
  line-height: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.tutorial-arrow-left {
  left: 8px;
}

.tutorial-arrow-right {
  right: 8px;
}

.tutorial-page-icons {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 10px;
  /* Reserves space so the icon row's height doesn't jump between a 1-icon page and the
     4-treasure-icon page — min-height, not a fixed height, since the hero variant below is
     deliberately taller. */
  min-height: 56px;
  margin-bottom: 4px;
}

.tutorial-page-icon-img {
  width: 44px;
  height: 44px;
  object-fit: contain;
}

/* The jackpot page's closing flourish uses the same jackpot-hero.png as the real in-game
   celebration modal (see #jackpot-modal) instead of the small progress icon everything else
   here uses — deliberately bigger, matching how the real game treats that moment. */
.tutorial-page-icon-hero {
  width: 96px;
  height: 96px;
}

.tutorial-page-icon-brand {
  width: 40px;
  height: 40px;
}

#tutorial-page-title {
  margin-top: 0;
}

#tutorial-page-body {
  min-height: 4.2em;
}

#tutorial-dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin: 4px 0 16px;
}

.tutorial-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: var(--border-soft);
}

.tutorial-dot-active {
  background: var(--brass);
}

.tutorial-checkbox-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: var(--hint);
  font-size: 0.85rem;
  margin-bottom: 16px;
  cursor: pointer;
}

.tutorial-checkbox-row input {
  width: 16px;
  height: 16px;
  accent-color: var(--brass);
}

.paytable-table {
  width: 100%;
  border-collapse: collapse;
  text-align: left;
  font-size: 0.9rem;
}

.paytable-table th,
.paytable-table td {
  padding: 8px 4px;
  border-bottom: 1px solid var(--border-soft);
}

.paytable-table td:last-child,
.paytable-table th:last-child {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-family: var(--font-display);
}

/* Positive-payout rows read in the same green the pool/action-button use for "ahead" — the
   paytable previously gave every row identical neutral-white weight (see UI audit §4). */
.paytable-table td.positive {
  color: var(--positive);
  font-weight: 600;
}
.paytable-table td.negative {
  color: var(--negative);
  font-weight: 600;
}

.chest-modal-icon {
  width: 140px;
  height: 140px;
  object-fit: contain;
  margin-bottom: 8px;
}

#chest-modal-reward {
  font-family: var(--font-display);
  font-size: 1.4rem;
  font-weight: 600;
  color: var(--positive);
  margin: 8px 0 16px;
}

/* === Jackpot celebration (Phase 2) ===
   Wires in img/jackpot-hero.png — a hand-illustrated replacement for the earlier
   photorealistic jackpot-hero.jpg, which existed in the repo but had no UI to appear in
   (see UI audit — the single largest number the game shows had zero visual treatment). Brass
   border/glow instead of the chest modal's plain card border, so a jackpot still reads as a
   tier above a chest resolution even though both use the same .modal-card base. */
.jackpot-modal-card {
  border-color: rgba(217, 164, 65, 0.4);
  box-shadow: 0 12px 50px rgba(217, 164, 65, 0.25), 0 12px 40px rgba(0, 0, 0, 0.5);
}

/* Hand-illustrated hero art (trophy + the four treasure types + sunburst), isolated on a
   transparent background like .chest-modal-icon — not a cropped photo, so no aspect-ratio
   crop / border-radius / box-shadow frame. The ambient glow that used to be baked into the
   raster is done here instead via drop-shadow, which hugs the actual illustration silhouette
   (rays included) rather than a fixed circle, and keeps the shipped PNG far under budget. */
.jackpot-modal-hero {
  display: block;
  width: 210px;
  height: 210px;
  object-fit: contain;
  margin: 0 auto 10px;
  filter: drop-shadow(0 0 26px rgba(217, 164, 65, 0.5));
}

.jackpot-modal-card h2 {
  color: var(--brass-bright);
}

#jackpot-modal-reward {
  font-family: var(--font-display);
  font-size: 1.6rem;
  font-weight: 600;
  color: var(--brass-bright);
  text-shadow: 0 0 20px rgba(217, 164, 65, 0.4);
  margin: 8px 0 16px;
}

@media (prefers-reduced-motion: no-preference) {
  #jackpot-modal:not([hidden]) .jackpot-modal-hero {
    animation: chestBounce 0.4s ease-out;
  }
}

/* Fairness detail table (Phase 2) — reuses .paytable-table's row/border styling but the
   value column is seed/hash hex, not a right-aligned number, so it overrides that column's
   text-align/font from .paytable-table td:last-child. */
#fairness-detail-table td:first-child {
  color: var(--hint);
  font-size: 0.82rem;
}
#fairness-detail-table td:last-child {
  text-align: left;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.78rem;
}

/* Decorative motion only — press feedback above stays on regardless, since a tiny
   press-depth cue isn't the kind of motion prefers-reduced-motion is meant to suppress. */
@media (prefers-reduced-motion: no-preference) {
  /* A single random closed cell shakes at a time, at irregular 1.5-4s intervals (see
     grid.js's teaseRandomClosedCell/scheduleNextTease) — "teasing the player" rather than a
     synchronized sweep across the whole grid. One-shot (not infinite): grid.js adds this
     class to exactly one cell and removes it again on animationend. */
  .cell-tease {
    animation: cellTease 0.5s ease-in-out;
  }

  @keyframes cellTease {
    0%, 100% {
      transform: translateX(0) rotate(0deg);
    }
    20% {
      transform: translateX(-3px) rotate(-2deg);
    }
    40% {
      transform: translateX(3px) rotate(2deg);
    }
    60% {
      transform: translateX(-2px) rotate(-1deg);
    }
    80% {
      transform: translateX(2px) rotate(1deg);
    }
  }

  .cell-pop {
    animation: cellPop 0.22s ease-out;
  }

  @keyframes cellPop {
    from {
      transform: scale(0.55);
      opacity: 0;
    }
    to {
      transform: scale(1);
      opacity: 1;
    }
  }

  .cell-mine-hit {
    animation: mineShake 0.35s ease;
  }

  @keyframes mineShake {
    0%, 100% {
      transform: scale(1) rotate(0deg);
    }
    25% {
      transform: scale(1.15) rotate(-8deg);
    }
    75% {
      transform: scale(1.15) rotate(8deg);
    }
  }

  .mine-burst {
    animation: mineBurstPop 0.5s ease-out;
  }

  @keyframes mineBurstPop {
    0% {
      opacity: 0;
      transform: scale(0.3);
    }
    35% {
      opacity: 1;
      transform: scale(1.3);
    }
    100% {
      opacity: 0;
      transform: scale(1.6);
    }
  }

  .grid-shake {
    animation: gridShake 0.4s ease;
  }

  @keyframes gridShake {
    10%, 90% {
      transform: translateX(-4px);
    }
    20%, 80% {
      transform: translateX(4px);
    }
    30%, 50%, 70% {
      transform: translateX(-6px);
    }
    40%, 60% {
      transform: translateX(6px);
    }
  }

  /* Replays automatically every time — animations restart whenever an element goes from
     display:none to visible, which is exactly when these modals get shown. No JS needed. */
  .modal-card {
    animation: modalPop 0.2s ease-out;
  }

  @keyframes modalPop {
    from {
      transform: scale(0.9);
      opacity: 0;
    }
    to {
      transform: scale(1);
      opacity: 1;
    }
  }

  .chest-modal-icon {
    animation: chestBounce 0.4s ease-out;
  }

  @keyframes chestBounce {
    0% {
      transform: scale(0.4) rotate(-15deg);
      opacity: 0;
    }
    60% {
      transform: scale(1.15) rotate(6deg);
      opacity: 1;
    }
    100% {
      transform: scale(1) rotate(0deg);
    }
  }

  .floating-reward {
    animation: floatReward 1.8s ease-out forwards;
  }

  @keyframes floatReward {
    0% {
      opacity: 0;
      transform: translate(-50%, -60%) scale(0.8);
    }
    10% {
      opacity: 1;
      transform: translate(-50%, -70%) scale(1.15);
    }
    20% {
      transform: translate(-50%, -70%) scale(1);
    }
    /* Held near-static and fully opaque through the middle of the animation — this is the
       "can't even notice it" fix: most of the duration is spent readable, not moving/fading. */
    65% {
      opacity: 1;
      transform: translate(-50%, calc(-70% - 14px)) scale(1);
    }
    100% {
      opacity: 0;
      transform: translate(-50%, calc(-70% - 46px)) scale(1);
    }
  }
}

/* Positioning/layering for JS-created elements — always applied; the *motion* itself
   (the animate() call in app.js, the floatReward keyframe above) is what's gated behind
   prefers-reduced-motion, via a runtime matchMedia check in app.js for the JS-driven flight
   and the media query above for the CSS-driven float. Without motion, these still need to
   not be statically visible/misplaced, so a reduced-motion run just skips creating them. */
.floating-reward {
  position: fixed;
  pointer-events: none;
  font-weight: 700;
  font-size: 1.05rem;
  z-index: 200;
  transform: translate(-50%, -50%);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
  background: rgba(0, 0, 0, 0.65);
  padding: 3px 9px;
  border-radius: 10px;
  white-space: nowrap;
}
.floating-reward.reward-positive {
  color: var(--positive);
}
.floating-reward.reward-negative {
  color: var(--negative);
}

.flying-icon {
  position: fixed;
  pointer-events: none;
  z-index: 200;
  object-fit: contain;
}

/* z-index above .modal-overlay (100) so the burst reads as bursting out of/over the open
   chest modal rather than being hidden behind it. Motion itself is driven by a per-particle
   animate() call in app.js (randomized per coin), which already checks
   prefers-reduced-motion before creating any of these elements — nothing to gate here. */
.coin-burst-particle {
  position: fixed;
  pointer-events: none;
  z-index: 300;
  object-fit: contain;
}
