/* blazeplinko.css — 🪀 Blaze Drop.
 *
 * Modeled on blazedice.css: same dark-panel, gold-glow, Comic-Sans identity as
 * the rest of the site, and self-sufficient on purpose — nav.css is the only
 * other stylesheet this page links, so the .viewer-info-btn / .viewer-modal-*
 * "how it works" block and the footer/trust-strip rules are copied here rather
 * than shared.
 *
 * nav.css CONSUMES --navbar-bg, --glow, --c-gold and --c-orange from the page's
 * own stylesheet. A page that forgets them renders the navbar transparent with
 * no shadow, and an unresolved var() fails silently. They are declared first,
 * below, for exactly that reason.
 *
 * THE BOARD'S GEOMETRY, in one place. --bp-pitch is the horizontal distance
 * between two neighbouring pegs and --bp-row-pitch the vertical distance
 * between two rows. blazeplinko.js never computes a pixel: it writes two
 * UNITLESS numbers onto the ball — --bp-off (horizontal position in HALF
 * pitches; a 12-row board spans -12..+12) and --bp-row (which row it has
 * reached) — and the transform below turns those into a position. That keeps
 * the physics in JS, the layout in CSS, and lets the board resize at a
 * breakpoint without the animation knowing anything about it.
 */

*, *::before, *::after { box-sizing: border-box; }

:root {
    --nav-h: 80px;
    --touch: 44px;

    /* The four tokens nav.css needs. Do not remove. */
    --navbar-bg: rgba(0, 0, 0, 0.95);
    --glow: 0 0 10px rgba(255, 69, 0, 0.7);
    --c-gold: #ffcf48;
    --c-orange: orange;

    --c-red: #ff4500;
    --c-green: #2ecc71;

    /* Board geometry. 13 pegs across the widest row, so --bp-pitch * 13 is
       roughly the board's inner width. */
    --bp-peg: 9px;
    --bp-pitch: 34px;
    --bp-row-pitch: 30px;
    /* Owner feedback, 2026-08-06: "the ball size should be a bit smaller."
       25% down from the original 20px — still comfortably clears the 34px
       peg pitch (and the narrower pitches at the phone breakpoints below;
       verified against tests/blazeplinko.spec.js's 390px overflow checks). */
    --bp-ball: 15px;

    /* TEXT CONTRAST. Two solid warm neutrals, both well clear of 7:1 on the
       dark panel. Translucent text over a translucent panel over a photograph
       has no fixed contrast ratio at all — it is whatever pixel is behind it.
         --c-soft  secondary copy that sits next to a heading
         --c-dim   the quietest line on the page — still readable, still opaque */
    --c-soft: #f2ece0;
    --c-dim: #ded6c6;
}

body {
    margin: 0;
    padding: 0;
    font-family: var(--font-body, "Comic Sans MS", system-ui, sans-serif);
    background: url('images/BlazeitBG.png') no-repeat center center;
    background-size: 100% auto;
    background-repeat: no-repeat;
    background-position: center center;
    background-attachment: fixed;
    background-color: #b0e8f7;
    color: white;
    display: flex;
    flex-direction: column;
    align-items: center;
    min-height: 100vh;
    width: 100%;
    overflow-x: hidden;
    padding-top: calc(var(--nav-h) + 24px);
}

@media screen and (max-width: 768px) {
    body { padding-top: calc(var(--nav-h) + 16px); }
}

/* Sticky footer: body is the flex column, main takes the slack, footer
   stretches across the bottom.

   THREE COLUMNS, same arrangement as blazedice.html. minmax(0, …) on every
   track is load-bearing: a grid item's default min-width is auto, so a long
   unbroken string inside any panel would push its track wider than its share
   and blow the page out horizontally. */
.container {
    flex: 1 1 auto;
    width: 100%;
    max-width: 1500px;
    padding: 2rem;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.75fr) minmax(0, 1fr);
    align-items: start;
    gap: 20px;
}

.info-panel,
.game-panel {
    min-width: 0;
    background: rgba(0, 0, 0, 0.75);
    padding: 25px;
    border-radius: 20px;
    box-shadow: 0 0 10px rgba(255, 69, 0, 0.6);
}

.game-panel { text-align: center; }
.info-panel { text-align: left; }
.info-panel h3, .info-panel h2 { text-align: center; }

.title {
    font-size: 2rem;
    margin-bottom: 5px;
    color: var(--c-gold, #ffcf48);
    text-shadow: 0 0 6px #ff8c00, 0 0 10px #ff4500;
    font-weight: bold;
}

.tagline {
    margin: 0 0 14px;
    color: var(--c-soft);
    font-size: 0.95rem;
}

.wallet-btn {
    padding: 10px 20px;
    border-radius: 10px;
    background-color: orange;
    color: black;
    font-weight: bold;
    border: none;
    cursor: pointer;
    min-height: var(--touch);
    transition: background-color 0.2s, opacity 0.2s;
    font-family: inherit;
    font-size: 1rem;
}
.wallet-btn:hover:not(:disabled) { background-color: darkorange; }
.wallet-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wallet-btn:focus-visible { outline: 3px solid var(--c-gold); outline-offset: 2px; }

.stats {
    background-color: rgba(0, 0, 0, 0.6);
    border-radius: 15px;
    padding: 20px;
    margin-bottom: 20px;
    box-shadow: 0 0 10px rgba(255, 69, 0, 0.5);
    text-align: left;
    overflow-wrap: break-word;
    word-break: break-word;
    max-width: 100%;
}
.stats h3, .stats h2 { margin: 0 0 10px; color: var(--c-gold, #ffcf48); }
.stats p { margin: 0 0 6px; font-size: 0.95rem; }
.stats p:last-child { margin-bottom: 0; }

/* ---- The hero ---- */
.bp-hero {
    width: 100%;
    display: flex;
    flex-direction: column;
    align-items: center;
    margin: 0 0 6px;
}
.bp-hero .bp-result,
.bp-hero .bp-quote {
    width: 100%;
    max-width: 640px;
}

/* ---- Banners ---- */
.bp-banner {
    border-radius: 12px;
    padding: 12px 16px;
    margin: 0 0 14px;
    font-size: 0.95rem;
    text-align: center;
}
.bp-banner[hidden] { display: none; }
.bp-banner-warn {
    background: rgba(255, 69, 0, 0.18);
    border: 1px solid rgba(255, 122, 24, 0.7);
    color: #ffd9a8;
}

/* ---- The board ----
   position: relative is the ball's containing block. Every dimension derives
   from --bp-pitch / --bp-row-pitch so the pegs, the buckets and the ball all
   move together when the pitch changes at a breakpoint. */
.bp-board {
    position: relative;
    width: 100%;
    max-width: calc(var(--bp-pitch) * 13);
    min-width: calc(var(--bp-pitch) * 13);
    margin: 6px auto 12px;
    padding: calc(var(--bp-row-pitch) * 0.8) 0 0;
    display: flex;
    flex-direction: column;
    align-items: center;
}

.bp-pegs {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: calc(var(--bp-row-pitch) - var(--bp-peg));
    width: 100%;
    /* NO TRANSFORM HERE, DELIBERATELY (2026-08-08, fifth screenshot round:
       "fix it to be symmetrical from side to side"). An earlier fix slid the
       whole pyramid left by half a pitch to stop the last row sitting on the
       bucket CENTRES. It worked, and it also put every single row half a
       pitch left of the board's centre line — which is the asymmetry the
       owner then reported, and which no choice of shift can undo (a uniform
       shift S leaves margins 0.5*pitch - S - peg/2 and 0.5*pitch + S -
       peg/2, equal only at S = 0). blazeplinko.js's buildPegs() now emits
       the canonical triangle instead — row r carries exactly r pegs — so
       the last row's 12 pegs land on the 12 interior bucket boundaries with
       NOTHING moved: flex centring alone makes every row symmetric, at
       every breakpoint, and the peg-0 endcap patch that used to sit below
       this rule is gone with it. */
}

.bp-peg-row {
    display: flex;
    justify-content: center;
    gap: calc(var(--bp-pitch) - var(--bp-peg));
    height: var(--bp-peg);
}

.bp-peg {
    position: relative;
    width: var(--bp-peg);
    height: var(--bp-peg);
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #fff6e0 0%, #ffd9a8 55%, #f0a545 100%);
    box-shadow: 0 0 6px rgba(255, 122, 24, 0.6);
    flex: 0 0 auto;
}

/* The chip. --bp-off is HALF pitches (an integer in -12..12), --bp-row is the
   row it has reached (0..12). Both are unitless numbers written by JS.
   `transform` (translateX/translateY) is JS-owned position; the independent
   `rotate`/`scale` CSS properties below are CSS-owned motion (idle wobble,
   bounce squash) so the two never fight over one property. */
.bp-ball {
    position: absolute;
    /* VERTICAL CENTERING FIX (owner report, 2026-08-07: "gets off the
       rails"). `top: 0` plus `translateY(row-pitch * row)` puts the ball's
       TOP edge at the row's y — its CENTER then lands `ball/2` below that,
       while a peg row's own center sits at `0.8 * row-pitch` (the board's
       padding-top) below the row above it, offset by `peg/2`. Those two
       don't cancel unless ball and peg happen to be the same size, so a
       constant, size-aware gap opened up between the ball's rendered
       center and the peg it actually struck — audited on 2026-08-07 and
       measured at exactly 9px on desktop (15px ball) after the resize,
       11.5px before it at the old 20px ball, so this was already present,
       just partly masked at the old size. This `top` bakes in the
       correction so the two centers coincide EXACTLY, at every row and
       every breakpoint (all four terms are var()-driven, so mobile/390px
       recomputes it automatically — see
       "the ball sits dead-center on the peg it struck" in
       tests/blazeplinko.spec.js, which checks this to sub-pixel precision).
       HORIZONTAL is untouched: translateX's `-50%` is relative to the
       ball's OWN box, so it was already diameter-independent and measured
       dx=0.000px exact against the real peg DOM before this fix — see the
       same test. */
    top: calc(-1 * (0.2 * var(--bp-row-pitch) + (var(--bp-ball) - var(--bp-peg)) / 2));
    left: 50%;
    width: var(--bp-ball);
    height: var(--bp-ball);
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #fff3c4 0%, #ffb347 45%, #ff4500 100%);
    box-shadow: 0 0 12px rgba(255, 69, 0, 0.9), 0 0 22px rgba(255, 207, 72, 0.5);
    /* THE FINAL DROP (owner report, 2026-08-07, THIRD screenshot round: "the
       ball comes to rest at peg-row level ON a peg column — it never
       visually enters a bucket... its final translate must continue past
       the last peg row down to the bucket row, landing centered on the
       winning cube, overlapping its top edge"). --bp-row's own formula
       (row-pitch * row) tops out at ROWS — the last PEG row's y — because
       it never had anywhere else to send it.

       --bp-drop is 0 for every mid-flight frame and 1 ONLY on the landing
       frame (blazeplinko.js's settleBall(), both the live per-row loop and
       the reduced-motion instant path). When it flips on, this adds a
       CONSTANT extra translateY, in the same var()-driven units the rest of
       this file already uses so it self-corrects at every breakpoint:
         0.6 * --bp-row-pitch + 0.5 * --bp-peg
           the exact vertical gap from the last peg row's rendered center to
           the bucket row's TOP edge — derived from the same box-model
           numbers `.bp-pegs`' own height (peg + 11 * row-pitch) and
           `.bp-buckets`' `margin-top: 0.6 * row-pitch` already use, not
           eyeballed.
         + 0.5 * --bp-ball
           an extra half-ball dip so the ball's OWN top edge lands exactly
           on the bucket's top edge and its body reads as sitting IN the
           cube, not merely touching the boundary line.
       --bp-off on the landing frame is the bucket-centre value
       (ballOffsets()'s own offsets[ROWS]) — an EVEN half-pitch offset, so it
       falls exactly between the last peg row's two nearest dividers, which
       sit at odd offsets. Only Y needed the extra distance; X was already
       correct. */
    transform:
        translateX(calc(-50% + var(--bp-pitch) * var(--bp-off, 0) / 2))
        translateY(calc(
            var(--bp-row-pitch) * var(--bp-row, 0) +
            var(--bp-drop, 0) * (0.6 * var(--bp-row-pitch) + 0.5 * var(--bp-peg) + 0.5 * var(--bp-ball))
        ));
    /* Owner feedback, 2026-08-06: "the ball dropping effect is super fast
       and a bit weird — tweak it to look more physical as a drop." The old
       curve (0.45, 0.05, 0.55, 0.95) was a symmetric ease-in-out — slow at
       BOTH ends of every hop, which reads as a slide, not a fall. This is a
       monotonic ease-in (easeInCubic-shaped): each hop starts gently and
       keeps accelerating all the way to the peg it lands on, same as
       gravity, so the hop between pegs reads as a drop-and-bounce rather
       than a smooth glide. Paired with blazeplinko.js's per-row
       acceleration (ROW_PACE_MS_MIN/MAX — row 1 slow, the last row fast),
       not just this curve. */
    transition: transform var(--bp-step-ms, 110ms) cubic-bezier(0.32, 0, 0.67, 0);
    z-index: 2;
    opacity: 0;
}
/* Idle IS visible — a wobbling ember perched at the drop point is the
   invitation to play (owner directive: "board subtly alive, inviting the
   drop"). Only the true pre-init instant (no data-state set at all) stays
   invisible. */
.bp-ball[data-state="falling"],
.bp-ball[data-state="landed"],
.bp-ball[data-state="idle"] { opacity: 1; }
.bp-ball[data-state="landed"] { box-shadow: 0 0 18px rgba(255, 207, 72, 1), 0 0 34px rgba(255, 69, 0, 0.7); }

/* ---- The buckets ----
   Thirteen equal cells under the board. Colour scales cold -> gold -> fire by
   multiplier; blazeplinko.js picks the tier class from the LIVE curve, so a
   board relabel recolours it too. */
.bp-buckets {
    display: flex;
    width: 100%;
    gap: 2px;
    margin-top: calc(var(--bp-row-pitch) * 0.6);
}

.bp-bucket {
    flex: 1 1 0;
    min-width: 0;
    padding: 6px 1px;
    border-radius: 6px;
    font-size: 0.62rem;
    font-weight: bold;
    text-align: center;
    color: #1a0d00;
    overflow: hidden;
    text-overflow: ellipsis;
    transition: transform 0.18s, box-shadow 0.18s;
}
.bp-bucket-cold { background: #6fb7d6; }
.bp-bucket-cool { background: #a8d8ea; }
.bp-bucket-warm { background: var(--c-gold); }
.bp-bucket-hot  { background: #ff9d3c; }
.bp-bucket-fire { background: var(--c-red); color: #fff6e0; }

.bp-bucket-hit {
    transform: translateY(4px) scale(1.14);
    box-shadow: 0 0 16px rgba(255, 207, 72, 0.95), 0 0 28px rgba(255, 69, 0, 0.7);
}

.bp-ball-readout {
    margin: 10px 0 12px;
    font-size: 1.05rem;
    color: var(--c-gold);
    font-weight: bold;
}

/* ---- Result + quote ---- */
.bp-result {
    border-radius: 14px;
    padding: 14px 16px;
    margin: 0 auto 10px;
    font-size: 1.05rem;
    font-weight: bold;
    text-align: center;
}
.bp-result[hidden] { display: none; }
.bp-result.win {
    background: rgba(46, 204, 113, 0.16);
    border: 1px solid rgba(46, 204, 113, 0.8);
    color: #b8ffcf;
}
.bp-result.loss {
    background: rgba(255, 69, 0, 0.16);
    border: 1px solid rgba(255, 122, 24, 0.8);
    color: #ffd9a8;
}
.bp-result.neutral {
    background: rgba(255, 255, 255, 0.1);
    border: 1px solid rgba(255, 255, 255, 0.35);
    color: #fff;
}
.bp-quote {
    margin: 0 auto 16px;
    min-height: 1.2em;
    font-size: 0.95rem;
    font-style: italic;
    color: var(--c-soft);
    text-align: center;
}

/* ---- The console ---- */
.bp-console {
    text-align: center;
    border: 1px solid rgba(255, 122, 24, 0.55);
    box-shadow: 0 0 14px rgba(255, 69, 0, 0.55);
}
.bp-budget-line {
    margin: 0 0 8px;
    font-size: 0.95rem;
    color: var(--c-gold);
    font-weight: bold;
    min-height: 1.2em;
}
.bp-call-summary { margin: 0 0 6px; font-size: 1rem; color: var(--c-soft); }
#bpCallText { color: var(--c-gold); font-weight: bold; }
.bp-payout-preview { margin: 0 0 8px; font-size: 0.95rem; color: var(--c-gold); min-height: 1.2em; }

/* Native-S oracle fee, VRF mode only. Cooler than the gold money copy on
   purpose — this is a cost in a different currency, not part of the payout. */
.bp-vrf-fee {
    margin: 0 0 10px;
    padding: 8px 10px;
    border-radius: 8px;
    background: rgba(120, 180, 255, 0.1);
    border: 1px solid rgba(120, 180, 255, 0.35);
    font-size: 0.85rem;
    color: #bcd9ff;
}
.bp-vrf-fee[hidden] { display: none; }

/* ---- Pickers ---- */
.bp-picker { text-align: left; }
.bp-group-label { margin: 0 0 8px; font-size: 0.9rem; color: var(--c-soft); }
.bp-seg { display: flex; flex-wrap: wrap; gap: 8px; }
.bp-pick {
    flex: 1 1 100px;
    min-height: var(--touch);
    padding: 8px 12px;
    border-radius: 10px;
    border: 1px solid rgba(255, 207, 72, 0.45);
    background: rgba(0, 0, 0, 0.5);
    color: var(--c-gold);
    font-family: inherit;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color 0.15s, border-color 0.15s, transform 0.15s;
}
.bp-pick:hover { background: rgba(255, 122, 24, 0.2); border-color: rgba(255, 207, 72, 0.95); }
.bp-pick[aria-pressed="true"] {
    background: orange;
    color: #000;
    border-color: #fff;
    font-weight: bold;
    transform: scale(1.03);
}

/* ---- Amount + budget ---- */
.bp-amount, .bp-budget { text-align: left; }
#bpAmountInput {
    width: 100%;
    padding: 12px 14px;
    min-height: var(--touch);
    border-radius: 10px;
    border: 1px solid rgba(255, 207, 72, 0.45);
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-family: inherit;
    font-size: 1.1rem;
}
.bp-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.bp-chip, .bp-budget-chip {
    flex: 1 1 60px;
    min-height: var(--touch);
    padding: 8px 10px;
    border-radius: 999px;
    border: 1px solid rgba(255, 207, 72, 0.45);
    background: rgba(0, 0, 0, 0.5);
    color: var(--c-gold);
    font-family: inherit;
    font-size: 0.95rem;
    cursor: pointer;
    transition: background-color 0.15s, border-color 0.15s;
}
.bp-chip:hover:not(:disabled), .bp-budget-chip:hover { background: rgba(255, 122, 24, 0.2); border-color: rgba(255, 207, 72, 0.95); }

/* 🔥 ALL IN — the one chip whose amount is computed, not typed. Given its own
   weight because it stakes the WHOLE remaining budget in a single drop, and
   dimmed (not hidden) before a budget exists so its tooltip can say why. It
   needs more room than "420" too, hence the wider flex basis. */
.bp-chip-allin {
    flex: 1 1 148px;
    font-weight: bold;
    color: #1a0d00;
    background: linear-gradient(180deg, #ffcf48 0%, #ff7a18 100%);
    border-color: #ffcf48;
}
.bp-chip-allin:hover:not(:disabled) {
    background: linear-gradient(180deg, #ffe08a 0%, #ff9040 100%);
    border-color: #fff;
}
.bp-chip-allin:disabled {
    background: rgba(0, 0, 0, 0.5);
    color: var(--c-dim);
    border-color: rgba(255, 207, 72, 0.25);
    cursor: not-allowed;
    opacity: 0.75;
}
.bp-budget-chip[aria-pressed="true"] {
    background: orange;
    color: #000;
    border-color: #fff;
    font-weight: bold;
}
.bp-budget .bp-chips { margin-top: 0; }
.bp-budget-note { margin: 10px 0 0; font-size: 0.9rem; color: var(--c-dim); min-height: 1.2em; }

.bp-limits { margin: 10px 0 0; font-size: 0.9rem; color: var(--c-dim); }
.bp-amount .bp-limits:last-child { margin-bottom: 0; }
.bp-amount-error { margin: 8px 0 0; font-size: 0.9rem; color: #ff8f6b; }
.bp-amount-error[hidden] { display: none; }

/* ---- Actions ---- */
.bp-actions { margin-top: 0; }
.bp-action-btn { display: block; width: 100%; font-size: 1.15rem; padding: 14px 20px; }
.bp-reclaim-btn { display: block; width: 100%; margin-top: 10px; background-color: var(--c-gold); }
.bp-reclaim-btn[hidden] { display: none; }
.bp-step { margin: 10px 0 0; font-size: 0.9rem; color: var(--c-dim); text-align: center; }
.bp-step[hidden] { display: none; }
.bp-hint { margin: 6px 0 0; font-size: 0.9rem; line-height: 1.5; color: var(--c-dim); text-align: center; }

.bp-tx-status { margin: 12px 0 0; font-size: 0.95rem; color: var(--c-gold, #ffcf48); min-height: 1.2em; }
.bp-tx-link { margin: 6px 0 0; font-size: 0.9rem; }
.bp-tx-link a { color: var(--c-gold); }
/* The Receipt deep link sits on its own line under the SonicScan link. */
.bp-tx-link a.receipt-link { display: block; margin-top: 4px; }

/* ---- History ---- */
.bp-history { list-style: none; margin: 0; padding: 0; }
/* ONE DROP, ONE ROW (owner, 2026-08-14).
   `flex-wrap: wrap` let a long row spill its right half onto a second line, and
   because only SOME rows were long enough to do it the list came out ragged —
   some entries one line, some two, with no pattern to it. Ragged is worse than
   tight: the eye cannot find the column.
   nowrap plus a slightly smaller face keeps every drop on its own line. The
   left half is the one allowed to shrink if a panel ever gets narrower than
   this, because the outcome on the right is the part you are scanning for. */
.bp-history li {
    display: flex;
    flex-wrap: nowrap;
    align-items: baseline;
    gap: 4px 8px;
    justify-content: space-between;
    padding: 7px 0;
    border-bottom: 1px dashed rgba(255, 255, 255, 0.15);
    font-size: 0.82rem;
    white-space: nowrap;
}
.bp-history li > :first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.bp-history li > :last-child { flex: 0 0 auto; }
.bp-history li:last-child { border-bottom: none; }
.bp-history .bp-h-call { color: var(--c-gold); }
.bp-history .bp-h-result.win { color: #b8ffcf; }
.bp-history .bp-h-result.loss { color: #ffb3a0; }
.bp-history .bp-h-result.pending { color: #ffd9a8; }
.bp-history-status { margin: 8px 0 0; font-size: 0.9rem; color: var(--c-dim); }

/* ---- At-a-glance panels ---- */
.bp-howto p { margin: 0 0 8px; font-size: 0.9rem; line-height: 1.55; color: var(--c-soft); }
.bp-howto p:last-child { margin-bottom: 0; }
.bp-howto strong { color: var(--c-gold); }

.bp-gameinfo p { line-height: 1.5; color: var(--c-soft); }
/* Every one of these is a LIVE value read off getGameSettings — gold, like
   every other number on this page that came from the chain. */
.bp-gameinfo p > span,
.bp-gameinfo .bp-info-list li { color: var(--c-gold); font-weight: bold; }

.bp-info-label { margin: 8px 0 4px; }
.bp-gameinfo .bp-info-label:first-of-type { margin-top: 0; }
.bp-info-list {
    list-style: none;
    margin: 0 0 6px;
    padding: 0 0 0 6px;
    font-size: 0.95rem;
    line-height: 1.6;
}
.bp-info-list li { overflow-wrap: anywhere; }

.bp-verify-head {
    margin: 14px 0 6px;
    padding-top: 12px;
    border-top: 1px dashed rgba(255, 255, 255, 0.2);
    color: var(--c-gold);
    font-weight: bold;
}
.bp-verify p { font-size: 0.9rem; }
.bp-verify-note { color: var(--c-dim); font-size: 0.9rem; }
.bp-verify-note strong { color: var(--c-gold); }
.contract-link { color: var(--c-gold, #ffcf48); font-weight: bold; text-decoration: none; overflow-wrap: anywhere; }
.contract-link:hover { color: orange; text-decoration: underline; }

/* ---- a11y helpers ---- */
.sr-only {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
}
a:focus-visible, button:focus-visible, input:focus-visible {
    outline: 3px solid var(--c-gold);
    outline-offset: 2px;
}
.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    background: #000;
    color: var(--c-gold);
    padding: 10px 16px;
    z-index: 2000;
    border-radius: 0 0 8px 0;
}
.skip-link:focus { left: 0; }

/* ---- Footer — same mechanism as every other page ---- */
.footer {
    width: 100%;
    text-align: center;
    padding: 10px 0;
    background: rgba(0, 0, 0, 0.8);
    color: white;
    position: relative;
    font-size: 14px;
    margin-top: 20px;
    align-self: stretch;
}
.trust-strip {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: baseline;
    gap: 4px 12px;
    margin-top: 6px;
    padding: 0 12px;
    font-size: 12px;
    color: #ded6c6; /* opaque: translucent text over a translucent panel has no contrast ratio (READABILITY rule) */
}
.trust-strip a { color: var(--c-gold); text-decoration: none; }
.trust-strip a:hover, .trust-strip a:focus-visible { color: var(--c-orange); text-decoration: underline; }
.trust-sep { opacity: 0.5; }

/* ---- "how it works" info button + modal — copied from blazedice.css ---- */
.viewer-info-btn {
    display: inline-block;
    font-family: inherit;
    font-size: 14px;
    margin: 0 auto 16px;
    padding: 6px 14px;
    min-height: var(--touch);
    border-radius: 999px;
    border: 1px solid rgba(255, 207, 72, .6);
    background: rgba(0, 0, 0, .45);
    color: var(--c-gold);
    cursor: pointer;
    transition: background-color .2s ease, border-color .2s ease;
}

.viewer-info-btn:focus-visible { outline: 3px solid var(--c-gold); outline-offset: 2px; }

.viewer-modal {
    position: fixed;
    inset: 0;
    z-index: 1000;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
}


.viewer-modal-card {
    position: relative;
    width: 100%;
    max-width: 520px;
    max-height: calc(100vh - 40px);
    overflow-y: auto;
    padding: 26px 24px 24px;
    border-radius: 16px;
    border: 2px solid #ff7a18;
    background: rgba(10, 6, 2, .97);
    color: #fff;
    box-shadow: 0 20px 60px -12px rgba(0, 0, 0, .8), var(--glow);
    animation: bpModalIn .22s ease;
    text-align: center;
}
@keyframes bpModalIn {
    from { opacity: 0; transform: translateY(10px) scale(.98); }
    to   { opacity: 1; transform: none; }
}


.viewer-modal-close:focus-visible { outline: 3px solid var(--c-gold); outline-offset: 2px; }

#bpInfoTitle {
    margin: 0 0 4px;
    font-size: 20px;
    color: var(--c-gold);
    text-shadow: 0 0 6px #ff8c00, 0 0 10px #ff4500;
    text-wrap: balance;
}


.viewer-rap-mode { margin: 0 0 6px; font-weight: bold; color: var(--c-gold); font-size: 15px; }
.viewer-rap { margin: 0 0 8px; font-size: 14px; line-height: 1.7; color: #fff; }
.viewer-rap:last-child { margin-bottom: 0; }
.viewer-rap code { color: var(--c-gold); }


/* ---- The RESULT modal ----
   Built by blazeplinko.js through the shared BlazeModal shell
   (viewer-modal.js), which reuses the .viewer-modal-* classes above. The
   heading BlazeModal creates has no id of ours to hang styling on, so it is
   targeted structurally. */
.viewer-modal-card > h2 {
    margin: 0 0 10px;
    font-size: 22px;
    color: var(--c-gold);
    text-shadow: 0 0 6px #ff8c00, 0 0 10px #ff4500;
    text-wrap: balance;
}
.viewer-modal-body { text-align: center; }

/* The path replay thumbnail: 12 chevrons, one per row, exactly the bounces the
   contract computed. Static — the modal opens after the ball has landed. */
.bp-modal-path {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
    margin: 6px auto 12px;
    max-width: 340px;
}
.bp-modal-step {
    width: 22px;
    height: 22px;
    line-height: 22px;
    border-radius: 6px;
    font-size: 12px;
    font-weight: bold;
    background: rgba(255, 207, 72, 0.14);
    border: 1px solid rgba(255, 207, 72, 0.5);
    color: var(--c-gold);
}
.bp-modal-step.right { background: rgba(255, 69, 0, 0.2); border-color: rgba(255, 122, 24, 0.7); }

.bp-modal-bucket {
    display: inline-block;
    margin: 0 auto 10px;
    padding: 8px 18px;
    border-radius: 12px;
    font-size: 1.6rem;
    font-weight: bold;
    color: #1a0d00;
    background: var(--c-gold);
    box-shadow: 0 0 20px rgba(255, 122, 24, 0.6);
}
.bp-modal-bucket.loss { background: #6fb7d6; color: #06222e; }

.bp-modal-headline { margin: 0 0 10px; font-size: 1.15rem; font-weight: bold; }
.bp-modal-headline.win { color: #b8ffcf; }
.bp-modal-headline.loss { color: #ffd9a8; }

/* The degen jackpot headline variant — the 1/4096 edge hit. Reuses the same
   pulse keyframe the jackpot bucket gets so the modal and the board that sent
   you here read as one moment. */
.bp-modal-headline.jackpot {
    color: var(--c-gold);
    text-shadow: 0 0 10px #ff8c00, 0 0 18px #ff4500, 0 0 30px rgba(255, 207, 72, 0.9);
    animation: bpJackpotPulse 1s ease-in-out infinite;
}

.bp-modal-quote { margin: 0 0 16px; font-size: 0.95rem; font-style: italic; color: var(--c-soft); }

/* The receipt. A definition list because that is what it is: labelled values. */
.bp-modal-rows {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 6px 14px;
    margin: 0 0 16px;
    text-align: left;
    font-size: 0.95rem;
}
.bp-modal-rows dt { color: var(--c-dim); }
.bp-modal-rows dd { margin: 0; color: var(--c-gold); font-weight: bold; overflow-wrap: anywhere; }

.bp-modal-tx { margin: 0 0 16px; font-size: 0.9rem; }
.bp-modal-tx a { color: var(--c-gold); }

/* ---- Result effects: money rain (win) and smoke (sub-1x) ----
   Particles are emoji text nodes; all motion is here, in keyframes. The layer
   is fixed to the viewport, never takes a pointer event, and sits BELOW the
   modal's z-index 1000 on purpose — the effect plays around the result, never
   over the number the player is trying to read. */
.bp-fx-layer {
    position: fixed;
    inset: 0;
    z-index: 900;
    overflow: hidden;
    pointer-events: none;
}
.bp-fx {
    position: absolute;
    top: 0;
    font-size: var(--bp-fx-size, 26px);
    line-height: 1;
    will-change: transform, opacity;
    user-select: none;
}
.bp-fx-rain { animation: bpFxRain var(--bp-fx-dur, 2.4s) linear var(--bp-fx-delay, 0s) 1 both; }
@keyframes bpFxRain {
    0%   { transform: translate3d(0, -14vh, 0) rotate(0deg); opacity: 0; }
    8%   { opacity: 1; }
    90%  { opacity: 1; }
    100% { transform: translate3d(var(--bp-fx-drift, 0), 106vh, 0) rotate(var(--bp-fx-spin, 360deg)); opacity: 0; }
}
.bp-fx-smoke { animation: bpFxSmoke var(--bp-fx-dur, 2.2s) ease-out var(--bp-fx-delay, 0s) 1 both; }
@keyframes bpFxSmoke {
    0%   { transform: translate3d(0, 0, 0) scale(0.55); opacity: 0; }
    18%  { opacity: 0.9; }
    100% { transform: translate3d(var(--bp-fx-drift, 0), -230px, 0) scale(2.1); opacity: 0; }
}
/* Heavier/bigger drops for the jackpot money-rain burst — same keyframe,
   bigger and glowing so the rain itself reads as more excessive. */
.bp-fx-rain.bp-fx-big {
    font-size: calc(var(--bp-fx-size, 26px) * 1.5);
    filter: drop-shadow(0 0 6px rgba(255, 207, 72, 0.85));
}

/* ============================================================
   SHOWPIECE BOARD VISUALS — owner directive 2026-08-04.
   ("cool UI, animation, graphics" — the board is the page's identity.)

   This block is the CSS half of the contract; blazeplinko.js (Task 4) drives
   all of it by toggling classes/attributes ONLY — no inline styles beyond the
   --bp-off/--bp-row/--bp-step-ms/--bp-fx-* custom properties already
   documented above. Every animation here touches only transform (translate /
   rotate / scale as independent properties), opacity, box-shadow or filter —
   never layout-affecting properties — so it all stays GPU-composited.

   Hooks blazeplinko.js is expected to use:
     #bpBoard[data-dropping="true"]   suspends idle shimmer/wobble while a
                                       ball is in flight; removed on landing
     #bpBoard[data-idle="true"]       default state (ships in the HTML);
                                       pegs shimmer, ball wobbles at the drop
                                       point, inviting the drop
     .bp-peg-hit                      one-shot flash + ripple, added then
                                       removed after animationend each time
                                       the ball crosses a peg — reads as a
                                       pinball light-chase down the path
     #bpBall[data-bounce="true"]      one-shot squash-stretch. Toggle off,
                                       force reflow, toggle back on so the
                                       animation restarts on every row
     .bp-ball-trail-node (x3-4)       sibling spans placed inside #bpBoard
                                       (same containing block as #bpBall),
                                       each given its own lagged --bp-off /
                                       --bp-row so they read as an afterimage
                                       trail; opacity/size scale by :nth-child
     .bp-bucket-erupt                 one-shot eruption on the landing
                                       bucket; swap for the persistent
                                       .bp-bucket-hit once animationend fires
     .bp-bucket--jackpot on the
       landing bucket, .bp-fx-jackpot
       on #bpFxLayer                  the 1-in-4096 edge-hit special
     .bp-fx-rain.bp-fx-big            bigger/glowier rain for the jackpot
                                       money-rain burst (use more of them too)
     .bp-modal-headline.jackpot       degen-mode result modal headline

   Every effect above has a reduced-motion off-switch at the bottom of this
   file: instant bucket highlight + result, no flight, no shimmer, no
   eruption. Non-negotiable per the directive.
   ============================================================ */

/* ---- Idle liveliness: ball wobble + peg shimmer ---- */
.bp-ball[data-state="idle"] {
    animation: bpBallIdle 2.6s ease-in-out infinite;
}
@keyframes bpBallIdle {
    0%, 100% { rotate: -6deg; scale: 1; }
    50%      { rotate: 6deg; scale: 1.05; }
}

/* A soft highlight sweeps across each peg on its own offset loop, staggered
   by row so it reads as a slow ripple rather than a synchronized blink. Lives
   on ::before so it never competes with .bp-peg-hit's own filter/box-shadow. */
.bp-peg::before {
    content: "";
    position: absolute;
    inset: -3px;
    border-radius: 50%;
    background: radial-gradient(circle, rgba(255, 255, 255, 0.65), rgba(255, 255, 255, 0) 70%);
    opacity: 0;
    animation: bpPegShimmer 3.4s ease-in-out infinite;
}
.bp-peg-row:nth-child(2n) .bp-peg::before   { animation-delay: -1.1s; }
.bp-peg-row:nth-child(3n) .bp-peg::before   { animation-delay: -2.3s; }
.bp-peg-row:nth-child(5n) .bp-peg::before   { animation-delay: -0.6s; }
@keyframes bpPegShimmer {
    0%, 86%, 100% { opacity: 0; }
    93%           { opacity: 0.85; }
}
/* Mid-flight the board goes quiet — the light-chase (below) is the only peg
   animation while a ball is live. */
.bp-board[data-dropping="true"] .bp-peg::before { animation-play-state: paused; opacity: 0; }

/* ---- Peg light-chase: flash + ripple as the ball strikes ---- */
.bp-peg-hit {
    animation: bpPegHit 420ms ease-out;
}
@keyframes bpPegHit {
    0%   { filter: brightness(1); box-shadow: 0 0 6px rgba(255, 122, 24, 0.6); }
    30%  { filter: brightness(2.3); box-shadow: 0 0 18px rgba(255, 207, 72, 1), 0 0 30px rgba(255, 69, 0, 0.85); }
    100% { filter: brightness(1); box-shadow: 0 0 6px rgba(255, 122, 24, 0.6); }
}
.bp-peg-hit::after {
    content: "";
    position: absolute;
    inset: -6px;
    border-radius: 50%;
    border: 2px solid rgba(255, 207, 72, 0.9);
    animation: bpPegRipple 420ms ease-out;
    pointer-events: none;
}
@keyframes bpPegRipple {
    0%   { transform: scale(0.4); opacity: 0.9; }
    100% { transform: scale(2.4); opacity: 0; }
}

/* ---- Ball afterimage trail ----
   Diminishing size + opacity per node so it reads as motion blur behind the
   ember, not four extra balls. Uses the exact same position formula as
   #bpBall so a lagged --bp-off/--bp-row from JS is all it takes to trail. */
.bp-ball-trail-node {
    position: absolute;
    top: 0;
    left: 50%;
    border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, rgba(255, 243, 196, 0.55) 0%, rgba(255, 179, 71, 0.4) 45%, rgba(255, 69, 0, 0.3) 100%);
    transform:
        translateX(calc(-50% + var(--bp-pitch) * var(--bp-off, 0) / 2))
        translateY(calc(var(--bp-row-pitch) * var(--bp-row, 0)));
    /* Same easing as #bpBall (see its comment) — the trail must match the
       ball's own curve exactly, or the afterimage visibly detaches from the
       thing it's supposed to be a blur behind. */
    transition: transform var(--bp-step-ms, 110ms) cubic-bezier(0.32, 0, 0.67, 0), opacity 0.15s ease-out;
    pointer-events: none;
    z-index: 1;
    /* Hidden at rest — Task 4 (blazeplinko.js) prepends four of these as
       #bpBoard's first children and only moves them during a live flight; the
       nth-child opacities below only apply mid-drop (see the [data-dropping]
       rules), so an idle board never shows four faint circles stacked under
       the ball at the drop point. */
    opacity: 0;
}
.bp-ball-trail-node:nth-child(1) { width: calc(var(--bp-ball) * 0.88); height: calc(var(--bp-ball) * 0.88); }
.bp-ball-trail-node:nth-child(2) { width: calc(var(--bp-ball) * 0.74); height: calc(var(--bp-ball) * 0.74); }
.bp-ball-trail-node:nth-child(3) { width: calc(var(--bp-ball) * 0.6);  height: calc(var(--bp-ball) * 0.6); }
.bp-ball-trail-node:nth-child(4) { width: calc(var(--bp-ball) * 0.46); height: calc(var(--bp-ball) * 0.46); }
#bpBoard[data-dropping="true"] .bp-ball-trail-node:nth-child(1) { opacity: 0.32; }
#bpBoard[data-dropping="true"] .bp-ball-trail-node:nth-child(2) { opacity: 0.2; }
#bpBoard[data-dropping="true"] .bp-ball-trail-node:nth-child(3) { opacity: 0.11; }
#bpBoard[data-dropping="true"] .bp-ball-trail-node:nth-child(4) { opacity: 0.05; }

/* ---- Bounce squash-stretch ----
   Toggled on #bpBall alongside data-state="falling" every time it lands on a
   peg. `scale` is the independent property, layered over the JS-owned
   translateX/translateY `transform` above — never fights it.

   --bp-squash-x/--bp-squash-y are written by bounceBall() (blazeplinko.js)
   every row, growing slightly stronger on later, faster rows — a harder
   strike as the chip accelerates — and default to the base values
   (SQUASH_X_MIN/SQUASH_Y_MIN) for any caller that doesn't pass a row.

   RETUNED, 2026-08-07 (owner: "the ball bounces too much and gets off the
   rails" / "it doesn't look natural as a drop" — a real peg strike is a
   small, crisp, DAMPED deflection, not a big elastic bounce). Two changes
   from the first pass: the amplitude dropped (was 1.30/0.72 -> 1.45/0.64,
   now 1.15/0.85 -> 1.25/0.78 — see blazeplinko.js's SQUASH_X_MIN/MAX), and
   the old 70% keyframe that overshot BACK PAST normal (0.92 1.1 — taller
   and narrower than resting size) is gone entirely. A rebound past neutral
   is what reads as "elastic"/springy; a single squash-and-recover, damped
   to a stop, reads as a real deflection. Bound to stay inside the peg gap
   at both breakpoints — see "the squash never renders outside the peg gap"
   in tests/blazeplinko.spec.js. */
#bpBall[data-bounce="true"] {
    animation: bpBallSquash 180ms ease-out;
}
@keyframes bpBallSquash {
    0%   { scale: 1 1; }
    35%  { scale: var(--bp-squash-x, 1.15) var(--bp-squash-y, 0.85); }
    100% { scale: 1 1; }
}

/* ---- Bucket eruption ----
   A one-shot flash + brief shake that ends on the exact transform/box-shadow
   values .bp-bucket-hit already holds statically, so JS can hand off to
   .bp-bucket-hit once animationend fires and the glow persists cleanly. */
.bp-bucket-erupt {
    animation: bpBucketErupt 620ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes bpBucketErupt {
    0%   { transform: translateY(0) scale(1); box-shadow: 0 0 0 rgba(0, 0, 0, 0); }
    22%  { transform: translateY(-6px) scale(1.24) rotate(-2deg); box-shadow: 0 0 20px rgba(255, 207, 72, 1), 0 0 34px rgba(255, 69, 0, 0.85); }
    45%  { transform: translateY(2px) scale(0.95) rotate(2deg); }
    70%  { transform: translateY(6px) scale(1.18) rotate(-1deg); }
    100% { transform: translateY(4px) scale(1.14) rotate(0deg); box-shadow: 0 0 16px rgba(255, 207, 72, 0.95), 0 0 28px rgba(255, 69, 0, 0.7); }
}

/* ---- The jackpot: the 80×/edge 1-in-4096 special ---- */
.bp-fx-layer.bp-fx-jackpot::before {
    content: "";
    position: fixed;
    inset: 0;
    background: radial-gradient(ellipse at center, rgba(255, 207, 72, 0) 50%, rgba(255, 122, 24, 0.6) 100%);
    animation: bpJackpotFlash 900ms ease-out;
}
@keyframes bpJackpotFlash {
    0%   { opacity: 0; }
    16%  { opacity: 1; }
    100% { opacity: 0; }
}
.bp-bucket--jackpot {
    animation: bpBucketErupt 620ms cubic-bezier(0.34, 1.56, 0.64, 1), bpJackpotPulse 1.1s ease-in-out 620ms infinite;
}
@keyframes bpJackpotPulse {
    0%, 100% { box-shadow: 0 0 16px rgba(255, 207, 72, 0.95), 0 0 28px rgba(255, 69, 0, 0.7); }
    50%      { box-shadow: 0 0 30px rgba(255, 207, 72, 1), 0 0 56px rgba(255, 69, 0, 1); }
}

/* ---- Motion + small screens ---- */
@media (prefers-reduced-motion: reduce) {
    /* No travel: the ball is placed in its final bucket instantly.
       blazeplinko.js also refuses to run the per-row animation at all when
       this query matches, so nothing ever tweens. */
    .bp-ball { transition: none; }
    .bp-bucket { transition: none; }
    /* No rain, no smoke. blazeplinko.js also refuses to spawn a single
       particle when this query matches, so the layer stays empty rather than
       hidden. */
    .bp-fx-layer { display: none; }
    .bp-fx { animation: none; }
    .viewer-modal-card { animation: none; }
    .wallet-btn, .bp-pick, .bp-chip, .bp-budget-chip { transition: none; }

    /* Showpiece board visuals: every non-negotiable off-switch. Instant
       bucket highlight + result, no flight, no shimmer, no eruption. */
    .bp-ball[data-state="idle"] { animation: none; rotate: 0deg; scale: 1; }
    .bp-peg::before { animation: none; opacity: 0; }
    .bp-peg-hit { animation: none; }
    .bp-peg-hit::after { animation: none; content: none; }
    #bpBall[data-bounce="true"] { animation: none; scale: 1 1; }
    .bp-ball-trail-node { display: none; }
    .bp-bucket-erupt { animation: none; }
    .bp-bucket--jackpot { animation: none; }
    .bp-fx-layer.bp-fx-jackpot::before { display: none; }
    .bp-modal-headline.jackpot { animation: none; }
}

/* Below ~1200px three columns stop fitting: the spine needs real width for the
   board. So the spine goes full width FIRST and the two reference columns pair
   up underneath it. */
@media screen and (max-width: 1199px) {
    .container { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
    .game-panel { grid-column: 1 / -1; order: -1; }
    .left-panel { order: 0; }
    .right-panel { order: 1; }
}

@media screen and (max-width: 768px) {
    /* A tighter board on a phone. Every board dimension derives from these
       four, so the pegs, the buckets and the ball all shrink together and the
       animation needs no phone-specific arithmetic. */
    :root {
        --bp-peg: 7px;
        --bp-pitch: 25px;
        --bp-row-pitch: 22px;
        /* Same ~25% reduction as the desktop --bp-ball, scaled to the
           phone pitch — still well clear of the 25px (and 22px at the
           390px breakpoint below) peg pitch. */
        --bp-ball: 11px;
    }
    .bp-bucket { font-size: 0.52rem; padding: 5px 0; }

    /* One column, and the SPINE IS FIRST — the board and the action button
       have to be the first thing a thumb reaches. Rules second, numbers
       third. */
    .container { grid-template-columns: minmax(0, 1fr); padding: 1rem; gap: 16px; }
    .game-panel { grid-column: 1; order: -1; padding: 18px; }
    .info-panel { padding: 18px; }

    .viewer-modal-card { padding: 22px 16px 18px; }
    .viewer-modal-done { width: 100%; }
    .bp-modal-step { width: 18px; height: 18px; line-height: 18px; font-size: 11px; }
}

@media screen and (max-width: 380px) {
    /* The narrowest phones. 13 buckets across ~330px of panel is the binding
       constraint, not the pegs. */
    :root { --bp-pitch: 22px; --bp-row-pitch: 20px; }
}

/* 🔁 Replay drop (2026-08-14). Same pill as the crash page's Replay flight, so
   the two games teach the same affordance rather than each inventing one. */
.bp-replay-btn {
    display: block;
    /* The result banner directly below has margin-top:0, so a 0 bottom margin
       left the pill sitting flush on its border — it read as overlapping.
       12px matches the readout gap above it, so the button sits evenly between
       the two rather than clinging to one. */
    margin: 6px auto 12px;
    padding: 5px 14px;
    font-family: inherit;
    font-size: 0.85rem;
    color: var(--c-gold);
    background: rgba(255, 207, 72, 0.10);
    border: 1px solid rgba(255, 207, 72, 0.55);
    border-radius: 999px;
    cursor: pointer;
    transition: background 0.15s, transform 0.15s;
}
.bp-replay-btn:hover:not(:disabled) { background: rgba(255, 207, 72, 0.22); transform: translateY(-1px); }
.bp-replay-btn:disabled { opacity: 0.45; cursor: default; }
/* [hidden] is set by JS when there is nothing to replay, but the UA rule it
   relies on (display:none) LOSES to the display above — so the button stayed
   visible on every page load and, on the wheel, clickable. Reported live
   2026-08-15. Class+attribute outranks the bare class, so this settles it. */
.bp-replay-btn[hidden] { display: none; }

/* ============================ HUD READOUTS ============================
   Owner request, 2026-08-14: "too much information ... doesn't look like the
   UI of a game."

   The diagnosis was hierarchy, not volume. Every readout was
   `Label: value UNIT` at ONE weight and ONE colour, so "House float" and
   "1,309,996" competed as equals and the panel read as a spec sheet. A game HUD
   does the opposite: the label recedes to a caption, the NUMBER is the thing.

   So: labels shrink, uppercase and dim; the value — which is always the
   element carrying the id — goes big, bold and gold with tabular figures so it
   cannot jitter as it repolls. Row rules give the eye a grid to run down.

   CSS ONLY, and deliberately so. Every value already sits in a span[id] that
   the page scripts write into and the tests assert on by exact text, so
   restyling without touching markup means no id moves, no unit sneaks inside a
   value, and nothing to re-verify but the look.

   `:has(span[id])` is what keeps it honest — it selects only rows that actually
   carry a readout. Prose paragraphs in the same panels (the Jackpot Winners
   taunt, the "not audited" note) are left completely alone; uppercasing those
   would have been the obvious way to wreck this. */
.stats p:has(span[id]) {
    display: block;
    margin: 0;
    padding: 6px 0;
    font-size: 12px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* OPAQUE, and 7:1 on the panel. The first version used
       rgba(255,255,255,.42) and the dice page's readability guard caught it
       immediately: translucent text over a translucent panel over a photographic
       background has no fixed contrast ratio at all — it is whatever pixel
       happens to sit behind it. --c-dim is the token that exists for this exact
       job (14.5:1 here). The label still recedes, by SIZE, CASE and TRACKING
       rather than by fading out. */
    color: var(--c-dim, #ded6c6);
    border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.stats p:has(span[id]):last-child { border-bottom: none; }

.stats p:has(span[id]) span[id] {
    display: inline-block;
    margin-left: 2px;
    font-size: 15px;
    font-weight: bold;
    letter-spacing: 0;
    text-transform: none;
    color: var(--c-gold, #ffcf48);
    font-variant-numeric: tabular-nums;
}

/* Panel titles are h2 since 2026-09-13 (as h3 straight under the page's h1 they
   skipped a level in the screen-reader outline). This zero-specificity rule
   restores the browser's h3 metrics they were designed at, so the retag changes
   the outline and not the look; any real panel rule above still wins. */
:where(.stats) h2 { font-size: 1.17em; margin-block: 1em; }
