

/* Family names are aliases: swapping in a licensed face later is a one-line
   src change and nothing else in the codebase references the real name.
   Inter + Oswald, SIL OFL 1.1 — licences live next to the files. */
@font-face {
  font-family: "Vault UI";
  /* Subset to the characters the game can show and the weights it uses (400-900): 48 -> 28 KB. */
  src: url("assets/fonts/inter-sub-wght-400-900.woff2") format("woff2");
  font-weight: 400 900;
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2010-2027, U+2030-203A, U+2044, U+20A0-20CF, U+2122, U+2190-2199, U+2212, U+221E, U+2260, U+2264-2265;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Vault Display";
  src: url("assets/fonts/oswald-sub-wght-400-700.woff2") format("woff2");
  font-weight: 400 700;
  unicode-range: U+0020-007E, U+00A0-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2010-2027, U+2030-203A, U+2044, U+20A0-20CF, U+2122, U+2190-2199, U+2212, U+221E, U+2260, U+2264-2265;
  font-style: normal;
  font-display: swap;
}

:root {
  color-scheme: dark;

  /* — Surfaces: the vault, darkest → lightest — */
  --bg-vault-door:     #07080A; /* page backdrop, behind everything */
  --bg-gunmetal:       #111418; /* board well / reel table core */
  --bg-panel:          #191D22; /* HUD panels, modals */
  --bg-panel-raised:   #22272E; /* hover / pressed controls */
  --steel-edge:        #3A414A; /* hairlines, frames */
  --steel-brushed:     #6B7480; /* brushed-steel mid tone, disabled ink */
  --steel-sheen:       #B7BFC9; /* specular edge on bezels and frames */

  /* — Practical light — */
  --tungsten:          #FFB86B; /* warm lamp glow: idle fog, spin flash */
  --tungsten-soft:     #FFD9A8;

  /* — Accent: security laser — */
  --accent-laser:      #FF2A3A;
  --accent-laser-deep: #A30A16;
  --accent-laser-hot:  #FF7A84; /* beam core; laser-coloured text on dark */

  /* — Wins: vault gold — */
  --win-gold:          #F2C14E;
  --win-gold-bright:   #FFE7A3;
  --win-gold-deep:     #9C6B12;

  /* — Win aura per symbol (the glow behind a winning symbol; #rrggbb, read by THEME.aura) — */
  --aura-crown:        #FFC94A;
  --aura-hourglass:    #3FE0B5;
  --aura-ring:         #FFB86B;
  --aura-chalice:      #E070FF;
  --aura-red:          #FF4D5E;
  --aura-purple:       #A66BFF;
  --aura-yellow:       #FFD23F;
  --aura-green:        #3FE07A;
  --aura-blue:         #4FD8FF;

  /* — Glass surfaces (board + floating HUD pills) — */
  --glass-fill:        rgba(0, 0, 0, 0.40);   /* blur available */
  --glass-fill-solid:  rgba(8, 10, 14, 0.78); /* no backdrop blur (handhelds, perf-low, unsupported) */
  --glass-edge:        rgba(255, 255, 255, 0.10);
  --glass-sheen:       rgba(255, 255, 255, 0.06);
  --glass-shadow:      0 25px 50px -12px rgba(0, 0, 0, 0.60);
  --glass-blur:        blur(24px) saturate(125%);

  /* — Text — */
  --text-hud:          #EEF1F4;
  --text-muted:        #8E98A4;
  --text-dim:          #5A636E;

  /* — Signal — */
  --signal-ok:         #3FD68A; /* armed / connected */
  --signal-warn:       #FFB020;

  /* — Win tiers (see WIN_TIERS in main.js) — */
  --tier-nice:         #B7BFC9;
  --tier-big:          #F2C14E;
  --tier-mega:         #FF2A3A;
  --tier-epic:         #FFE7A3;
  --tier-legendary:    #7FF6E6;
  --tier-max:          #FFFFFF;

  /* — Multiplier rarity: cold steel → hot laser — */
  --mult-common:       #9AA8B6;
  --mult-rare:         #4FC3F7;
  --mult-epic:         #FF7A2B;
  --mult-legendary:    #F2C14E;
  --mult-mythic:       #FF2A3A;

  /* — Type — */
  --font-ui:      "Vault UI", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-display: "Vault Display", "Arial Narrow", "Roboto Condensed", Impact, sans-serif;

  /* — Elevation & glow — */
  --shadow-soft: 0 18px 40px rgba(0, 0, 0, 0.45);
  --shadow-hard: 0 34px 80px rgba(0, 0, 0, 0.65);
  --glow-laser:  0 0 18px color-mix(in srgb, var(--accent-laser) 55%, transparent);
  --glow-gold:   0 0 22px color-mix(in srgb, var(--win-gold) 60%, transparent);

  /* — Legacy aliases. ~20 existing rules still read these; each one is retired
       as its component is rebuilt. Do not add new uses. — */
  --bg-0: var(--bg-vault-door);
  --bg-1: var(--bg-gunmetal);
  --bg-2: var(--bg-panel);
  --panel:       color-mix(in srgb, var(--bg-panel) 88%, transparent);
  --panel-soft:  color-mix(in srgb, var(--bg-panel) 72%, transparent);
  --panel-deep:  color-mix(in srgb, var(--bg-gunmetal) 95%, transparent);
  --ink:         var(--text-hud);
  --ink-soft:    var(--text-muted);
  --line:        color-mix(in srgb, var(--steel-sheen) 14%, transparent);
  --line-strong: color-mix(in srgb, var(--steel-sheen) 32%, transparent);
  --sun:   var(--win-gold);
  --coral: var(--accent-laser);
  --jade:  var(--signal-ok);
  --wine:  var(--accent-laser-deep);
}

/* Balances, bets and wins: industrial display face, fixed-width digits so a
   counting value never jitters sideways. */
.u-num {
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums lining-nums;
  letter-spacing: 0.02em;
}

* {
  box-sizing: border-box;
}

html,
body {
  margin: 0;
  min-height: 100%;
}

body {
  font-family: var(--font-ui);
  color: var(--ink);
  overflow-x: hidden;
  /* Mobile input hardening: no double-tap zoom delay, no rubber-band/pull-to-
     refresh, and no grey tap flash on touch. */
  touch-action: manipulation;
  overscroll-behavior: none;
  -webkit-tap-highlight-color: transparent;
}

body::before,
body::after {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
}

.hidden {
  display: none !important;
}

/* ── Notification skeleton (#49, client/notify.js) ──
   Every popup root carries .vx-n .vx-n--<kind> and data-vx-state. Each kind
   keeps its own position and look (all outside the document flow); these are
   the shared rules: a closed popup never takes a click, and a skin's art
   (VxNotify.skin: assets.art → --vx-n-art) shows in [data-vx-slot="art"]. */
.vx-n[data-vx-state="closed"] { pointer-events: none; }
.vx-n [data-vx-slot="art"] { background: var(--vx-n-art, none) center / contain no-repeat; }

.sanctum {
  width: min(1540px, 100%);
  margin: 0 auto;
  padding: 18px;
}

.altar {
  position: relative;
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: 18px;
  /* Transparent: the chamber art on <body> is the backdrop (art, not CSS). */
  display: grid;
  gap: 16px;
}

/* Guardian-priestess host. Stands at the bottom-right of the game panel beside
   the reel table, in front of the temple backdrop. Decorative only (no pointer
   events). Tune side/size below; hidden on narrow + portrait where there's no
   room without covering the reels or controls. */
.stage-character {
  position: absolute;
  right: max(0px, env(safe-area-inset-right));
  bottom: 0;
  height: clamp(300px, 46vh, 620px);
  width: auto;
  z-index: 3;
  pointer-events: none;
  user-select: none;
  filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.55));
}

@media (max-width: 1024px), (max-aspect-ratio: 1 / 1) {
  .stage-character {
    display: none;
  }
}

.oracle-band,
.archive-band,
.codex-band {
  border: 1px solid var(--line);
  background: linear-gradient(180deg, var(--panel-soft), var(--panel-deep));
  box-shadow: inset 0 1px 0 rgba(255, 245, 221, 0.04), var(--shadow-soft);
}

.summary-grid span,
.sim-grid span,
#spinLogStatus {
  color: var(--ink-soft);
  text-transform: uppercase;
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.06em;
}

button,
select {
  font: inherit;
}

/* The page's button look stops at the shell (its controls are the same in every game). */
button:not(:where(.lsh *, .lsh-sheet *, .lsh-panel *)) {
  min-height: 58px;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  padding: 10px 14px;
  font-weight: 900;
  color: #1e1208;
  cursor: pointer;
  transition: transform 0.16s ease, filter 0.16s ease;
}

button:not(:where(.lsh *, .lsh-sheet *, .lsh-panel *)):hover:not(:disabled) {
  transform: translateY(-1px);
  filter: brightness(1.04);
}

button:not(:where(.lsh *, .lsh-sheet *, .lsh-panel *)):disabled {
  opacity: 0.58;
  cursor: not-allowed;
}

#simulateBtn,
#simulateBonusBtn {
  background: linear-gradient(180deg, #ecdcb7, #b89c6d);
}

/* Bonus total-multiplier badge — pinned top-center over the reels during free
   spins. Soft glow + gentle pulse that intensifies with the multiplier tier,
   and a pop on each increase. */
.bonus-multi-badge {
  position: absolute;
  /* Anchored to the TOP EDGE OF THE BOARD, not the panel. --board-y/--board-h
     are published by ReelCanvasRenderer.publishBoardBox() as percentages of
     .vault-window, so this tracks the letterbox at every viewport size. The
     fallbacks keep it sane for the one frame before the first resize() lands.
     Previously this was `top: 12px`, which floated the badge in the dead space
     above the grid whenever the board was letterboxed. */
  top: calc(var(--board-y, 0%) + var(--board-h, 100%) * 0.02);
  left: calc(var(--board-x, 0%) + var(--board-w, 100%) * 0.5);
  transform: translateX(-50%);
  z-index: 7;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  /* Scales with the board instead of sitting at a fixed 6px/20px. */
  padding: clamp(3px, 0.7vmin, 8px) clamp(10px, 2.4vmin, 24px);
  border-radius: clamp(8px, 1.4vmin, 16px);
  text-align: center;
  pointer-events: none;
  --badge-glow: rgba(255, 226, 138, 0.4);
  background: linear-gradient(180deg, rgba(26, 20, 46, 0.92), rgba(12, 10, 26, 0.96));
  border: 1px solid rgba(255, 226, 138, 0.5);
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.45), 0 0 16px var(--badge-glow);
}

/* The pulse used to animate `box-shadow` on the badge itself, which is not a
   composited property — it repainted the badge every frame for the WHOLE free
   spins round. Same look, but the wide-glow state now lives on a pseudo-element
   that only cross-fades its opacity, which the compositor handles on the GPU. */
.bonus-multi-badge::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 32px var(--badge-glow);
  opacity: 0;
}
/* One glow swell when the total grows (no idle motion: a resting badge is still). */
.bonus-multi-badge.badge-bump::after { animation: bonusBadgePulse 0.9s ease-in-out 1; }

/* No multiplier caught yet in this bonus (#46): no "1x". The badge stays laid
   out (so the first soul can fly to it) but invisible; the first catch fades
   it in. Opacity + transform only: composited, never moves anything else. */
.bonus-multi-badge {
  transition: opacity 0.22s ease-out, transform 0.22s cubic-bezier(0.2, 0.9, 0.2, 1);
}
.bonus-multi-badge.is-empty {
  opacity: 0;
  transform: translateX(-50%) scale(0.85);
}
.bonus-multi-badge-label {
  font: 800 clamp(0.42rem, 1.3vmin, 0.6rem)/1 "SF Pro Display", "Segoe UI", sans-serif;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: #cbb985;
}
.bonus-multi-badge-value {
  font: 900 clamp(1rem, 3.6vmin, 1.7rem)/1 "SF Pro Display", "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  color: #ffe28a;
  text-shadow: 0 0 14px rgba(255, 226, 138, 0.6);
  will-change: transform;
}

.bonus-multi-badge[data-tier="rare"] { --badge-glow: rgba(155, 231, 255, 0.5); border-color: rgba(155, 231, 255, 0.55); }
.bonus-multi-badge[data-tier="rare"] .bonus-multi-badge-value { color: #bfeaff; text-shadow: 0 0 16px rgba(155, 231, 255, 0.65); }
.bonus-multi-badge[data-tier="epic"] { --badge-glow: rgba(190, 214, 255, 0.55); border-color: rgba(190, 214, 255, 0.62); }
.bonus-multi-badge[data-tier="epic"] .bonus-multi-badge-value { color: #d8e4ff; text-shadow: 0 0 18px rgba(190, 214, 255, 0.7); }
.bonus-multi-badge[data-tier="legendary"] { --badge-glow: rgba(255, 165, 224, 0.6); border-color: rgba(255, 165, 224, 0.68); }
.bonus-multi-badge[data-tier="legendary"] .bonus-multi-badge-value { color: #ffc6ec; text-shadow: 0 0 20px rgba(255, 165, 224, 0.78); }
.bonus-multi-badge[data-tier="mythic"] { --badge-glow: rgba(255, 205, 130, 0.7); border-color: rgba(255, 210, 140, 0.78); }
.bonus-multi-badge[data-tier="mythic"] .bonus-multi-badge-value { color: #ffd98c; text-shadow: 0 0 24px rgba(255, 210, 140, 0.88); }

/* The sum the total is made of (5x + 10x), under it, once two or more add up. */
.bonus-multi-badge-sum {
  font: 700 clamp(0.62rem, 1.9vmin, 0.85rem)/1.1 "SF Pro Display", "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.78);
  white-space: nowrap;
}
.bonus-multi-badge-sum:empty { display: none; }

@keyframes bonusBadgePulse {
  0%, 100% { opacity: 0; }
  50%      { opacity: 1; }
}
/* An empty badge is invisible (opacity 0, laid out for the souls to fly to: every
   base-game round). Its glow must not pulse behind it: an infinite animation asks
   the compositor for a new frame on every vsync, for nothing on screen. It starts
   with the first catch, exactly when the badge fades in. */
.bonus-multi-badge.is-empty::after { animation: none; }

/* Pop the value (not the badge) on each increase so it never collides with the
   badge's infinite glow pulse. */
.bonus-multi-badge.badge-bump .bonus-multi-badge-value {
  animation: bonusBadgeBump 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.4);
}
@keyframes bonusBadgeBump {
  0% { transform: scale(1); }
  40% { transform: scale(1.32); }
  100% { transform: scale(1); }
}

/* Richer themed reel frame — warmer inner glow reads more premium. */
body.public-mode .reels-stage {
  border: 1px solid rgba(245, 214, 169, 0.22);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.4),
    inset 0 0 44px rgba(238, 183, 106, 0.07),
    0 10px 40px rgba(0, 0, 0, 0.5);
}

.altar-core {
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr) 340px;
  gap: 16px;
  align-items: start;
}

/* Public (player) mode — fullscreen single-viewport game. Hides every admin
   panel and locks the layout to 100vw × 100vh with no scroll. */
body.public-mode .admin-only {
  display: none !important;
}

html:has(body.public-mode),
body.public-mode {
  height: 100vh;
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  overflow: hidden;
  /* In the casino iframe the player should never select/drag the chrome. The
     Round-ID value re-enables selection on itself (user-select: all). */
  -webkit-user-select: none;
  user-select: none;
}

body.public-mode .sanctum {
  width: 100vw;
  max-width: 100vw;
  height: 100vh;
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  padding: 0;
  margin: 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* ── ONE SCALING CONTAINER ─────────────────────────────────────────────────
   .altar is the single layout root: it owns the whole viewport and every
   dimension below it is derived from the scale tokens declared here, so the
   header, HUD, board and bottom bar grow and shrink together as one unit.

   The tokens are viewport-relative on purpose. vmin drives anything that must
   stay usable in BOTH orientations (tap targets), vh drives the vertical
   chrome budget, and every clamp() carries a floor so nothing collapses on a
   320px phone and a ceiling so nothing sprawls on a 4K monitor.

   There is deliberately NO max-width in px here. The old rule capped the game
   at 1040px, which on a 1920px screen left ~440px of dead backdrop down each
   side while the board itself stayed small. */
body.public-mode .altar {
  /* Vertical rhythm between the stacked rows. */
  --gap: clamp(3px, 0.9vh, 10px);
  /* Chrome scale: the SCALED ROOT EM every control derives from. Use it as
     `calc(<multiple> * var(--ui))` — e.g. `calc(0.58 * var(--ui))` is "0.58rem,
     scaled" — and `calc(<px> * var(--ui) / 16)` for pixel quantities.

     This is a LENGTH, deliberately. It used to be the unitless ratio
     `clamp(0.72, min(100vw / 480, 100vh / 900), 1.25)`, which is invalid CSS:
     `100vw / 480` is a length (a viewport unit divided by a number), so the
     clamp mixed a length against the unitless bounds 0.72 and 1.25. Every
     `calc(Xrem * var(--ui))` built on it was therefore invalid at computed-value
     time and fell back to `inherit` — which is why the whole proportional-chrome
     system silently did nothing and HUD labels rendered at the inherited 16px
     instead of ~9px, crowding and ellipsizing the bar (#25's "controls and
     spacing become awkward"). Folding the 16px in keeps every existing
     multiplier's meaning identical while making the token valid:
       11.52px = 0.72 * 16   |   100vw / 30 = (100vw / 480) * 16
       20px    = 1.25 * 16   |   100vh / 56.25 = (100vh / 900) * 16

     The ramp is then re-calibrated against the phone rather than the old 480px
     reference. Because the token never actually worked, its numbers had only
     ever been checked against the inherited 16px fallback; a literal port put
     HUD micro-labels at 7.5px on a 390px phone, which fails #25's "readable
     controls, not too small on mobile". The floor and the width reference are
     raised so a 390px phone lands near 14px (labels ~9.5px, values ~15px) and a
     desktop caps at 21px. */
  --ui: clamp(12.5px, min(100vw / 28, 100vh / 52), 21px);
  /* Tap-target sizing. Driven by vh, not vmin: these are stacked vertically, so
     the height budget is what decides whether the board or the chrome wins. On
     a 1920x1080 desktop vmin is 1080, which made every control hit its ceiling
     and left the chrome eating ~430px — the board got only 650px of 1080.
     vw is folded in via min() so ultra-wide short windows stay sane. */
  --btn: clamp(30px, min(6.4vh, 9vw), 54px);
  --spin: clamp(52px, min(11.5vh, 17vw), 104px);
  --radius: clamp(8px, 1.4vmin, 18px);
  /* Reserved heights for the two chrome rows that change CONTENT during play —
     the message rail and the HUD strip. Both are a FIXED budget, never content
     driven, so nothing they display can resize the board. See the
     .hud-message-rail and .hud-bar rules for the bugs that forced this. */
  --rail: clamp(16px, 2.4vh, 28px);
  --hud-h: clamp(38px, 5.6vh, 62px);

  width: 100vw;
  max-width: 100vw;
  height: 100vh;
  height: 100dvh;
  max-height: 100vh;
  max-height: 100dvh;
  margin: 0;
  border-radius: 0;
  /* FLEX, not grid-with-fixed-rows. `grid-template-rows: auto minmax(0,1fr)`
     assigns rows by child ORDER, and .altar-top is display:none in landscape —
     which promoted .altar-core into the `auto` row and left it content-sized
     (190px of a 390px viewport), collapsing the board to 2px. Flex assigns the
     free space by flex-grow, so a hidden header simply drops out. */
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  /* Pad for notches / home indicator on phones; falls back to the clamp floor. */
  padding:
    max(clamp(2px, 0.6vh, 8px), env(safe-area-inset-top))
    max(clamp(4px, 1.2vw, 18px), env(safe-area-inset-right))
    max(clamp(2px, 0.6vh, 8px), env(safe-area-inset-bottom))
    max(clamp(4px, 1.2vw, 18px), env(safe-area-inset-left));
  overflow: hidden;
}
body.public-mode .altar-core {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  display: block;
  grid-template-columns: none;
  gap: 0;
}

/* THE BOARD ROW — the only row that grows. Everything else is content-sized, so
   whatever the chrome does not use belongs to the grid. `min-height: 0` is what
   lets it shrink on short viewports rather than pushing the deck off-screen. */
body.public-mode .vault-window {
  flex: 1 1 auto;
  min-height: 0;
  height: auto;
  width: 100%;
  display: flex;
  /* No top bar: the board sits as high as it can. Where it is width-limited
     (portrait) the spare height goes below it, not above. */
  align-items: flex-start;
  justify-content: center;
  padding: clamp(2px, 0.5vmin, 8px);
  margin: 0;
  /* Now that flex gives this a DEFINITE resolved height, cqh below is real.
     Previously this sat in an auto row, so cqh resolved against an indefinite
     height — which is what collapsed the board to ~2px in landscape. */
  container-type: size;
}

/* Everything that is not the board holds its natural height. */
body.public-mode .round-id-bar {
  flex: 0 0 auto;
}

body.public-mode .reels-stage {
  /* Keep the engine's true 6/5 board shape on EVERY viewport. Setting both
     width:100% and height:100% makes the browser ignore aspect-ratio, so the
     board would stretch to the container's shape. Bind whichever axis is
     limiting instead: 120cqh is 6/5 * 100cqh, so min() picks the width that
     fits both dimensions → pillarbox on wide-short containers, letterbox on
     tall-narrow ones. Because .vault-window now has a definite height, both
     cq units are meaningful in every orientation.

     This is the ONLY place the board's size is decided. The per-breakpoint
     overrides that used to re-declare width/height/aspect (and disagreed with
     each other — desktop said 3/2 while the canvas draws 6/5) are gone. */
  aspect-ratio: 6 / 5;
  width: min(100cqw, 120cqh);
  height: auto;
  min-height: 0;
  max-width: 100%;
  max-height: 100%;
  /* Centred across, top-aligned down: with no top bar the board sits as high
     as it can, and spare height (portrait) falls below it. */
  margin: 0 auto;
}

/* NOTE: .altar-core / .vault-shell / .vault-window used to be re-declared here
   with a second, slightly different set of values. Two sources of truth for the
   same boxes is what let the board's sizing drift out of sync in the first
   place, so they now live once, in the ONE SCALING CONTAINER block above. */

body.public-mode .reels-canvas {
  width: 100% !important;
  height: 100% !important;
}

/* On any roomy viewport the board is height-bound, so let the temple backdrop
   show through the side margins instead of a grey box around the reel frame.
   Purely cosmetic — no sizing here. All dimensions come from the scale tokens
   on .altar, so there is nothing per-breakpoint left to declare. */
@media (min-width: 1000px) and (min-height: 700px) {
  body.public-mode .vault-window {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
  }
}

/* Same zero-reflow contract for the Round ID strip. It is written fresh after
   EVERY resolved round (platform mode), and its base styling is `flex-wrap:
   wrap` + `word-break: break-all` — so a round ref one character too long for
   the viewport wrapped to a second line and resized the board mid-round, again
   once per free spin. In player view it is one fixed-height line, clipped. */
body.public-mode .round-id-bar {
  flex-wrap: nowrap;
  height: var(--rail);
  min-height: var(--rail);
  max-height: var(--rail);
  padding: 0 calc(8 * var(--ui) / 16);
  margin-top: 0;
  gap: calc(6 * var(--ui) / 16);
  overflow: hidden;
}
body.public-mode .round-id-value {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  word-break: normal;
  padding: 0 calc(5 * var(--ui) / 16);
  font-size: calc(0.7 * var(--ui));
}
body.public-mode .round-id-label,
body.public-mode .round-id-copy,
body.public-mode .round-id-copied {
  flex: 0 0 auto;
  white-space: nowrap;
  font-size: calc(0.58 * var(--ui));
}
body.public-mode .round-id-copy {
  padding: calc(2 * var(--ui) / 16) calc(7 * var(--ui) / 16);
}

/* Portrait phones/tablets. The board is width-bound here, so the only job left
   is to reclaim horizontal pixels — symbol size is boardWidth/6, so every pixel
   of side padding costs symbol size. No sizing overrides: the board rule and the
   scale tokens already handle it. */
@media (orientation: portrait) {
  body.public-mode .altar {
    padding-left: max(2px, env(safe-area-inset-left));
    padding-right: max(2px, env(safe-area-inset-right));
    /* A 6/5 board on a tall phone is WIDTH-bound: at 390px wide it can only be
       325px tall, so a 844px viewport has ~230px of slack the board physically
       cannot use. Rather than let that pool as empty backdrop above and below
       the grid (the "large empty areas" this refactor is fixing), spend it on
       the bottom controls — which is also where a thumb wants them. Still no
       fixed pixels: both tokens are bound by width AND height so they cannot
       overflow a short or narrow phone. */
    /* Portrait --spin is derived from the spare height in PLAYER SHELL. */
  }

  /* Full-bleed board: drop the window's inner padding + dark fill so the 6/5
     board spans the full screen width and the space around it shows the temple
     backdrop rather than a grey box. */
  body.public-mode .vault-window {
    padding: 0;
    background: transparent;
    border-color: transparent;
    box-shadow: none;
  }
}

/* Landscape on short viewports: the header is the first thing to go, since the
   board is height-bound and every vertical pixel is board size. */
@media (orientation: landscape) and (max-height: 560px) {
  
  
  /* The Round ID is a support/compliance readout, not something read mid-spin,
     and .altar-top is already hidden at this size for the same reason. It stays
     available in portrait and on every larger viewport. */
  body.public-mode .round-id-bar {
    display: none;
  }
  /* 5.6vh floors at 38px here, which is a tenth of a 390px viewport for two
     short lines of text. Tighten the floor; the nodes are still single-line and
     clipped, so the bar cannot grow when the bonus nodes appear. */
  body.public-mode .altar {
    --hud-h: clamp(30px, 8.5vh, 44px);
  }
}

/* ── PLAYER SHELL (Pragmatic-style) ──────────────────────────────────────────
   The player view is one grid of NAMED areas around the board:

        desktop / landscape                      portrait
     ┌──────┬─────────────┬──────┐          ┌─────────────────┐
     │      │             │ logo │          │      logo       │
     │ left │   center    ├──────┤          │     center      │
     │ buy  │   (board)   │      │          │     (board)     │
     │ ante │             │mascot│          ├────────┬────────┤
     ├──────┴─────────────┴──────┤          │  buy   │  ante  │
     │ sys │   status   │  spin  │          ├────────┴────────┤
     └─────┴────────────┴────────┘          │  spin (−  ●  +) │
                                            │ sys  │  status  │
                                            └─────────────────┘

   Zero-reflow contract:
     • Every track size comes from a scale token, never from content. The only
       flexible track is the board's, and nothing else can grow into it.
     • Zones are placed by grid-area NAME, not by child order, so hiding
       something (a slot, a bonus node) can never shift the board
       to another track. Row-counted layouts broke exactly that way before
       (see the ONE SCALING CONTAINER notes).
     • Text that changes during play (ticker, balance, bet) is single-line,
       clipped and tabular inside a fixed-size box.

   Art: the logo and mascot .ps-slot boxes paint var(--slot-art), contained and
   centred. Set it on the slot, e.g.
   `.ps-slot--logo { --slot-art: url(assets/ui/logo.webp); }`, and drop
   `data-empty` from the element to hide the placeholder label. The controls
   (spin, buy, ante) are vector: no raster art at all. */

body.public-mode .altar {
  --side-w: clamp(150px, 20vw, 380px);          /* left + right columns */
  --deck-h: clamp(52px, 9vh, 96px);             /* bottom control deck */
  --sub-h: clamp(28px, 4.6vh, 40px);            /* autoplay / turbo row under spin */
  --feat-h: clamp(40px, 9vh, 96px);             /* buy + ante buttons */
  --bet-btn: max(36px, calc(var(--spin) * 0.46)); /* − / + beside spin */
  --sys-btn: clamp(36px, 5vh, 44px);            /* menu / info / sound */
}

body.public-mode .vault-shell {
  display: grid;
  grid-template-columns: var(--side-w) minmax(0, 1fr) var(--side-w);
  grid-template-rows: minmax(0, 1fr) var(--deck-h);
  grid-template-areas:
    "left center right"
    "deck deck   deck";
  gap: var(--gap);
  width: 100%;
  max-width: none;
  height: 100%;
  min-height: 0;
  margin: 0;
}

/* The board column: the vertical stack the ONE SCALING CONTAINER block
   describes (HUD, round id, message rail, board). The board row is the only
   one that grows. */
body.public-mode .ps-center {
  grid-area: center;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

/* The two global `button` rules (58px min-height, bevel shadows, hover lift)
   style the legacy admin buttons. The shell's own buttons opt out here so
   each one is exactly the size and surface its own rule gives it. */
.ps-icon-btn {
  min-height: 0;
  box-shadow: none;
  text-shadow: none;
}

/* ── Touch targets (Step 6) ──
   Every control's HIT area is at least 44×44 CSS px (Apple HIG; WCAG 2.5.5),
   even where its painted size is smaller to give the board more room. An
   invisible ::after grows the hit area outward around the control's centre;
   it is out of flow, so nothing moves and the board is untouched. inset
   percentages resolve per axis (top/bottom against height, left/right against
   width), so each axis grows only as far as it needs to reach 44px. */
.ps-icon-btn {
  position: relative;
}
.ps-icon-btn::after {
  content: "";
  position: absolute;
  inset: min(0px, calc((100% - 44px) / 2));
}
.ps-icon-btn {
  width: var(--sys-btn, 40px);
  height: var(--sys-btn, 40px);
  min-width: 0;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: 50%;
  border: 1px solid var(--steel-edge);
  background: radial-gradient(circle at 50% 30%, var(--bg-panel-raised), var(--bg-gunmetal));
  color: var(--steel-sheen);
  cursor: pointer;
}
.ps-icon-btn svg {
  width: 56%;
  height: 56%;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ps-icon-btn:hover { color: var(--text-hud); border-color: var(--steel-brushed); }
.ps-icon-btn:focus-visible { outline: 2px solid var(--accent-laser); outline-offset: 2px; }
/* Multiplier souls landing (flyMultiplierSouls): light, never movement. */
.bonus-multi-badge.soul-hit {
  animation: soulGlow 0.6s cubic-bezier(0.2, 0.9, 0.2, 1);
}
@keyframes soulGlow {
  0%   { filter: none; }
  30%  { filter: brightness(1.7) drop-shadow(0 0 10px var(--soul-glow, rgba(255, 226, 138, 0.55))); }
  100% { filter: none; }
}
@media (prefers-reduced-motion: reduce) {
  .bonus-multi-badge.soul-hit { animation: none; }
}

/* ── Portrait: stack it ── */
@media (orientation: portrait) {
  /* Portrait boards are WIDTH-bound: at full width a 6/5 board is 5/6 of the
     viewport width tall, and no taller. Every row below therefore takes a
     share of the height the board cannot use (--slack), floored so tap targets
     survive on small phones. Fixed vh tokens here shrank a 768x1024 tablet
     board from 712px wide to 467px. */
  body.public-mode .altar {
    --slack: calc(100dvh - 100vw * 5 / 6 - var(--hud-h) - var(--rail) - 8 * var(--gap));
    --logo-h: clamp(28px, calc(var(--slack) * 0.16), 120px);
    --feat-h: clamp(36px, calc(var(--slack) * 0.16), 80px);
    --status-h: clamp(38px, calc(var(--slack) * 0.12), 52px); /* fits the 36px system buttons */
    --sub-h: clamp(28px, calc(var(--slack) * 0.09), 40px);
    --spin: clamp(64px, min(30vw, calc(var(--slack) * 0.44)), 148px);
    /* + 12px inside the spin cluster, + the 12px floor between deck rows. */
    --deck-h: calc(var(--spin) + var(--sub-h) + var(--status-h) + 2 * var(--gap) + 12px + max(0px, 12px - var(--gap)));
  }
  body.public-mode .vault-shell {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    grid-template-rows: var(--logo-h) minmax(0, 1fr) var(--feat-h) var(--deck-h);
    grid-template-areas:
      "logo   logo"
      "center center"
      "buy    ante"
      "deck   deck";
  }

}
/* Short portrait (320x568 class): the logo row is the first thing to give
   its height to the board. */
@media (orientation: portrait) and (max-height: 640px) {
  body.public-mode .altar { --logo-h: 0px; }

}

/* ── Short landscape (phones): every vertical pixel is board ── */
@media (orientation: landscape) and (max-height: 560px) {
  body.public-mode .altar {
    --deck-h: clamp(46px, 13vh, 60px);
    --feat-h: clamp(36px, 13vh, 56px);
  }
  

}

body.admin-mode .ps-center { display: grid; gap: 10px; }

/* ── Info / settings modal ── */
.ps-modal {
  position: fixed;
  inset: 0;
  z-index: 9100; /* above the game and buy-confirm (9000), below the big-win overlay */
  contain: strict;
  display: grid;
  place-items: center;
  padding: max(12px, env(safe-area-inset-top)) max(12px, env(safe-area-inset-right))
           max(12px, env(safe-area-inset-bottom)) max(12px, env(safe-area-inset-left));
  background: color-mix(in srgb, var(--bg-vault-door) 82%, transparent);
}
.ps-modal[hidden] { display: none; }
.ps-modal__panel {
  width: min(960px, 100%);
  height: min(680px, 100%);
  display: grid;
  grid-template-columns: clamp(180px, 24%, 240px) minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "head head"
    "nav  body"
    "dots dots";
  border-radius: 16px;
  border: 1px solid var(--steel-edge);
  background: linear-gradient(180deg, var(--bg-panel), var(--bg-gunmetal));
  box-shadow: var(--shadow-hard);
  overflow: hidden;
}
.ps-modal__head {
  grid-area: head;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--steel-edge);
}
.ps-modal__title {
  margin: 0;
  font: 700 1.05rem / 1 var(--font-display);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--win-gold);
}

.ps-modal__body {
  grid-area: body;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 18px 22px;
  color: var(--text-hud);
  font: 400 0.92rem / 1.6 var(--font-ui);
}
.ps-modal__section[hidden] { display: none; }
.ps-modal__section h3 {
  margin: 0 0 12px;
  font: 700 1rem / 1.2 var(--font-display);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--text-hud);
}

/* Generated Game Rules page (renderInfoRules in main.js). */
.ps-rules-meta {
  margin: 0 0 14px;
  font: 600 0.78rem / 1.2 var(--font-ui);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--win-gold);
}
.ps-modal__section h4 {
  margin: 18px 0 6px;
  font: 700 0.86rem / 1.2 var(--font-display);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-hud);
}
.ps-modal__section ul {
  margin: 0;
  padding-left: 1.2em;
  color: var(--text-muted);
}
.ps-modal__section li + li { margin-top: 4px; }
/* Player paytable (renderPlayerPaytable in main.js): scaled pays, i.e. what
   a win actually pays at the current bet. */
.ps-paytable__note {
  margin: 0 0 10px;
  color: var(--text-muted);
}
.ps-paytable__table {
  width: 100%;
  margin: 0 0 14px;
  border-collapse: collapse;
  font: 500 0.82rem / 1.3 var(--font-ui);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}
.ps-paytable__table caption {
  padding: 0 0 6px;
  text-align: left;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--win-gold);
}
.ps-paytable__table th,
.ps-paytable__table td {
  padding: 4px 6px;
  border-bottom: 1px solid var(--steel-edge);
  text-align: right;
}
.ps-paytable__table th:first-child,
.ps-paytable__table td:first-child {
  text-align: left;
  color: var(--text-hud);
}

/* Phones: sections become pages with a scrolling tab strip on top; the dots
   show where you are. */
@media (max-width: 720px) {
  .ps-modal { padding: 0; }
  .ps-modal__panel {
    width: 100%;
    height: 100%;
    border-radius: 0;
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto minmax(0, 1fr) auto;
    grid-template-areas: "head" "nav" "body" "dots";
  }

}

/* Round-ID badge — only shown when a server-authoritative round_ref is known
   (platform mode). Players hand this ID to support to look up the exact spin. */
.round-id-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 8px;
  padding: 6px 12px;
  border-radius: 10px;
  border: 1px solid rgba(170, 210, 255, 0.16);
  background: linear-gradient(180deg, rgba(20, 28, 50, 0.58), rgba(8, 12, 24, 0.72));
  font: 600 0.78rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  color: #cad7f0;
  flex-wrap: wrap;
}
.round-id-bar.hidden { display: none; }
.round-id-label {
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-size: 0.62rem;
  color: #8aa0c2;
}
.round-id-value {
  font-family: ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
  font-size: 0.85rem;
  color: #ffd86f;
  background: rgba(0, 0, 0, 0.3);
  padding: 4px 8px;
  border-radius: 6px;
  border: 1px solid rgba(255, 216, 111, 0.18);
  letter-spacing: 0.02em;
  user-select: all;
  word-break: break-all;
}
.round-id-copy {
  background: rgba(255, 216, 111, 0.14);
  border: 1px solid rgba(255, 216, 111, 0.34);
  color: #ffe9a3;
  padding: 4px 10px;
  border-radius: 6px;
  font: inherit;
  cursor: pointer;
}
.round-id-copy:hover {
  background: rgba(255, 216, 111, 0.22);
}
.round-id-copied {
  color: #82e6a4;
  font-size: 0.78rem;
}

/* Bonus-only nodes (bonus total / free spins / persistent multiplier) are
   visible in both player and admin views so the player can always see the
   running bonus state. The general hud-bar still shows everywhere. */

.oracle-panel,
.archive-panel {
  display: grid;
  gap: 10px;
}

.oracle-band,
.archive-band,
.codex-band {
  border-radius: 6px;
  padding: 12px;
}

.band-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 10px;
}

.band-head h2,
.oracle-band h2,
.archive-band h2,
.codex-band h2,
.codex-band h3 {
  margin: 0;
}

.summary-grid,
.sim-grid {
  display: grid;
  gap: 8px;
}

.summary-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.summary-grid > div,
.sim-grid > div {
  border: 1px solid rgba(245, 214, 169, 0.08);
  border-radius: 6px;
  padding: 8px;
  background: rgba(255, 247, 231, 0.02);
}

.summary-grid strong,
.sim-grid strong {
  display: block;
  margin-top: 4px;
  font-size: 0.96rem;
}

.spin-log-list,
#caughtLines,
#lineExamples {
  margin: 0;
  padding-left: 18px;
}

.test-lab-copy {
  margin: 0 0 10px;
  color: var(--ink-soft);
  font-size: 0.88rem;
}

.test-actions {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
  margin-bottom: 10px;
}

.test-actions button {
  min-height: 40px;
  padding: 8px 10px;
  font-size: 0.82rem;
}

.test-actions-crazy {
  grid-template-columns: 1fr;
}

.crazy-toggle {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 46px;
  padding: 8px 14px;
  border-radius: 12px;
  border: 1px solid rgba(255, 120, 80, 0.55);
  background: linear-gradient(180deg, rgba(60, 12, 16, 0.85), rgba(28, 4, 8, 0.95));
  color: #ffd9c8;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-size: 0.82rem;
  cursor: pointer;
  transition: transform 120ms ease, box-shadow 200ms ease, border-color 200ms ease, color 200ms ease;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 200, 160, 0.12);
}
.crazy-toggle:hover { transform: translateY(-1px); }
.crazy-toggle .crazy-toggle-state {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 38px;
  padding: 4px 8px;
  border-radius: 999px;
  background: rgba(255, 80, 60, 0.18);
  border: 1px solid rgba(255, 130, 100, 0.45);
  color: #ffb098;
  font-size: 0.72rem;
}
.crazy-toggle.is-on {
  color: #fff7e6;
  border-color: rgba(255, 200, 100, 0.85);
  background: linear-gradient(180deg, rgba(180, 30, 30, 0.9), rgba(80, 8, 8, 0.95));
  box-shadow: 0 6px 22px rgba(255, 80, 60, 0.45), inset 0 1px 0 rgba(255, 240, 200, 0.18);
  animation: crazyPulse 1.4s ease-in-out infinite;
}
.crazy-toggle.is-on .crazy-toggle-state {
  background: linear-gradient(180deg, #ffe089, #ff9a3c);
  border-color: rgba(255, 220, 120, 0.85);
  color: #4a1c00;
  text-shadow: 0 1px 0 rgba(255, 240, 180, 0.6);
}

@keyframes crazyPulse {
  0%, 100% { box-shadow: 0 6px 22px rgba(255, 80, 60, 0.45), inset 0 1px 0 rgba(255, 240, 200, 0.18); }
  50% { box-shadow: 0 10px 30px rgba(255, 140, 60, 0.65), inset 0 1px 0 rgba(255, 240, 200, 0.28); }
}

/* Crazy-mode accent: a thin OUTER glow on the vault frame only — never
   covers reels, buttons, or HUD. No inset fills, no full-viewport pseudo. */
body.crazy-mode-on .vault-window {
  box-shadow: 0 0 0 1px rgba(255, 120, 80, 0.55), 0 0 18px rgba(255, 90, 60, 0.25);
}

.win-chip {
  position: fixed;
  z-index: 9500;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(0.6);
  opacity: 0;
  padding: 6px 14px;
  border-radius: 999px;
  font-family: inherit;
  font-weight: 900;
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  color: #fff;
  text-shadow: 0 2px 6px rgba(0, 0, 0, 0.55);
  background: linear-gradient(180deg, var(--steel-sheen), var(--steel-brushed));
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.25) inset;
  will-change: transform, opacity;
}
.win-chip[data-tier="great"] {
  background: linear-gradient(180deg, var(--accent-laser), var(--accent-laser-deep));
  box-shadow: 0 10px 28px color-mix(in srgb, var(--win-gold) 55%, transparent), 0 0 0 1px rgba(255, 255, 255, 0.3) inset;
  font-size: 1.18rem;
}
.win-chip[data-tier="medium"] {
  background: linear-gradient(180deg, var(--win-gold), var(--win-gold-deep));
  box-shadow: 0 9px 26px color-mix(in srgb, var(--win-gold) 50%, transparent), 0 0 0 1px rgba(255, 255, 255, 0.28) inset;
}
.win-chip.is-rising {
  animation: winChipRise 220ms cubic-bezier(0.2, 0.9, 0.3, 1) forwards;
}
.win-chip.is-flying {
  animation: winChipFly 360ms cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes winChipRise {
  0% { transform: translate(-50%, -50%) scale(0.85); opacity: 0; }
  100% { transform: translate(-50%, -75%) scale(1.0); opacity: 1; }
}
@keyframes winChipFly {
  0% { transform: translate(-50%, -75%) scale(1.0); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--to-x)), calc(-75% + var(--to-y))) scale(0.55); opacity: 0; }
}

.test-multi-head {
  margin: 6px 0 8px;
  color: var(--ink-soft);
  text-transform: uppercase;
  font-size: 0.68rem;
  font-weight: 800;
  letter-spacing: 0.07em;
}

.test-multi-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 8px;
}

.test-multi-btn {
  min-height: 40px;
  padding: 6px 6px;
  border-radius: 10px;
  border: 1px solid var(--line-strong);
  color: #f3f8ff;
  font-size: 0.92rem;
  font-weight: 900;
  letter-spacing: 0.01em;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

.test-multi-btn.tier-common {
  background: linear-gradient(180deg, #63c7ff, #3572ff);
}

.test-multi-btn.tier-rare {
  background: linear-gradient(180deg, #a28bff, #6a55d8);
}

.test-multi-btn.tier-epic {
  background: linear-gradient(180deg, #ffb777, #f1832f);
}

.test-multi-btn.tier-legendary {
  background: linear-gradient(180deg, #ffd86f, #d29520);
}

.test-multi-btn.tier-mythic {
  background: linear-gradient(180deg, #ff92ce, #cf4fa1);
}

.test-multi-btn.is-armed {
  box-shadow:
    inset 0 0 0 2px rgba(238, 247, 255, 0.9),
    0 0 0 2px rgba(170, 215, 255, 0.55),
    0 0 26px rgba(170, 215, 255, 0.52);
  transform: translateY(-1px) scale(1.03);
}

.spin-log-list {
  max-height: 280px;
  overflow: auto;
}

.spin-log-list li,
#caughtLines li,
#lineExamples li {
  margin-bottom: 7px;
  color: #e9ddc4;
}

/* Production-style spin cards */
.spin-log-list li.spin-card {
  list-style: none;
  margin: 0 0 8px 0;
  padding: 9px 11px 10px 12px;
  border-radius: 10px;
  background: linear-gradient(180deg, rgba(20, 30, 54, 0.74), rgba(10, 17, 34, 0.82));
  border: 1px solid rgba(165, 203, 255, 0.14);
  border-left: 3px solid rgba(160, 190, 230, 0.35);
  display: flex;
  flex-direction: column;
  gap: 6px;
  font-family: "SF Pro Text", "Segoe UI", system-ui, sans-serif;
  color: #cfdbef;
  transition: border-color 0.18s ease, background 0.18s ease;
}

.spin-log-list li.spin-card.spin-tier-loss {
  opacity: 0.78;
  border-left-color: rgba(150, 165, 190, 0.3);
}

.spin-log-list li.spin-card.spin-tier-small {
  border-left-color: rgba(170, 220, 255, 0.55);
}

.spin-log-list li.spin-card.spin-tier-medium {
  border-left-color: rgba(255, 210, 130, 0.7);
  background: linear-gradient(180deg, rgba(36, 32, 28, 0.78), rgba(18, 14, 10, 0.86));
}

.spin-log-list li.spin-card.spin-tier-great {
  border-left-color: #ffd166;
  background: linear-gradient(180deg, rgba(58, 40, 18, 0.86), rgba(24, 14, 6, 0.92));
  box-shadow: inset 0 0 0 1px rgba(255, 209, 102, 0.18), 0 0 16px rgba(255, 209, 102, 0.08);
}

.spin-log-list li.spin-card.spin-bonus {
  border-left-color: #8de9a3;
}

.spin-card-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: nowrap;
}

.spin-seq {
  font: 700 0.78rem/1 "SF Mono", "Consolas", monospace;
  color: #8aa0c2;
  letter-spacing: 0.04em;
  min-width: 38px;
}

.spin-chip {
  font: 800 0.66rem/1 "SF Pro Text", "Segoe UI", sans-serif;
  letter-spacing: 0.08em;
  padding: 3px 7px;
  border-radius: 4px;
  text-transform: uppercase;
  color: #cfe1ff;
  background: rgba(140, 180, 230, 0.12);
  border: 1px solid rgba(165, 203, 255, 0.22);
}

.spin-chip.mode-base {
  color: #b9c8e3;
}

.spin-chip.mode-bonus {
  color: #d6ffd9;
  background: rgba(141, 233, 163, 0.16);
  border-color: rgba(141, 233, 163, 0.42);
}

.spin-chip.mode-tumble {
  color: #ffe2a6;
  background: rgba(255, 209, 102, 0.14);
  border-color: rgba(255, 209, 102, 0.36);
}

.spin-bet {
  font: 600 0.78rem/1 "SF Pro Text", "Segoe UI", sans-serif;
  color: #8aa0c2;
  margin-left: auto;
  white-space: nowrap;
}

.spin-win {
  font: 800 0.98rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  color: #f4f7ff;
  white-space: nowrap;
  padding-left: 8px;
}

.spin-tier-medium .spin-win {
  color: #ffd99d;
  text-shadow: 0 0 8px rgba(255, 217, 157, 0.35);
}

.spin-tier-great .spin-win {
  font-size: 1.08rem;
  color: #ffd166;
  text-shadow: 0 0 12px rgba(255, 209, 102, 0.55);
}

.spin-win-none {
  font-weight: 600;
  color: #6c7a93;
  text-shadow: none;
}

.spin-card-body {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

.spin-pills {
  display: flex;
  gap: 5px;
  flex-wrap: wrap;
}

.spin-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 7px 2px 5px;
  border-radius: 999px;
  background: rgba(15, 24, 42, 0.6);
  border: 1px solid rgba(165, 203, 255, 0.16);
  font: 700 0.72rem/1 "SF Mono", "Consolas", monospace;
  color: #c8d4eb;
}

.spin-pill-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  flex-shrink: 0;
}

.spin-pill-count {
  font-variant-numeric: tabular-nums;
}

.spin-pill-more {
  font-style: italic;
  color: #8aa0c2;
}

.spin-no-win {
  font: 600 0.76rem/1 "SF Pro Text", "Segoe UI", sans-serif;
  color: #6c7a93;
  letter-spacing: 0.04em;
}

.spin-math {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-variant-numeric: tabular-nums;
}

.spin-raw {
  font: 700 0.82rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  color: #cfdbef;
}

.spin-math-op {
  font: 600 0.78rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  color: #8aa0c2;
  letter-spacing: 0.04em;
}

.spin-mult {
  font: 800 0.78rem/1 "SF Pro Text", "Segoe UI", sans-serif;
  color: #ffe28a;
  background: rgba(255, 209, 102, 0.14);
  border: 1px solid rgba(255, 209, 102, 0.42);
  border-radius: 4px;
  padding: 3px 7px;
  letter-spacing: 0.04em;
  text-shadow: 0 0 8px rgba(255, 226, 138, 0.45);
}

.spin-log-list li.log-event {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 8px;
  background: rgba(14, 22, 40, 0.5);
  font: 600 0.78rem/1.2 "SF Pro Text", "Segoe UI", sans-serif;
}

.spin-log-list li.log-event .event-tag {
  font: 800 0.62rem/1 "SF Pro Text", "Segoe UI", sans-serif;
  letter-spacing: 0.1em;
  color: #8aa0c2;
  background: rgba(140, 180, 230, 0.12);
  padding: 3px 6px;
  border-radius: 3px;
}

.vault-shell {
  display: grid;
  gap: 10px;
}

.spin-log-list li.log-event {
  border-left: 2px solid rgba(255, 220, 173, 0.45);
  padding-left: 8px;
}

.vault-window {
  position: relative;
  padding: 14px 14px 16px;
  border: 1px solid var(--line);
  border-radius: 6px;
  /* Mostly transparent so the temple backdrop on .altar shows through around the
     reel table; just a faint warm glow + edge darkening for depth. */
  background:
    radial-gradient(circle at 50% 18%, rgba(234, 125, 99, 0.10), transparent 30%),
    linear-gradient(180deg, rgba(18, 11, 10, 0.18), rgba(8, 6, 6, 0.34));
  overflow: hidden;
  min-height: clamp(520px, 70vh, 780px);
}

.reels-stage {
  position: relative;
  z-index: 1;
  width: min(100%, 980px);
  margin: 0 auto;
  aspect-ratio: 16 / 12;
  min-height: clamp(420px, 62vh, 760px);
  border-radius: 10px;
  overflow: hidden;
  border: 1px solid rgba(245, 214, 169, 0.12);
  background:
    radial-gradient(circle at 50% 5%, rgba(238, 183, 106, 0.11), transparent 35%),
    linear-gradient(180deg, rgba(24, 14, 14, 0.86), rgba(10, 8, 8, 0.92));
}

.reels-canvas {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: inherit;
}

.visually-hidden {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

#simulateBtn,
#simulateBonusBtn {
  font-size: 0.84rem;
}

.progress-track {
  height: 14px;
  border-radius: 6px;
  border: 1px solid rgba(245, 214, 169, 0.12);
  background: #0d0908;
  overflow: hidden;
  margin-bottom: 10px;
}

.progress-bar {
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, var(--jade), var(--sun), var(--coral));
  transition: width 0.2s linear;
}

.sim-grid {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.codex-strip {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(420px, 1.2fr) minmax(320px, 0.9fr);
  gap: 12px;
}

.codex-band p {
  margin: 0 0 10px;
  color: #e1d3bb;
}

.multi-tier-line {
  display: inline-block;
  margin-bottom: 2px;
}

.multi-tier-name {
  font-weight: 900;
  letter-spacing: 0.02em;
}

.tier-common {
  color: #8fd8ff;
}

.tier-rare {
  color: #b8a1ff;
}

.tier-epic {
  color: #ffb067;
}

.tier-legendary {
  color: #ffd95f;
}

.tier-mythic {
  color: #ff9bd1;
}

.table-wrap {
  overflow-x: auto;
  margin-top: 8px;
}

.paytable {
  width: 100%;
  border-collapse: collapse;
}

.paytable th,
.paytable td {
  padding: 7px 8px;
  text-align: left;
  border: 1px solid rgba(245, 214, 169, 0.09);
}

.paytable th {
  color: #ffd89d;
}

pre {
  margin: 0 0 12px;
  padding: 10px;
  border-radius: 6px;
  border: 1px solid rgba(245, 214, 169, 0.08);
  background: rgba(0, 0, 0, 0.18);
  max-height: 240px;
  overflow: auto;
  white-space: pre-wrap;
  word-break: break-word;
  color: #e9ddc6;
}

#sessionNet.positive,
#simLiveCasinoNet.positive {
  color: var(--jade);
}

#sessionNet.negative,
#simLiveCasinoNet.negative {
  color: var(--coral);
}

@media (max-width: 1340px) {
  .altar-core {
    grid-template-columns: 280px minmax(0, 1fr);
  }

  .archive-panel {
    grid-column: 1 / -1;
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

  .codex-strip {
    grid-template-columns: 1fr;
  }
}

@media (max-width: 980px) {
  .altar-core {
    grid-template-columns: 1fr;
    display: grid;
  }

  .oracle-panel,
  .archive-panel {
    grid-template-columns: 1fr;
  }

  .test-actions {
    grid-template-columns: 1fr;
  }

  .test-multi-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }

}

@media (max-width: 720px) {
  .sanctum {
    padding: 12px;
  }

  .altar {
    padding: 14px;
  }

  .summary-grid,
  .sim-grid,
  .test-multi-grid {
    grid-template-columns: 1fr;
  }


}

/* Reduced motion: players who ask the OS to minimise motion get a calm board —
   no shake, no aura pulses, no spinning glints. Canvas particle FX are thinned
   separately in main.js (fxScale + reduced-motion gates). */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

body {
  font-family: var(--font-ui);
  /* Clean, static temple backdrop: the image is shown FULLY and clearly — no dark
     scrim, no colour wash, no animated overlays. One fixed, viewport-covering image
     fills the whole game; the reel table and controls float on top. */
  background: url("assets/symbols/Background.webp") center / cover no-repeat fixed;
  background-color: var(--bg-vault-door);
}
/* Phones: the 640 px backdrop (same query as PHONE_ART in main.js and the preload). */
@media (pointer: coarse) and (max-width: 600px) {
  body {
    background-image: url("assets/symbols/sm/Background.webp");
  }
  .altar {
    background-image:
      linear-gradient(180deg, rgba(20, 13, 11, 0.66), rgba(10, 7, 6, 0.82)),
      url("assets/symbols/sm/Background.webp");
  }
}

/* The game UI stays unpainted until the splash has painted (fast first paint), then
   renders at full opacity UNDER the opaque splash (z-index 9999, `ui-prerender`, set
   by index.html on first contentful paint): the browser rasterizes it while the last
   assets load, and the reveal is a cheap fade of the splash alone. It used to fade
   in with the splash: the whole screen's first raster plus a full-screen blend per
   frame landed exactly on the player's first tap (bench, Slow 4G + 4x CPU, software
   GPU: that tap waited 4-7 s). Nothing shows until the splash fades. */
body:not(.app-ready):not(.ui-prerender) .sanctum { visibility: hidden; }

/* ── Graceful degradation on weak devices ──────────────────────────────────
   ReelCanvasRenderer publishes its quality tier onto <body> (perf-mid/perf-low)
   plus a separate perf-handheld flag for any touch device; these rules cash that
   in for the cheapest CSS wins.

   `background-attachment: fixed` forces the backdrop to be re-rasterized against
   the viewport and is a well-known mobile repaint cost, so anything that is not
   a desktop scrolls it with the page instead. */
body.perf-mid,
body.perf-low,
body.perf-handheld {
  background-attachment: scroll;
}
body.perf-low .stage-character { display: none; }

/* backdrop-filter is a live resample of everything behind the element. `.hud-bar`
   carries one permanently and sits directly over the reels, so on a phone it is
   re-blurring a moving canvas every single frame. Dropped on ALL handhelds, not
   just devices that have already been demoted to perf-low. */
/* The shared controls are exempt: their glass only blurs the static art behind the deck,
   rails and sheets (never the board), and the chrome must look the same in every game. */
body.perf-low *:not(.lsh):not(:where(.lsh *)):not(.lsh-sheet):not(:where(.lsh-sheet *)),
body.perf-low *:not(.lsh):not(:where(.lsh *)):not(.lsh-sheet):not(:where(.lsh-sheet *))::before,
body.perf-low *:not(.lsh):not(:where(.lsh *)):not(.lsh-sheet):not(:where(.lsh-sheet *))::after,
body.perf-handheld *:not(.lsh):not(:where(.lsh *)):not(.lsh-sheet):not(:where(.lsh-sheet *)),
body.perf-handheld *:not(.lsh):not(:where(.lsh *)):not(.lsh-sheet):not(:where(.lsh-sheet *))::before,
body.perf-handheld *:not(.lsh):not(:where(.lsh *)):not(.lsh-sheet):not(:where(.lsh-sheet *))::after {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

/* Overlay layers removed — nothing washes/tints the background anymore. */
body::before,
body::after {
  display: none;
}

.altar {
  border-radius: 20px;
  border-color: rgba(171, 207, 255, 0.26);
  /* Transparent so the clean <body> temple image shows through fully. No own image
     copy, no dark scrim, and NO backdrop-filter blur (that frosted/blurred the
     background — the "blended, not fully shown" look). */
  background: transparent;
}

.oracle-band,
.archive-band,
.codex-band {
  border-radius: 14px;
  border-color: rgba(165, 203, 255, 0.24);
  background: linear-gradient(180deg, rgba(22, 33, 58, 0.5), rgba(10, 17, 34, 0.54));
  backdrop-filter: blur(16px) saturate(145%);
  -webkit-backdrop-filter: blur(16px) saturate(145%);
}

.summary-grid strong,
.sim-grid strong {
  color: #e9f2ff;
}

.vault-window {
  border-radius: 18px;
  border-color: rgba(168, 205, 255, 0.26);
  background:
    radial-gradient(circle at 50% 12%, rgba(128, 196, 255, 0.22), transparent 28%),
    linear-gradient(180deg, rgba(10, 18, 36, 0.88), rgba(5, 11, 24, 0.92));
}

.reels-stage {
  border-radius: 16px;
  border-color: rgba(166, 206, 255, 0.24);
  box-shadow:
    inset 0 1px 0 rgba(228, 241, 255, 0.2),
    inset 0 -14px 24px rgba(0, 0, 0, 0.28),
    0 16px 38px rgba(3, 8, 20, 0.5);
  background:
    radial-gradient(circle at 50% 5%, rgba(167, 213, 255, 0.16), transparent 40%),
    linear-gradient(180deg, rgba(16, 26, 48, 0.82), rgba(8, 14, 30, 0.9));
}

button:not(:where(.lsh *, .lsh-sheet *, .lsh-panel *)) {
  border-radius: 12px;
  color: #f4f8ff;
  border-color: rgba(165, 205, 255, 0.46);
  text-shadow: 0 1px 2px rgba(3, 10, 22, 0.55);
  box-shadow:
    inset 0 1px 0 rgba(236, 247, 255, 0.22),
    0 10px 24px rgba(3, 9, 20, 0.45);
}

#simulateBtn,
#simulateBonusBtn {
  background: linear-gradient(180deg, #9fbfff, #546ed5);
}

.summary-grid > div,
.sim-grid > div {
  border-color: rgba(165, 206, 255, 0.2);
  background: rgba(160, 203, 255, 0.05);
  border-radius: 10px;
}

.progress-track {
  border-radius: 999px;
  border-color: rgba(158, 204, 255, 0.26);
  background: rgba(8, 14, 29, 0.78);
}

.progress-bar {
  background: linear-gradient(90deg, #63d7ff, #6f89ff, #88f0d0);
}

/* ---------------------------------------------------------------------------
   Admin dashboard polish — clearer panel hierarchy + readable dense grids.
   Scoped to admin so the player view is untouched.
--------------------------------------------------------------------------- */
body.admin-mode .band-head {
  margin-bottom: 12px;
  padding-bottom: 9px;
  border-bottom: 1px solid rgba(165, 203, 255, 0.14);
}
body.admin-mode .band-head h2,
body.admin-mode .band-head h3 {
  display: inline-flex;
  align-items: center;
  font-size: 0.82rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #d2e3ff;
}
body.admin-mode .band-head h2::before {
  content: "";
  width: 3px;
  height: 0.92em;
  margin-right: 9px;
  border-radius: 2px;
  background: linear-gradient(180deg, #63d7ff, #6f89ff);
  box-shadow: 0 0 8px rgba(99, 215, 255, 0.5);
}
body.admin-mode .oracle-band,
body.admin-mode .archive-band,
body.admin-mode .codex-band {
  padding: 14px 15px;
}
body.admin-mode .summary-grid > div,
body.admin-mode .sim-grid > div {
  background: rgba(160, 203, 255, 0.07);
  border-color: rgba(165, 206, 255, 0.16);
}
body.admin-mode .summary-grid strong,
body.admin-mode .sim-grid strong {
  font-size: 1.02rem;
}

/* Win-combine finale: fly raw win + multiplier together → BANG → total.
   Played on every multiplied win. The chips launch from where their values live
   (--from-x/--from-y: the WIN line, the Total Multiplier badge), and every
   duration scales with turbo (--combine-t, set on the overlay). */
.win-combine-overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 7;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: opacity calc(0.32s * var(--combine-t, 1)) ease;
}

.win-combine-overlay.is-fading {
  opacity: 0;
}

.combine-chip {
  position: absolute;
  top: 50%;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  padding: 16px 28px;
  border-radius: 14px;
  background: linear-gradient(180deg, rgba(20, 28, 50, 0.94), rgba(8, 12, 24, 0.98));
  border: 1px solid rgba(170, 210, 255, 0.32);
  box-shadow:
    0 18px 50px rgba(0, 0, 0, 0.6),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(14px) saturate(140%);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  opacity: 0;
  transform: translate(calc(-50% + var(--from-x, 0px)), calc(-50% + var(--from-y, 0px))) scale(0.6);
  transition:
    transform calc(0.48s * var(--combine-t, 1)) cubic-bezier(0.18, 0.84, 0.22, 1),
    opacity calc(0.32s * var(--combine-t, 1)) ease,
    filter calc(0.32s * var(--combine-t, 1)) ease;
  white-space: nowrap;
}

.combine-kicker {
  font: 800 0.66rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: #9ec0ff;
  text-shadow: 0 0 10px rgba(158, 192, 255, 0.4);
}

.combine-value {
  font: 900 2rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  color: transparent;
  background: linear-gradient(180deg, #ffffff 0%, #ffe9b6 55%, #f1b76a 100%);
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.4)) drop-shadow(0 0 18px rgba(255, 200, 120, 0.45));
}

/* Amount chip — rises from the WIN line (else slides in from the left). */
.combine-amount {
  left: 25%;
  border-color: rgba(255, 209, 102, 0.5);
}
.combine-amount .combine-kicker { color: #ffd99d; }

/* Multiplier chip — flies down from the Total Multiplier badge (else slides in
   from the right), tier-colored. */
.combine-multi {
  left: 75%;
  border-color: var(--combine-color, rgba(255, 132, 184, 0.6));
  box-shadow:
    0 18px 50px rgba(0, 0, 0, 0.6),
    0 0 36px var(--combine-glow, rgba(255, 132, 184, 0.4)),
    inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.combine-multi .combine-kicker { color: var(--combine-color, #ff9ec8); letter-spacing: 0.36em; }
.combine-multi .combine-value {
  background: linear-gradient(180deg, #ffffff 0%, #ffd9ec 35%, var(--combine-color, #ff7faf) 100%);
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.45)) drop-shadow(0 0 22px var(--combine-glow, rgba(255, 132, 184, 0.6)));
}

.combine-amount.is-entered,
.combine-multi.is-entered {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.combine-amount.is-slamming {
  transition: transform calc(0.36s * var(--combine-t, 1)) cubic-bezier(0.5, 0, 0.85, 1);
  transform: translate(calc(-50% + (50% - 25%) * 1px), -50%) scale(1.08);
  /* Move toward 50% — using left: 25% means we need to translate +25% of viewport. */
  left: 50%;
}
.combine-multi.is-slamming {
  transition: transform calc(0.36s * var(--combine-t, 1)) cubic-bezier(0.5, 0, 0.85, 1), left calc(0.36s * var(--combine-t, 1)) cubic-bezier(0.5, 0, 0.85, 1);
  left: 50%;
  transform: translate(-50%, -50%) scale(1.08);
}
.combine-amount.is-slamming {
  transition: left calc(0.36s * var(--combine-t, 1)) cubic-bezier(0.5, 0, 0.85, 1), transform calc(0.36s * var(--combine-t, 1)) cubic-bezier(0.5, 0, 0.85, 1);
}

.combine-amount.is-merged,
.combine-multi.is-merged {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.4);
  filter: blur(6px) brightness(1.6);
  transition: opacity calc(0.18s * var(--combine-t, 1)) ease, transform calc(0.22s * var(--combine-t, 1)) ease, filter calc(0.18s * var(--combine-t, 1)) ease;
}

/* Collision flash — full-frame white-hot punch. */
.combine-flash {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 50%,
    rgba(255, 255, 255, 0.95) 0%,
    rgba(255, 220, 150, 0.7) 18%,
    rgba(255, 132, 184, 0.4) 40%,
    rgba(0, 0, 0, 0) 70%);
  opacity: 0;
  transform: scale(0.4);
  pointer-events: none;
}
.combine-flash.is-live {
  animation: combineFlash calc(0.6s * var(--combine-t, 1)) cubic-bezier(0.16, 0.84, 0.22, 1) forwards;
}
@keyframes combineFlash {
  0%   { opacity: 0; transform: scale(0.4); }
  18%  { opacity: 1; transform: scale(1.4); }
  100% { opacity: 0; transform: scale(2.6); }
}

/* Total chip — appears after the merge with shine sweep + overshoot pop. */
.combine-total {
  left: 50%;
  padding: 26px 44px 28px;
  border-radius: 18px;
  border-color: rgba(255, 209, 102, 0.7);
  background: linear-gradient(180deg, rgba(58, 40, 18, 0.94), rgba(20, 12, 6, 0.98));
  box-shadow:
    0 28px 90px rgba(255, 170, 60, 0.36),
    inset 0 1px 0 rgba(255, 220, 150, 0.22),
    inset 0 0 0 1px rgba(255, 209, 102, 0.22);
  overflow: hidden;
  isolation: isolate;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.4);
}
.combine-total .combine-kicker {
  color: #ffd166;
  letter-spacing: 0.42em;
  text-shadow: 0 0 16px rgba(255, 209, 102, 0.65);
}
.combine-total .combine-value {
  font-size: 3.2rem;
  background: linear-gradient(180deg, #ffffff 0%, #ffe9b6 35%, #ffae3c 100%);
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(0 3px 0 rgba(0, 0, 0, 0.5)) drop-shadow(0 0 32px rgba(255, 180, 80, 0.8));
}
.combine-total.is-live {
  animation: combineTotalIn calc(1.5s * var(--combine-t, 1)) cubic-bezier(0.16, 1.1, 0.3, 1) forwards;
}
.combine-total.is-live::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(110deg, transparent 32%, rgba(255, 255, 255, 0.32) 50%, transparent 68%);
  transform: translateX(-100%);
  animation: combineTotalShine calc(1.1s * var(--combine-t, 1)) cubic-bezier(0.2, 0.7, 0.3, 1) calc(0.4s * var(--combine-t, 1)) forwards;
  pointer-events: none;
}
@keyframes combineTotalIn {
  0%   { opacity: 0; transform: translate(-50%, -50%) scale(0.4); filter: blur(8px); }
  20%  { opacity: 1; transform: translate(-50%, -50%) scale(1.18); filter: blur(0); }
  44%  { transform: translate(-50%, -50%) scale(0.96); }
  64%  { transform: translate(-50%, -50%) scale(1.04); }
  100% { opacity: 1; transform: translate(-50%, -50%) scale(1); filter: blur(0); }
}
@keyframes combineTotalShine {
  to { transform: translateX(100%); }
}

.soul-layer {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 9999;
  overflow: visible;
}

.soul-orb {
  position: fixed;
  left: 0;
  top: 0;
  width: 44px;
  height: 44px;
  margin-left: -22px;
  margin-top: -22px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  font: 900 0.78rem/1 "Trebuchet MS", "Segoe UI", sans-serif;
  color: #fff;
  text-shadow: 0 0 8px rgba(0, 0, 0, 0.7), 0 1px 1px rgba(0, 0, 0, 0.85);
  background: radial-gradient(circle at 35% 30%, rgba(255, 255, 255, 0.95), var(--soul-color, #9dcfff) 38%, rgba(0, 0, 0, 0) 80%);
  box-shadow:
    0 0 14px 4px var(--soul-glow, rgba(157, 207, 255, 0.55)),
    0 0 38px 12px var(--soul-glow, rgba(157, 207, 255, 0.35)),
    inset 0 0 12px rgba(255, 255, 255, 0.55);
  will-change: transform, opacity;
  transform: translate3d(0, 0, 0) scale(1);
}

.soul-orb::after {
  content: "";
  position: absolute;
  inset: -16px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--soul-glow, rgba(157, 207, 255, 0.4)), rgba(0, 0, 0, 0) 70%);
  animation: soulHalo 0.9s ease-in-out infinite;
}

/* Pooled multiplier trails (SoulTrails): one viewport canvas, drawn additively. */
.soul-trails {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

@keyframes soulHalo {
  0%, 100% { transform: scale(1); opacity: 0.7; }
  50% { transform: scale(1.18); opacity: 1; }
}

.soul-plus {
  position: fixed;
  left: 0;
  top: 0;
  font: 900 1.1rem/1 "Trebuchet MS", "Segoe UI", sans-serif;
  color: var(--soul-color, #ffe28a);
  text-shadow: 0 0 10px var(--soul-glow, rgba(255, 226, 138, 0.85)), 0 1px 2px rgba(0, 0, 0, 0.85);
  pointer-events: none;
  animation: soulPlusRise 0.9s cubic-bezier(0.2, 0.7, 0.2, 1) forwards;
  z-index: 9999;
}

/* The landed value pops at the HUD (scale + glow flare), then rises away. It
   lives in the soul layer, so the HUD itself only glows (soulGlow), never moves. */
@keyframes soulPlusRise {
  0%   { opacity: 0; transform: translate(-50%, 0) scale(0.6); filter: brightness(1); }
  16%  { opacity: 1; transform: translate(-50%, -8px) scale(1.45); filter: brightness(1.8) drop-shadow(0 0 12px var(--soul-glow, rgba(255, 226, 138, 0.85))); }
  34%  { transform: translate(-50%, -14px) scale(1.05); filter: brightness(1.2); }
  100% { opacity: 0; transform: translate(-50%, -42px) scale(1); filter: brightness(1); }
}
@keyframes soulPlusFade {
  0%, 100% { opacity: 0; transform: translate(-50%, -8px); }
  20%, 60% { opacity: 1; transform: translate(-50%, -8px); }
}
@media (prefers-reduced-motion: reduce) {
  .soul-plus { animation-name: soulPlusFade; }
}

pre,
.table-wrap,
.paytable th,
.paytable td {
  border-color: rgba(165, 206, 255, 0.18);
}

/* Buy Feature confirmation modal */
.confirm-modal {
  position: fixed;
  inset: 0;
  z-index: 9000;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(4, 8, 18, 0.72);
  backdrop-filter: blur(6px) saturate(120%);
  -webkit-backdrop-filter: blur(6px) saturate(120%);
  animation: confirmFade 0.18s ease;
}

.confirm-modal.hidden { display: none; }

@keyframes confirmFade {
  from { opacity: 0; }
  to { opacity: 1; }
}

.confirm-dialog {
  width: min(420px, calc(100% - 32px));
  padding: 28px 32px 24px;
  border-radius: 16px;
  border: 1px solid rgba(255, 209, 102, 0.42);
  background: linear-gradient(180deg, rgba(38, 28, 14, 0.96), rgba(14, 10, 6, 0.98));
  box-shadow:
    0 28px 80px rgba(0, 0, 0, 0.6),
    inset 0 1px 0 rgba(255, 220, 150, 0.18),
    0 0 36px rgba(255, 170, 60, 0.12);
  text-align: center;
  animation: confirmPop 0.32s cubic-bezier(0.16, 1.05, 0.3, 1);
}

@keyframes confirmPop {
  0% { opacity: 0; transform: scale(0.85) translateY(8px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

.confirm-title {
  margin: 0 0 6px;
  font: 900 1.5rem/1.1 "SF Pro Display", "Segoe UI", sans-serif;
  letter-spacing: 0.04em;
  color: #ffd166;
  text-shadow: 0 0 14px rgba(255, 209, 102, 0.45);
}

.confirm-copy {
  margin: 0 0 16px;
  font: 600 0.9rem/1.3 "SF Pro Text", "Segoe UI", sans-serif;
  color: #cfdbef;
}

.confirm-cost {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 8px;
  margin: 4px 0 8px;
}

.confirm-cost-amount {
  font: 900 2.4rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(180deg, #ffffff 0%, #ffe9b6 55%, #f1b76a 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 2px 0 rgba(0, 0, 0, 0.4)) drop-shadow(0 0 18px rgba(255, 200, 120, 0.5));
}

.confirm-cost-currency {
  font: 800 1rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  letter-spacing: 0.16em;
  color: #ffd99d;
}

.confirm-meta {
  margin: 4px 0 22px;
  font: 600 0.78rem/1 "SF Pro Text", "Segoe UI", sans-serif;
  color: #8aa0c2;
  letter-spacing: 0.02em;
}

.confirm-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.confirm-btn {
  border: 1px solid;
  border-radius: 10px;
  padding: 12px 16px;
  font: 800 0.88rem/1 "SF Pro Display", "Segoe UI", sans-serif;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  transition: transform 0.12s ease, filter 0.12s ease, background 0.12s ease;
  min-height: 0 !important;
}

.confirm-btn-cancel {
  background: linear-gradient(180deg, rgba(40, 50, 80, 0.85), rgba(20, 26, 46, 0.95));
  border-color: rgba(170, 210, 255, 0.32);
  color: #cfdbef;
}
.confirm-btn-cancel:hover { filter: brightness(1.15); }

.confirm-btn-confirm {
  background: linear-gradient(180deg, #ffc466, #d48739);
  border-color: rgba(255, 209, 102, 0.7);
  color: #2a1808;
  text-shadow: 0 1px 0 rgba(255, 220, 150, 0.5);
}
.confirm-btn-confirm:hover { filter: brightness(1.08); transform: translateY(-1px); }
.confirm-btn:active { transform: scale(0.97); }

/* ===========================================================================
   AMBIANCE STACKING CONTEXT

   The ~300 lines of ambiance styling that used to live here (moving nebula
   backdrop, drifting deco gems, deity mascot, lightning, god-rays, coin
   shower) were removed. Ambiance.init() in client/engine/ambiance.js returns
   unconditionally, so none of those elements are ever built — but the rules
   were still parsed on every load, and several of the infinite keyframes
   animated margin-left/top, which force layout rather than compositing.

   These two rules are kept because they apply to REAL elements and define the
   stacking order of the game itself, not the ambiance layer.
   =========================================================================== */
.sanctum { position: relative; z-index: 1; }
.vault-window { position: relative; z-index: 2; }


/* ===========================================================================
   OPERATOR FREE ROUNDS (Campaign Manager)
   Award modal on the Step 6 shell (.ps-modal: fixed, contained, outside the
   flow) with the buy-confirm dialog's vault-gold look. While free rounds run the
   deck's Bet readout becomes the counter; bet/ante/buy are disabled by
   setControls(). Colour only: nothing here changes any element's size.
   =========================================================================== */
.fr-dialog { position: relative; }
.fr-kicker {
  margin: 0 0 10px;
  font: 800 0.72rem/1 var(--font-ui, "Segoe UI", sans-serif);
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: var(--accent-laser-hot, #ffd166);
}
.fr-count { font-variant-numeric: tabular-nums; }
.fr-play { width: 100%; }
.fr-play:focus-visible { outline: 2px solid var(--win-gold-bright, #ffe08a); outline-offset: 3px; }
body.free-rounds-on #betReadoutLabel { color: var(--accent-laser-hot, #ffd166); }
body.free-rounds-on #footerBet { color: var(--win-gold-bright, #ffe08a); }

/* ── FRAMELESS GLASS (v10) ─────────────────────────────────────────────────────
   No painted reel frame: the canvas is transparent (buildStageCache bakes only the
   cell-corner marks) and the board's surface is this glass panel. The HUD is a set
   of floating dark-glass pills instead of one full-width bar.

   Zero-reflow contract (docs/styling-guide.md): nothing here sets a size, a track
   or a margin on the board or the deck. Edges are drawn with existing 1px borders
   (colour only) or box-shadows, never a new border width, so every box keeps its
   exact geometry; layout-check.js holds that at 9 viewports.

   Blur: backdrop-filter re-samples what is behind the element. The perf-handheld /
   perf-low rule above already strips every backdrop-filter on phones, so there the
   glass falls back to a more opaque fill (--glass-bg below), same for browsers
   without backdrop-filter. */
body.public-mode { --glass-bg: var(--glass-fill); }
body.public-mode.perf-handheld,
body.public-mode.perf-low { --glass-bg: var(--glass-fill-solid); }
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  body.public-mode { --glass-bg: var(--glass-fill-solid); }
}

/* The board: a dark glass panel, symbols floating on it. */
body.public-mode .vault-window {
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
body.public-mode .reels-stage {
  border-radius: 16px;
  border-color: var(--glass-edge);
  background:
    linear-gradient(180deg, var(--glass-sheen), transparent 22%),
    var(--glass-bg);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--glass-shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}

body.public-mode .bonus-multi-badge {
  background:
    linear-gradient(180deg, var(--glass-sheen), transparent 55%),
    var(--glass-bg);
  backdrop-filter: blur(16px) saturate(125%);
  -webkit-backdrop-filter: blur(16px) saturate(125%);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.65), inset 0 0 0 1px var(--glass-edge);
}

/* System buttons inside a pill: borderless glass discs, not steel plates. */
body.public-mode .ps-icon-btn {
  border-color: transparent;
  background: rgba(255, 255, 255, 0.06);
  box-shadow: none;
}
body.public-mode .ps-icon-btn:hover {
  background: rgba(255, 255, 255, 0.12);
  border-color: transparent;
}

/* Bonus "Total Multiplier": a floating glass pill; the tier still colours its
   edge glow (--badge-glow) and the value. */
body.public-mode .bonus-multi-badge {
  border-radius: 999px;
  border-color: var(--glass-edge);
  box-shadow: 0 12px 32px -12px rgba(0, 0, 0, 0.65), 0 0 18px var(--badge-glow), inset 0 0 0 1px var(--glass-edge);
}
body.public-mode .bonus-multi-badge-label,
body.public-mode .bonus-multi-badge-value {
  font-family: var(--font-display);
}

/* ── Reality check (jurisdiction profile) ── a fixed overlay (never in the flow) on the
   .ps-modal scrim; a compact glass card with the session's figures. */
.vx-reality { z-index: 9200; } /* above the info modal: it interrupts play */
.vx-reality__panel {
  width: min(420px, 100%);
  display: grid;
  gap: 14px;
  padding: clamp(18px, 3vh, 28px);
  border-radius: 18px;
  border: 1px solid var(--glass-edge);
  background: linear-gradient(180deg, var(--glass-sheen), transparent 40%), var(--glass-fill-solid);
  box-shadow: var(--glass-shadow);
  color: var(--text-hud);
  text-align: center;
}
.vx-reality__title {
  margin: 0;
  font: 700 clamp(1.1rem, 3vh, 1.5rem)/1.1 var(--font-display);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--win-gold-bright);
}
.vx-reality__body { margin: 0; color: var(--text-muted); }
.vx-reality__body strong { color: var(--text-hud); }
.vx-reality__stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
  margin: 0;
}
.vx-reality__stats div {
  padding: 8px 10px;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.05);
}
.vx-reality__stats dt { font-size: 0.7rem; letter-spacing: 0.14em; text-transform: uppercase; color: var(--text-muted); }
.vx-reality__stats dd { margin: 2px 0 0; font-weight: 700; font-variant-numeric: tabular-nums; }
.vx-reality__actions { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.vx-reality__actions button { min-height: 44px; border-radius: 999px; font-weight: 700; cursor: pointer; }
.vx-reality__leave { border: 1px solid var(--glass-edge); background: transparent; color: var(--text-hud); }
.vx-reality__continue { border: 1px solid var(--win-gold-deep); background: linear-gradient(180deg, var(--win-gold-bright), var(--win-gold)); color: #1a1a1a; }
.vx-reality__actions button:focus-visible { outline: 2px solid var(--accent-laser); outline-offset: 2px; }

/* Controls a jurisdiction profile switches off: hidden in place, so nothing moves. */
.is-reg-off { visibility: hidden !important; pointer-events: none !important; }

/* Low tier: controls change colour, border, glow and filter instantly. Those are
   paint properties: a transition on them repaints the control on every frame it
   runs (twice per round, at start and settle), on top of the reels animating.
   Transform stays animated (composited, cheap). Measured with tools/bench
   (client --trace): on the low tier, the CSS on screen during a spin cost about
   as much as the reels themselves. */
body.perf-low button,
body.perf-low .crazy-toggle {
  transition-property: transform !important;
}

/* ── Vault 20K on the Lumina Shell (client/vault-shell.js) ──
   The board column (.ps-center) lives in the shell's stage and fills it; the shell
   owns every control, which look the same in every Lumina game (layers.md, Layer 1). */
body.public-mode .vault-shell.is-mounted, body .vault-shell.is-mounted { display: none; } /* its board column moved into the shell */
.lsh[data-game="vault"] .lsh-stage > .ps-center { flex: 1 1 auto; min-width: 0; min-height: 0; }
.lsh[data-game="vault"] .vault-window { align-items: center; } /* the board sits in the middle of its stage */
/* The info sheet carries the rules template's sections, all open, one after another. */
.lsh-sheet .vx-info .ps-modal__section + .ps-modal__section { margin-top: 18px; }

.reels-stage::after { content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none; border-radius: inherit; opacity: 0; transition: opacity 0.6s ease;
  background: radial-gradient(ellipse at 50% 0%, rgba(255, 196, 96, 0.22), transparent 60%), radial-gradient(ellipse at 50% 100%, rgba(255, 170, 60, 0.12), transparent 55%);
  box-shadow: inset 0 0 0 2px rgba(255, 200, 110, 0.45), inset 0 0 36px rgba(255, 176, 64, 0.25); }
.reels-stage.is-bonus-lit::after { opacity: 1; }

/* Colchis Treasury (client/assets/ART.md): the chamber behind everything, wide on landscape
   screens and tall on portrait ones (same queries as main.js uiAssets and the preloads);
   during free spins the same room with the Golden Fleece crossfades in on body::after.
   The board sits in a thin bead rail with a ram at each corner: one 9-slice image
   (Frame.webp), outset so it never moves or resizes the board. */
body {
  background: url("assets/symbols/Background.webp") center / cover no-repeat fixed;
  background-color: #0b0806;
}
body::after {
  display: block;
  z-index: -1;
  background: url("assets/symbols/Feature.webp") center / cover no-repeat;
  opacity: 0;
  transition: opacity var(--chamber-fade, 0ms) ease;
}
body.colchis-feature::after { opacity: 1; }
@media (orientation: portrait) {
  body { background-image: url("assets/symbols/Background-portrait.webp"); }
  body::after { background-image: url("assets/symbols/Feature-portrait.webp"); }
}
@media (pointer: coarse) and (max-width: 600px) {
  body { background-image: url("assets/symbols/sm/Background.webp"); }
  body::after { background-image: url("assets/symbols/sm/Feature.webp"); }
}
body .reels-stage,
body.public-mode .reels-stage {
  border-radius: 4px;
  border: 1px solid transparent;
  border-image: url("assets/symbols/Frame.webp") 160 round;
  border-image-width: 44px;
  border-image-outset: 22px;
  background: linear-gradient(180deg, rgba(20, 15, 11, 0.86), rgba(9, 7, 5, 0.92));
}
@media (max-width: 600px) {
  body .reels-stage,
  body.public-mode .reels-stage {
    border-image-width: 32px;
    border-image-outset: 16px;
  }
}

/* Big win (Vault): the title and amount are one SVG (index.html, .vx-bw-figures); CSS
   only sizes it and sets the face. The gold and the dark edge live in the SVG. */
.vx-bw-figures { display: block; width: min(94vw, 980px); height: auto; overflow: visible; }
.vx-bw-figures text { font-family: var(--font-display); font-weight: 700; text-transform: uppercase; font-variant-numeric: tabular-nums lining-nums; }
.vx-bw-title { transform-box: fill-box; transform-origin: center; }
