/* =============================================================================
   bust.fun — CRASH styles (one-shot + live Rocket stage)  (extracted from styles.css; games-foundation refactor)
   -----------------------------------------------------------------------------
   Game-SPECIFIC styles for this module. Linked from index.html. Synthwave tokens
   only (tokens.css); no hard-coded palette. Moved VERBATIM from styles.css so the
   rendered pixels are identical — per-game teams own this file going forward.
   ========================================================================== */

/* =============================================================================
   Live Crash — canvas stage
   ========================================================================== */
.crash-stage {
  position: relative; border-radius: var(--r-lg); overflow: hidden;
  background: radial-gradient(120% 100% at 50% 100%, #11202d, #080c14);
  border: 1px solid var(--line-2); aspect-ratio: 16/9; min-height: 240px; margin: var(--sp-2) 0;
  /* Pin width to the container. Without this, aspect-ratio 16/9 + min-height forces the box WIDER than
     its column on a narrow phone (min-height 200px -> ~355px wide), overflowing the 360px viewport. */
  width: 100%; max-width: 100%;
  transition: border-color var(--dur-2);
}
.crash-stage.betting { border-color: var(--info); }
.crash-stage.running { border-color: var(--brand); }
.crash-stage.crashed { border-color: var(--lose); }
/* rising TENSION: once the live multiplier is hot (>=5x) warm the stage border + a soft inner glow.
   Border/box-shadow only (no transform/opacity loop) so it's cheap + reduced-motion-irrelevant. */
.crash-stage.running.hot { border-color: var(--gold); box-shadow: inset 0 0 40px rgba(255,207,92,.10); }
.crash-stage.running.hot .crash-mult { color: var(--gold); text-shadow: 0 2px 30px rgba(0,0,0,.6), 0 0 18px var(--gold-glow); }
#crashCanvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.crash-overlay {
  position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
  pointer-events: none; text-align: center; gap: 4px;
}
/* Legibility scrim behind the big multiplier so it reads cleanly OVER the chart gridlines + y-axis tick
   labels (esp. on a narrow 360px phone where the centered number sits near the left-edge axis labels).
   A soft radial darkening centered on the number — purely cosmetic, pointer-events:none, no motion. */
.crash-overlay::before {
  content: ""; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: 78%; height: 56%;
  background: radial-gradient(ellipse at center, rgba(8,12,20,.62) 0%, rgba(8,12,20,.34) 45%, rgba(8,12,20,0) 75%);
  pointer-events: none; z-index: 0;
}
.crash-mult, .crash-state, .crash-countdown { position: relative; z-index: 1; } /* keep text above the scrim */
.crash-mult {
  font-family: var(--font-display);
  font-size: clamp(40px, 9vw, var(--fs-display)); font-weight: var(--fw-black);
  font-variant-numeric: tabular-nums; letter-spacing: -.02em; line-height: 1;
  text-shadow: 0 2px 30px rgba(0,0,0,.6);
}
.crash-stage.running .crash-mult { color: var(--brand); text-shadow: 0 2px 30px rgba(0,0,0,.6), var(--neon-glow-text); animation: crash-mult-breathe 1.8s var(--ease-in-out) infinite; }
@keyframes crash-mult-breathe { 0%,100% { transform: scale(1); } 50% { transform: scale(1.035); } }
@media (prefers-reduced-motion: reduce) { .crash-stage.running .crash-mult { animation: none; } }
.crash-stage.crashed .crash-mult { color: var(--lose); text-shadow: 0 2px 30px rgba(0,0,0,.6), 0 0 18px var(--lose-glow); animation: crash-shake .5s var(--ease-out); }
@keyframes crash-shake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-7px); } 40% { transform: translateX(6px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(2px); } }
.crash-state { font-size: var(--fs-sm); color: var(--fg-dim); text-transform: uppercase; letter-spacing: .12em; font-weight: var(--fw-semi); }
.crash-history { display: flex; gap: 6px; flex-wrap: wrap; margin: var(--sp-3) 0; }
.crash-chip {
  font-size: var(--fs-xs); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  padding: 4px 9px; border-radius: var(--r-pill); border: 1px solid var(--line-2); background: var(--surface-2);
  animation: chip-in var(--dur-2) var(--ease-spring) both;
}
.crash-chip.low { color: var(--lose); border-color: rgba(255,84,112,.35); }
.crash-chip.mid { color: var(--gold); border-color: rgba(255,207,92,.3); }
.crash-chip.high{ color: var(--win); border-color: rgba(31,212,126,.35); }
@keyframes chip-in { from { opacity: 0; transform: scale(.7); } to { opacity: 1; transform: none; } }

#crashVerify.ok { color: var(--win); }
#crashVerify.err { color: var(--lose); }

/* SINGLE-SOURCE COUNTDOWN (P13 — owner's "seconds shown twice"): the bare center number used to render the
   SAME betting/cooldown seconds already shown in the labeled .crash-state ("betting - N.Ns" / "next round in
   N.Ns"), so the value appeared twice. The labeled .crash-state is now the ONE source (crash-live.js no longer
   writes #crashCountdown), and this element is hidden so the bare number can never render — matching
   roulette/baccarat, which surface a single labeled countdown. Kept in the DOM (not removed) so no markup
   change is needed; display:none takes it fully out of flow + the a11y tree. */
.crash-countdown {
  display: none;
  position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%);
  font-size: var(--fs-md); font-weight: var(--fw-bold); font-variant-numeric: tabular-nums;
  color: var(--info); opacity: .85; pointer-events: none;
}

/* connection indicator (live/offline) */
.conn { display: inline-flex; align-items: center; gap: 7px; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .07em; font-weight: var(--fw-semi); color: var(--muted); }
.conn .conn-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); box-shadow: 0 0 0 3px rgba(124,135,154,.18); flex: none; }
.conn.connecting .conn-dot { background: var(--warn); box-shadow: 0 0 0 3px rgba(255,184,77,.2); animation: pulse-dot-warn 1.4s var(--ease-in-out) infinite; }
.conn.online .conn-dot { background: var(--win); box-shadow: 0 0 0 3px var(--win-glow); animation: pulse-dot-on 2.4s var(--ease-in-out) infinite; }
.conn.online { color: var(--win); }
.conn.offline .conn-dot { background: var(--lose); box-shadow: 0 0 0 3px var(--lose-glow); }
.conn.offline { color: var(--lose); }
@keyframes pulse-dot-on { 0%,100% { box-shadow: 0 0 0 3px var(--win-glow); } 50% { box-shadow: 0 0 0 6px rgba(31,212,126,.08); } }
@keyframes pulse-dot-warn { 0%,100% { opacity: 1; } 50% { opacity: .4; } }

/* "your position" banner */
/* CLS FIX (baccarat report #5 pattern): #crashPosition is the LAST child of the bet section, with the
   "Recent crashes" + "Live cashouts" + "Reconnect" sections below it in the rail. It used to toggle the
   GLOBAL `.hidden { display:none !important }` (no position) ↔ display:flex (a position), so every time a
   bet went in / settled the banner APPEARED and shoved those three sections DOWN ~46px (then the next
   round hid it and they jumped back UP). Now we PRE-ALLOCATE a fixed slot: the box is ALWAYS in flow with
   a reserved min-height (one line of text), and `.hidden` only makes its CONTENT invisible + inert (the
   override below) — the box (and the sections below it) never move across idle → bet → run → result → idle.
   min-height = 1 line (fs-md ≈ 1.4em) + 2×10px pad + 2×1px border, matching the shown one-line height. */
.crash-position {
  box-sizing: border-box; min-height: calc(1.4em + 20px + 2px);
  display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
  margin: var(--sp-3) 0; padding: 10px 14px; border-radius: var(--r-md);
  border: 1px solid var(--line-2); background: var(--surface-2); font-size: var(--fs-md);
  animation: result-in var(--dur-2) var(--ease-spring) both;
}
/* hidden = reserved-but-invisible: keep the box (and its reserved min-height) IN FLOW so the sections
   below never shift, but make it fully invisible + inert. The global `.hidden { display:none !important }`
   utility would otherwise pull the box out of flow — we override it with our own `!important` (same trick
   as .crash-result below) so the slot stays reserved; `visibility:hidden` hides the content/tint/border.
   visibility:hidden also removes it from the a11y tree, matching the old display:none aria semantics. */
.crash-position.hidden { display: flex !important; visibility: hidden; pointer-events: none; animation: none; }
.crash-position b { font-variant-numeric: tabular-nums; }
.crash-position .pos-tag { font-weight: var(--fw-black); }
.crash-position.pending { border-color: var(--info); color: var(--fg-dim); }
.crash-position.pending .pos-tag { color: var(--info); }
.crash-position.active { border-color: var(--brand); box-shadow: inset 0 0 0 1px var(--brand-glow); }
.crash-position.active .pos-tag { color: var(--violet); }
.crash-position.cashed { border-color: var(--win); box-shadow: inset 0 0 0 1px var(--win-glow); }
.crash-position.cashed .pos-tag, .crash-position.cashed b { color: var(--win); }
.crash-position.busted { border-color: var(--lose); }
.crash-position.busted .pos-tag, .crash-position.busted b { color: var(--lose); }

/* action area: bet controls + cash-out button */
.crash-actions { margin: var(--sp-3) 0; }
.crash-bet { transition: opacity var(--dur-2); }
/* "Betting window closed" disabled state. We dim the AFFORDANCES (inputs, odds line, Bet button) to
   read as inactive, but DO NOT halve the opacity of the informational text labels — at opacity .5 the
   --muted labels render ~2.74:1 (fails AA). The labels stay at full --muted (>=4.5:1) so the field
   names remain readable while the control still reads as dimmed/inactive. */
.crash-bet.dimmed { pointer-events: none; }
.crash-bet.dimmed input,
.crash-bet.dimmed .crash-odds,
.crash-bet.dimmed .bet-btn { opacity: .5; }
/* Keep ALL field-label text (incl. the nested "opt." note) at full --muted so it stays >=4.5:1. */
.crash-bet.dimmed .seg > span,
.crash-bet.dimmed .seg > span .muted { opacity: 1; color: var(--muted); }
/* Stake quick-adjust (½ / 2× / presets) — crash-live.js injects #crashStakeQuick under the stake input.
   Mirrors the shared .stake-quick chrome so the live Rocket has the same affordance as every other game.
   Sits inside the stake's label.seg (column flow), directly under the input. */
.crash-stake-quick { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
.crash-stake-quick button { padding: 5px 12px; font-size: var(--fs-xs); min-width: 44px; }
@media (hover: none), (pointer: coarse) { .crash-stake-quick button { min-height: var(--touch); } }
/* Max: subtly accented so "go big" reads as a deliberate affordance (parity with the shared rail). */
.crash-stake-quick button.crash-stake-max {
  font-weight: var(--fw-semi);
  color: var(--accent, #8b5cf6);
  border-color: color-mix(in oklab, var(--accent, #8b5cf6) 45%, var(--line-2, var(--line)));
}
.crash-stake-quick button.crash-stake-max:hover:not(:disabled) {
  background: color-mix(in oklab, var(--accent, #8b5cf6) 16%, transparent);
}
/* Rebet: a quieter "repeat last stake" — pushed to the row end so it reads as the trailing utility action. */
.crash-stake-quick button.crash-stake-rebet { margin-left: auto; }
.crash-bet-foot { display: flex; flex-wrap: wrap; align-items: stretch; gap: var(--sp-3); margin-top: var(--sp-3); }
.crash-odds { flex: 1; align-self: center; font-size: var(--fs-sm); color: var(--fg-dim); font-variant-numeric: tabular-nums; }
/* House max-win cap indicator (crash-live.js injects #crashMaxWin): a small, unobtrusive pill so players
   know payouts are capped at 100× (mirrors MAX_CRASH_MULTIPLIER_X100). Sits at the start of the foot row. */
.crash-maxwin {
  flex: 0 0 100%; align-self: center; font-size: var(--fs-sm); color: var(--fg-dim);
  font-variant-numeric: tabular-nums; letter-spacing: .02em;
}
/* Content-width Bet button in the foot row (odds text on the left, Bet on the right). Override the base
   `.bet-btn { width:100% }` — at 100% the flex item took the FULL foot width and overlapped the odds,
   overflowing the column by ~31px on tablet (#/play). On phone the foot stacks + the button goes 100%
   again (see the <=720px block). */
.crash-bet-foot .bet-btn { flex: none; width: auto; min-width: 140px; padding: 14px 20px; }
.cashout-btn {
  width: 100%; padding: 18px; font-size: var(--fs-lg); margin-top: var(--sp-2);
  background: linear-gradient(180deg, var(--win), var(--win-grad-end)); color: #00120a;
  animation: cashout-glow 1.1s var(--ease-in-out) infinite;
}
.cashout-btn:hover { box-shadow: 0 6px 26px var(--win-glow); }
@keyframes cashout-glow { 0%,100% { box-shadow: 0 4px 18px rgba(31,212,126,.25); } 50% { box-shadow: 0 6px 30px rgba(31,212,126,.55); } }
@media (prefers-reduced-motion: reduce) { .cashout-btn { animation: none; } }
/* LIVE-bet CANCEL — destructive-tone bet-window pull (shown only with a placed, un-launched bet). It sits on
   its OWN line BELOW the Bet button (order:3 + full-width wrap), NOT beside/where Bet was: previously Cancel
   un-hid into the foot row and pushed the Bet button aside, so a reflexive second click landed on Cancel and
   pulled the bet. Now a repeat click on the Bet spot hits the (disabled) Bet button — a no-op — and Cancel is a
   smaller, clearly-secondary, deliberate action underneath. (Owner: "cancel replaced Bet in its spot — easy to
   cancel by accident".) The 2-class selector beats `.crash-bet-foot .bet-btn` above so the layout overrides win. */
.crash-bet-foot .crash-cancel-btn {
  order: 3; flex: 0 0 100%; width: 100%; min-width: 0; margin-top: var(--sp-1);
  padding: 9px 16px; font-size: var(--fs-sm); min-height: 0;
  background: transparent; color: var(--lose, #ff6b6b); border: 1px solid var(--lose, #ff6b6b);
}
.crash-cancel-btn:hover:not(:disabled) { filter: brightness(1.12); }
.crash-cancel-btn.hidden { display: none; }
.crash-auth-note { color: var(--warn); }

/* live cashouts feed */
.crash-feed-wrap { margin: var(--sp-4) 0 var(--sp-2); }
.crash-feed-head { font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); font-weight: var(--fw-semi); margin-bottom: var(--sp-2); }
.crash-feed { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; max-height: 168px; overflow-y: auto; }
.crash-feed:empty::after { content: "no cashouts yet"; color: var(--muted); font-size: var(--fs-sm); }
.crash-feed-item {
  display: flex; align-items: center; gap: var(--sp-2); padding: 6px 10px; border-radius: var(--r-sm);
  background: var(--surface-2); border: 1px solid var(--line); font-size: var(--fs-sm);
  animation: chip-in var(--dur-2) var(--ease-spring) both;
}
.crash-feed-item .fx { font-weight: var(--fw-bold); color: var(--win); font-variant-numeric: tabular-nums; min-width: 64px; }
.crash-feed-item .fp { color: var(--fg-dim); font-variant-numeric: tabular-nums; }
.crash-feed-item .ftag { margin-left: auto; font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .05em; color: var(--gold); border: 1px solid rgba(255,207,92,.3); border-radius: var(--r-sm); padding: 1px 6px; }

/* =============================================================================
   One-shot CRASH panel (instant auto-cashout via /play — the #/play tab panel)
   -----------------------------------------------------------------------------
   MIGRATED onto the shared gv-controls layout system: the target field (gv-controls
   stepper, #crashTarget), the INSTANT clarity note (#crashMode), the inline validity
   hint (gv-controls statusLine, #crashTargetErr), and the Win chance/Win pays preview
   (#crashWinChance/#crashPays) are built by crash.js and mounted in the LEFT rail
   (#gameControlsSlot). The shared chrome (.gv-stepper / .gv-status / .preview / over-cap
   gate) is owned by gv-controls.css; this file only adds the crash-specific note chip +
   the settled-round reveal line, and neutralizes the legacy in-panel margins so the rail
   flow owns the rhythm.
   ========================================================================== */
/* clarity chip: this is the INSTANT auto-cashout, distinct from the live Rocket round.
   Margins zeroed — it now sits in the rail flow (#gameControlsSlot) under the target field. */
.crash-mode-note {
  display: flex; align-items: flex-start; gap: var(--sp-2);
  margin: 0; font-size: var(--fs-sm); color: var(--fg-dim); line-height: 1.4;
}
.crash-mode-tag {
  flex: none; font-size: var(--fs-xs); font-weight: var(--fw-bold); letter-spacing: .08em;
  color: var(--cyan); border: 1px solid var(--cyan); border-radius: var(--r-sm);
  padding: 2px 7px; text-transform: uppercase; line-height: 1.3;
}
/* inline target-validity hint — a gv-controls statusLine. Color/size come from the shared
   .gv-status(.invalid) chrome; nothing crash-specific to fork here (margin zeroed for the rail). */
.crash-target-err { margin: 0; }
/* the win-chance stat sits beside "Win pays" in the shared .preview grid (built by preview()). */
#crashWinChance { font-variant-numeric: tabular-nums; }

/* in-panel settled-round reveal (one-shot): paints ONLY the server response (cashed/busted vs target).
   CLS: this line is now ALWAYS in flow (crash.js no longer display:none-toggles it) so it can't pop into
   the centered .panel void on settle. Reserve the 2-LINE worst case up front (min-height 3em ≈ 2 × 1.4
   line-height) so a wrapped reveal — e.g. "Cashed out at 100.00× - the round flew to 100.00×." — fills
   the SAME box as the idle/1-line state: zero shift. The idle state (.is-empty) keeps the box reserved
   via visibility:hidden (NOT display:none), and the result-in transform/opacity animation stays CLS-free
   (transform doesn't affect layout). */
.crash-reveal {
  margin: var(--sp-2) 0 0; padding: 8px 12px; border-radius: var(--r-md);
  min-height: 3em;
  font-size: var(--fs-sm); font-variant-numeric: tabular-nums; line-height: 1.4;
  border: 1px solid var(--line-2); background: var(--surface-2); color: var(--fg-dim);
  animation: result-in var(--dur-2) var(--ease-spring) both;
}
/* idle/pre-result + post-reset: keep the reserved box in flow but invisible (no display:none pop).
   visibility:hidden also takes it out of the a11y tree, matching the old display:none semantics. */
.crash-reveal.is-empty { visibility: hidden; }
@media (prefers-reduced-motion: reduce) { .crash-reveal { animation: none; } }
.crash-reveal.win { border-color: var(--win); color: var(--win); box-shadow: inset 0 0 0 1px var(--win-glow); }
.crash-reveal.loss { border-color: var(--lose); color: var(--lose); }

/* Reconnect: a recovery control, but still a tap target — meet the 44px a11y floor (game-specific). */
#crashConnect { min-height: 44px; padding: 0 16px; }

/* =============================================================================
   ROCKET — two-column layout (compact controls/info rail LEFT + large chart RIGHT)
   -----------------------------------------------------------------------------
   Tuned for a 1080x1920 PORTRAIT surface: the old tall single column read sparse
   (header ate ~20% up top; the bet + provably-fair block sprawled as a wide, low-
   density strip; the chart only got the leftover). This mirrors the .gameview grid
   (fixed-width left rail + flexible right display) so the chart stays LARGE and the
   minimal-text controls pack densely in the narrow rail. crash-live.js is untouched —
   it addresses every element by id, all of which remain descendants of #liveCrash.
   ========================================================================== */

/* Slim header band: back-link + compact title + connection status on one row, with
   the long primer collapsed into a one-line <details>. Keeps the header a thin band
   instead of the multi-line back-link + title + badge + paragraph stack. */
.crash-head {
  display: flex; align-items: center; gap: var(--sp-2) var(--sp-3); flex-wrap: wrap;
  margin: 0 0 var(--sp-3);
}
.crash-title { margin: 0; font-size: var(--fs-lg); display: inline-flex; align-items: center; gap: var(--sp-2); }
/* connection status pushed to the right end of the band */
.crash-head #crashConn { margin-left: auto; }
/* Fairness sits top-right next to the connection status (matches every other game's header), not on its own
   row; the description is the info icon (.crash-info) right after the title now. */
.crash-head .gv-fairness { margin-left: var(--sp-2); }
.crash-head .crash-info { flex: 0 0 auto; }
.crash-game-info { margin-top: var(--sp-3); }

/* Two-column grid: single column by default (mobile-first → chart first via source order),
   two columns at >=980px (covers the 1080-wide portrait). align-items:start so the rail
   doesn't stretch to the chart's tall height. */
.crash-layout { display: grid; grid-template-columns: 1fr; gap: var(--sp-3); align-items: start; }
.crash-rail, .crash-display { min-width: 0; }

/* LEFT rail: a stack of labeled SECTIONS (.crash-sec) separated by hairline dividers, so the
   controls read as organized groups (Bet · Recent crashes · Live cashouts · Provably-fair)
   rather than a loose stack. Uniform gap between sections; the legacy per-block margins are
   collapsed since each block now sits inside a section. */
.crash-rail { display: flex; flex-direction: column; gap: 0; }
.crash-sec { padding: var(--sp-3) 0; border-top: 1px solid var(--line); }
.crash-sec:first-child { padding-top: 0; border-top: 0; }
.crash-sec:last-child { padding-bottom: 0; }
/* section heading: small, uppercase, consistent across the rail */
.crash-sec-title {
  margin: 0 0 var(--sp-2); font-family: var(--font-display);
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .07em;
  color: var(--muted); font-weight: var(--fw-bold); display: flex; align-items: center; gap: 7px;
}
/* neutralize the old free-floating margins (the blocks are sectioned now) */
.crash-rail .crash-actions,
.crash-rail .crash-position,
.crash-rail .crash-history,
.crash-rail .crash-feed-wrap,
.crash-rail .crash-reconnect-row { margin: 0; }
.crash-rail .crash-position { margin-top: var(--sp-2); }
.crash-rail .crash-reconnect-row { margin-top: var(--sp-2); }
/* in the narrow rail, drop the bet foot to two rows (odds line over a full-width Bet) so
   the Bet button doesn't overlap the odds text (the override on .crash-bet-foot .bet-btn
   min-width:140px would otherwise crowd a ~300px column). */
.crash-rail .crash-bet-foot { flex-direction: column; align-items: stretch; }
.crash-rail .crash-bet-foot .bet-btn { width: 100%; min-width: 0; }
.crash-rail .crash-feed { max-height: 132px; }
/* CLEAN VERTICAL STACK — the bet panel now mirrors the shared #play .stake-field chrome (stake input
   full-width, ½/2×/preset quick-row directly under it, Auto cash-out on its own line, then the Bet
   button). Was a side-by-side seg .row where the injected quick-buttons made the Stake column taller
   than the Auto column (the misalignment the owner flagged). Consistent --sp gaps, all full-width. */
.crash-bet { display: flex; flex-direction: column; gap: var(--sp-3); }
.crash-bet .crash-stake-field,
.crash-bet .crash-auto-field { min-width: 0; }
.crash-bet .crash-stake-field input,
.crash-bet .crash-auto-field input { width: 100%; }
/* the bet-foot now sits in the flex column flow — drop its standalone margin so the column `gap`
   owns the rhythm (no doubled gap above the odds line / Bet button). */
.crash-bet .crash-bet-foot { margin-top: 0; }

/* ---- Provably-fair: a STABLE label/value grid that does NOT stretch when filled. -----------
   The user reported the rail jumping when #crashVerify / #crashSeed populate AFTER a round
   resolves. Fix: a fixed two-row-per-entry grid (label over value), each VALUE cell carries a
   reserved min-height and the long hash/seed strings wrap inside the fixed box (overflow-wrap:
   anywhere) instead of growing it. The rail width + per-row height stay constant whether the
   value is "—" or a 64-char hash. */
.crash-fair { margin: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.crash-fair .cf-row { display: grid; grid-template-columns: 1fr; gap: 2px; }
.crash-fair dt { font-size: var(--fs-xs); color: var(--muted); font-weight: var(--fw-semi); }
.crash-fair dd {
  margin: 0; font-size: var(--fs-sm); color: var(--fg-dim);
  /* reserve a FIXED height (min == max): a "—" and a wrapped 64-char hash occupy the SAME box,
     so filling in causes zero layout shift. The hash wraps + clips inside this fixed 3-line box. */
  min-height: 3.6em; max-height: 3.6em; overflow: hidden;
  display: flex; align-items: flex-start;
}
.crash-fair dd code,
.crash-fair dd #crashVerify {
  font-variant-numeric: tabular-nums; line-height: 1.3;
  /* wrap long commitments/seeds INSIDE the fixed box (don't grow the container) */
  overflow-wrap: anywhere; word-break: break-all; min-width: 0; max-width: 100%;
  display: block;
}
/* The verify-result fills LAST (after a round resolves) with two text segments + check/x SVG
   icons — the row the user saw stretching. Its filled string ("recomputed 12.47× ✓ matches ·
   sha256(seed)==commitment ✓") wraps to ~3 lines in this ~300px column, so reserve a STABLE
   3-line box up front (min == max). Going from "—" to the full string then causes ZERO shift;
   content wraps + clips inside the reserved box. */
.crash-fair dd:has(#crashVerify) { min-height: 4.1em; max-height: 4.1em; align-items: flex-start; }
.crash-fair #crashVerify { line-height: 1.35; }
.crash-fair #crashVerify svg { width: 13px; height: 13px; vertical-align: -2px; }

/* RIGHT display: the chart fills the column. min-height keeps it tall on portrait even
   before the aspect-ratio box resolves, so crash-chart.js measures a real box.
   TALL: drop the 16/9 aspect cap here so the stage can grow to fill the column's height
   (the rail is short; the chart should claim the rest of the viewport). crash-chart.js
   already re-measures its canvas backing store on every resize (window resize +
   setActive()), scaling the 2D context by DPR and redrawing crisply — so a taller box is
   picked up with no JS change and no run reset (the curve is driven from the server clock). */
.crash-display { display: flex; flex-direction: column; }
.crash-display .crash-stage {
  margin: 0; flex: 1; aspect-ratio: auto;
  /* fill the viewport height below the topbar + slim crash header; scales with the screen,
     floored so short laptops stay usable and capped so tall monitors don't sprawl. */
  min-height: clamp(300px, calc(100vh - 230px), 900px);
}

@media (min-width: 980px) {
  /* desktop / 1080-portrait: fixed-width controls rail (left) + flexible LARGE chart (right).
     On the crash route the global sidebar is collapsed to the 64px icon-rail (see styles.css
     "CRASH ROUTE — reclaim the sidebar width"), so the card is now ~984px at 1080-wide and the
     chart fills the reclaimed space. Rail ~300px; tight gap + slim card padding hand the rest
     to the chart (chart lands ~650px+). */
  .crash-layout { grid-template-columns: 300px minmax(0, 1fr); gap: var(--sp-2); }
  #liveCrash { padding-left: var(--sp-3); padding-right: var(--sp-3); }
  /* give the chart real vertical room so it stays LARGE + tall on the portrait surface (the
     16/9 box would otherwise be short relative to the rail's stacked content). Scale with the
     viewport height so the chart fills the screen below the topbar + header (no page scroll),
     with a generous floor for the portrait surface and a cap so tall monitors don't sprawl. */
  .crash-display .crash-stage { min-height: clamp(540px, calc(100vh - 210px), 900px); }
}

@media (max-width: 979.98px) {
  /* phone / narrow: stack to one column with the CHART FIRST, controls below. The chart is
     source-ordered SECOND, so re-order it above the rail. */
  .crash-display { order: 1; }
  .crash-rail { order: 2; }
  .crash-head #crashConn { margin-left: 0; }
}

/* PHONE (<=720px) — BET REACHABILITY (cluster-C in-game fit).
   The desktop/portrait chart height `clamp(300px, calc(100vh - 230px), 900px)` ≈ 614px on an 844px phone,
   so the chart ALONE filled the viewport and shoved the whole bet rail (stake + auto cash-out + Bet CTA)
   below the fold — the primary action (Bet, then the live Cash-Out) was unreachable without scrolling past
   the entire chart. Cap the chart to a bounded HERO on phones (clamp(220px, 40vh, 360px) ≈ 338px @844) so
   the chart stays the dominant element while the stake field + Bet CTA are co-visible on load. The .crash-
   actions bar is already position:sticky (06-mobile-bet-responsive.css) so once the short rail scrolls the
   Bet/Cash-Out pins under the thumb; a shorter chart is what makes it reachable WITHOUT scrolling too.
   Desktop (>=980px) keeps its tall chart via the min-width:980 block above (this is phone-only). */
@media (max-width: 720px) {
  .crash-display .crash-stage { min-height: clamp(220px, 40vh, 360px); }
}
