/* =====================================================================
   ARCADE CABINET — the reference-image redesign
   =====================================================================
   Everything that makes the game look like the reference lives in this
   one file, loaded last. That is deliberate: it is a look, and a look
   should be revertible by deleting one <link> rather than by unpicking
   changes scattered through twenty-four stylesheets.

   WHY THIS IS A LAYER AND NOT A REWRITE

   The game already has five themes driven by tokens, and a working
   theme switcher. Hard-coding the reference's navy-and-magenta would
   have looked right in one theme and broken the other four.

   So the reference's character is split in two:

     * STRUCTURE — the angular cabinet framing, the neon reel surround,
       the column dividers, the glowing control chrome. None of it is
       colour-specific, so it applies to every theme.

     * COLOUR — one new accent, --violet, defined per theme below. That
       is what carries the magenta in the reference without overriding
       the cyan chrome the rest of the game is built on.

   Nothing here changes markup or behaviour. Anything needing a DOM
   change is done in index.html and marked there.
   ===================================================================== */

/* ---------------------------------------------------------------------
   THE NEW ACCENT
   One per theme rather than one globally, so light mode gets a violet
   that works on a light panel instead of the same neon that only reads
   on black.
   --------------------------------------------------------------------- */
:root {
  --violet: #a855f7;
  --violet-mid: #c084fc;
  --violet-mid-rgb: 192, 132, 252;
  --violet-bright: #d8b4fe;
  --violet-glow: rgba(168, 85, 247, 0.38);
  /* The cabinet's own surfaces, derived so each theme keeps its depth. */
  --cab-edge: rgba(34, 227, 255, 0.55);
  --cab-corner: 18px;
}
html[data-theme="light"] {
  --violet: #7c3aed;
  --violet-mid: #8b5cf6;
  --violet-mid-rgb: 139, 92, 246;
  --violet-bright: #a78bfa;
  --violet-glow: rgba(124, 58, 237, 0.22);
  --cab-edge: rgba(8, 145, 168, 0.45);
}
html[data-theme="oled"],
html[data-theme="midnight"] {
  --cab-edge: rgba(34, 227, 255, 0.42);
}

/* ---------------------------------------------------------------------
   THE CABINET
   Angular corner brackets on the machine frame, which is the single
   strongest cue in the reference — it is what makes it read as a
   machine rather than as a web page with reels on it.

   Drawn with two pseudo-elements and border segments rather than eight
   positioned nodes: no markup, no extra layers to composite, and it
   scales with the frame instead of needing per-breakpoint numbers.
   --------------------------------------------------------------------- */
.machine-frame {
  border-width: 6px;
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.6),
    0 0 34px -6px var(--cyan-glow),
    inset 0 0 60px -20px var(--violet-glow);
}

/* On its own element: .machine-frame::after is already the rotating
   conic glow and ::before the backdrop, and .cabinet .machine-frame::after
   outranks a bare .machine-frame::after anyway. Taking either would have
   silently deleted an effect that was there on purpose. */
.cabinet-corners {
  content: '';
  position: absolute;
  inset: 10px;
  pointer-events: none;
  z-index: 4;
  border-radius: calc(var(--radius-lg) - 6px);
  /* Corner brackets: a transparent box whose four corners are painted
     by two crossed gradients, clipped to the corner regions. */
  background:
    linear-gradient(var(--cyan-bright), var(--cyan-bright)) left top,
    linear-gradient(var(--cyan-bright), var(--cyan-bright)) left top,
    linear-gradient(var(--violet-bright), var(--violet-bright)) right top,
    linear-gradient(var(--violet-bright), var(--violet-bright)) right top,
    linear-gradient(var(--violet-bright), var(--violet-bright)) left bottom,
    linear-gradient(var(--violet-bright), var(--violet-bright)) left bottom,
    linear-gradient(var(--cyan-bright), var(--cyan-bright)) right bottom,
    linear-gradient(var(--cyan-bright), var(--cyan-bright)) right bottom;
  background-repeat: no-repeat;
  background-size:
    var(--cab-corner) 2px, 2px var(--cab-corner),
    var(--cab-corner) 2px, 2px var(--cab-corner),
    var(--cab-corner) 2px, 2px var(--cab-corner),
    var(--cab-corner) 2px, 2px var(--cab-corner);
  opacity: 0.9;
}

/* ---------------------------------------------------------------------
   THE REEL SURROUND
   In the reference the reels sit in one lit box with thin dividers
   between the columns, rather than five separate bordered boxes. That
   reads as a single window into the machine, which is what a real
   cabinet looks like.
   --------------------------------------------------------------------- */
.reels {
  gap: 2px;
  padding: 6px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.55);
  box-shadow:
    inset 0 0 0 2px var(--cab-edge),
    inset 0 0 40px -12px var(--violet-glow);
}

.reel-col {
  border: none;
  border-radius: 4px;
  background: linear-gradient(180deg,
    rgba(255, 255, 255, 0.035), rgba(0, 0, 0, 0.25) 60%);
  /* The divider, as a border on one side only, so the outer edges stay
     flush against the surround rather than doubling its line. */
  border-right: 1px solid rgba(255, 255, 255, 0.09);
}
.reel-col:last-child { border-right: none; }

/* ---------------------------------------------------------------------
   CONTROL CHROME
   Angular, lit edges on the deck controls. Kept to border, background
   and box-shadow so nothing here costs a compositor layer — this sits
   under the reels, which are the thing that has to stay smooth.
   --------------------------------------------------------------------- */
.control-bar {
  border-radius: var(--radius-md);
  box-shadow:
    inset 0 0 0 1px var(--cab-edge),
    0 -10px 30px -22px var(--violet-glow);
}

.btn,
.meter,
.refill-pill,
.boosts-btn,
.shop-cta {
  border-radius: var(--radius-sm);
}

.btn:hover:not(:disabled),
.shop-cta:hover:not(:disabled) {
  box-shadow: 0 0 14px -2px var(--violet-glow);
}

/* The two currency plates get the cabinet treatment: they are the
   numbers the reference puts in lit housings down each side, and they
   are the numbers a player reads most. */
.hud-stat {
  border-radius: var(--radius-sm);
  box-shadow: inset 0 0 0 1px var(--cab-edge);
}

@media (prefers-reduced-motion: reduce) {
  .cabinet-corners { opacity: 0.7; }
}


/* ---------------------------------------------------------------------
   THE CHARACTER, BEHIND THE BET BOX
   She stands on the bet multiplier: her feet stop exactly on its top
   border so she reads as standing BEHIND the control rather than
   floating above it. That is the whole illusion, and it only works if
   the two elements share an anchor — hence position:relative on the
   meter and bottom:100% on her, which pins her base to its top edge
   whatever height she ends up.

   DESKTOP ONLY. She is display:none on phones in three separate rules
   and stays that way: her art is 1.6MB, a visible gif decodes every
   frame forever, and the phone is the device that was overheating.
   --------------------------------------------------------------------- */
@media (min-width: 900px) {
  .control-bar .meter-stake { position: relative; overflow: visible; }
}

/* The deck used to drop the mascot when it ran short of room, from back
   when she sat IN the control bar's flow and competed with the meters
   for width. She is absolutely positioned now and takes no flow space at
   all, so that rule would hide her for no reason. Re-shown only at the
   width the illusion needs; below it the earlier rules still hide her
   and the phone stays cool. */
@media (min-width: 900px) {
  /* One token, two consumers: her width above and the band reserved for
     her below. Set on .slot-machine because a custom property on the
     widget itself is not visible to .machine-stage. */
}

/* Standing room. She is anchored to the TOP border of the bet box, so
   without a reserved band she stands up into the reels — and because the
   frame is narrower than the stage (aspect-ratio 5/3, height-driven) its
   bottom-left corner lands on top of her. Measured clear air was only
   31px at 1280x720 against a 70px figure.

   This spends that air plus a little of the frame's height. The cost is
   real and worth stating: every 1px reserved costs 1.67px of frame width,
   about 10% at these sizes. A character who is half-drawn is worse than a
   slightly smaller machine.

   Its own breakpoints, matching the display rule above — a bare
   .machine-stage rule here would beat every responsive override in the
   codebase, which this file has done six times already. */
/* A BARE `@media` WITH NO CONDITION USED TO SIT ON THE NEXT LINE.

   Left behind when this rule's body was removed — the same cleanup that
   left the two empty `@media (min-width: 900px)` blocks above. A media
   at-rule with no query is invalid, and the browser does not skip just
   that token: it consumes what follows as the prelude, resolves the
   whole thing to `not all`, and DISCARDS THE ENTIRE NEXT BLOCK. So the
   `@media (max-height: 620px)` below was dead, silently, and adding
   rules to it did nothing — which is how it was found.

   Below 620px tall the base sheet hides the mascot, and it is right to:
   the frame is only ~195px tall there and a standing figure would eat
   it. That rule is (0,1,0) and the display rule above is (0,3,0), so
   without this she was shown on screens she was meant to be hidden on —
   measured display:block at 1200x460 — with no band reserved for her. */
/* =====================================================================
   SHORT VIEWPORTS — AND A PHONE IN LANDSCAPE, WHERE THE REELS VANISH
   ---------------------------------------------------------------------
   DELIBERATELY EMPTY, but no longer DEAD. See the note above: the bare
   `@media` that used to precede this block made it unparseable, so
   anything put here silently did nothing. That is now fixed, and this is
   a real block again.

   WHAT BELONGS HERE, AND WHY IT IS NOT HERE YET.

   At 667x375 — a phone turned sideways — THE REELS DO NOT RENDER.
   Measured: .machine-stage 0px tall, .machine-frame 8px, .reel-col 0px,
   zero symbols on screen. The player gets the HUD, the plaque, the
   jackpot and the deck, with a blank strip where the machine should be.

   Three causes stack:

     1. .machine-stage sizes itself with `flex: 1 1 auto`, but its parent
        .cabinet is `display: grid` (win-castle-account.css:704). A grid
        item ignores flex, so the stage has no growth rule at all and
        falls back to its content — a height-driven aspect-ratio box,
        i.e. zero.
     2. The shell is locked to one screen: html and body are
        `height: 100%; overflow: hidden` (win-castle-account.css) and
        .slot-machine is `grid-template-rows: auto 1fr auto` at
        min-height 100svh. At 375px tall the HUD and the deck take
        everything and the 1fr middle row is left with about 120px.
     3. base-machine.css DOES release the scroll lock — but under
        `@media (max-width: 768px)`. A landscape phone is 667 WIDE and
        375 TALL, so that query matches on the wrong axis for this case,
        and win-castle-account.css's unconditional `overflow: hidden`
        beats it regardless.

   A min-height on the stage is NOT the fix, and was tried: the reels
   appear, but the cabinet row does not grow to hold them, so they render
   straight over the control bar. Visible-but-overlapping is not better
   than absent, so it was reverted rather than shipped.

   The real fix is a landscape LAYOUT — a shorter HUD, a shorter deck and
   the machine taking the remainder, or a two-column arrangement with the
   deck beside the reels rather than under them. That is a design pass,
   not a patch, which is why this block is still empty.
   ===================================================================== */
@media (max-height: 620px) {
}


/* On a phone the readouts cannot share a line with the sound and theme
   controls: two balance plates plus those is ~400px of content in a
   375px viewport, and the overflow was clipped rather than scrolled, so
   the coin figure was simply cut off the side of the screen.
   They take a row of their own instead. */
@media (max-width: 720px) {
  /* flex: 0 0 auto is why this overflowed. The cluster refused to
     shrink, so adding the readouts to it pushed it to 399px inside a
     359px parent and the excess was clipped off the side of the screen
     rather than wrapping. Given a full row of its own it is bounded by
     the parent and wraps internally instead. */
  .top-controls-right {
    flex: 1 1 100%;
    flex-wrap: wrap;
    justify-content: flex-end;
    row-gap: 6px;
    min-width: 0;
    max-width: 100%;
  }
}

/* =====================================================================
   THE DECK, IN ROWS
   ---------------------------------------------------------------------
   The old deck was a three-column grid holding five children, which is
   what put the refill pill sixty pixels off the left edge of a phone and
   the HOLD button entirely off the right. Rows that wrap cannot produce
   that failure: there is no fixed track count for content to overflow.
   ===================================================================== */
/* section.control-bar, not .control-bar: an earlier sheet sets grid with
   an element+class selector, which outranks a bare class however late it
   comes. The same trap this deck sprang once already — the rows laid out
   side by side instead of stacked because the grid was still winning. */
section.control-bar {
  display: flex;
  flex-direction: column;
  /* nowrap is load-bearing. An earlier sheet sets flex-wrap:wrap for when
     this was a ROW; on a column, wrap means "start a second column when
     you run out of height", which is exactly what it did — readouts and
     controls in one column, footer and socials in another beside them. */
  flex-wrap: nowrap;
  gap: 10px;
  align-items: stretch;
  grid-template-columns: none;
}


/* Row 2 — things you PRESS. SPIN is centre and largest because it is
   pressed more than everything else on this screen put together. */
.cabinet-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  min-width: 0;
  /* The mascot stands on the bet box and is absolutely positioned above
     it, so this row must not clip her. */
  overflow: visible;
}
.cabinet-controls > * { flex: 0 1 auto; min-width: 0; }
.cabinet-controls .meter-stake { flex: 1 1 auto; max-width: 260px; }
.cabinet-controls .spin-btn {
  flex: 0 0 auto;
  width: clamp(96px, 15vw, 168px);
  order: 0;
}
.cabinet-controls .btn { flex: 0 0 auto; min-width: 68px; }


/* The socials are the least important thing here and go first when the
   deck runs short. */
.control-bar .deck-socials { justify-content: center; }

@media (max-width: 720px) {
  .control-bar { gap: 7px; }
  .cabinet-controls { gap: 8px; flex-wrap: wrap; }
  .cabinet-controls .meter-stake { flex: 1 1 100%; max-width: none; order: -1; }
  .cabinet-controls .spin-btn { width: clamp(88px, 26vw, 130px); }
  .cabinet-controls .btn { min-width: 56px; font-size: 0.56rem; }
  .control-bar .deck-socials { display: none; }
}

/* SPIN centred, WITHOUT inflating a real button to do it.

   The first attempt gave the outermost item on each side `flex: 1 1 0`
   so the two flanks balanced. It centred SPIN to within 28px -- and blew
   STATS up to 488px, a slab nearly three times the width of SPIN itself.
   Balancing a row by stretching a button people actually read and press
   is paying for symmetry with the thing the symmetry was meant to serve.

   Natural widths instead, centred as a row. SPIN lands 51px right of the
   centre line on a 1440px screen -- 3% of the width, because the bet
   meter is wider than STATS -- which is not visible, whereas the slab
   very much was. Truly equal flanks would need the two ends locked to the
   same width, and the bet meter changes width with the digits in it. */
.cabinet-controls .meter-stake { flex: 0 1 auto; max-width: 260px; }
.cabinet-controls .btn.stats,
.cabinet-controls .btn.hold,
.cabinet-controls .btn.auto    { flex: 0 0 auto; }

@media (max-width: 720px) {
  /* Stacked on a phone: the bet stepper takes the full width above, and
     the four controls share the row beneath it, so SPIN stays centred
     without the meter fighting it for room. */
  .cabinet-controls .meter-stake { flex: 1 1 100%; }
  .cabinet-controls .btn.stats   { flex: 0 0 auto; }
}

/* The base sheet has `@media (max-width: 768px) { .btn { width: 100% } }`
   — correct when these buttons were a vertical stack in the rail, and
   ruinous now they are a horizontal row: every one took a full line, the
   controls row grew to 353px, and the footer was pushed to y=975 in an
   812px viewport where it could not be reached at all.

   That is the third time this layout pass has hit the same shape of bug:
   a rule written for an element's OLD container quietly misbehaving in
   its new one. Blanket selectors scoped to a parent are fine until
   something moves in. */
@media (max-width: 768px) {
  .cabinet-controls .btn { width: auto; }
}

/* And the row itself gets tighter on a phone, since it now has to hold
   four controls and the bet stepper without eating the reels. */
@media (max-width: 720px) {
  .cabinet-controls .btn { padding: 8px 10px; }
  .cabinet-controls .meter-stake { padding-block: 4px; }
}

/* Twenty-three pixels over on a 375x812 phone, which is a scrollbar's
   worth of "nothing scrolls" broken. Taken back out of the gaps and the
   padding rather than out of any control's tap target — the buttons are
   already at the small end and shrinking them further to win a row is
   the wrong trade. */
@media (max-width: 720px) {
  section.control-bar { gap: 5px; padding-block: 6px; }
  .cabinet-controls { gap: 6px; row-gap: 5px; }
}


/* THE FRAME HAS TO GIVE THE DECK ITS ROOM
   The deck went from one row to three, so it needs roughly 250px where
   it used to want 90. The machine frame is sized by a clamp against the
   viewport height and knows nothing about that, so it kept its old
   height and pushed the footer row — and the socials under it — off the
   bottom of the screen. Capped against the space actually left, rather
   than given another hand-tuned clamp that the next layout change would
   invalidate in the same way. */
/* .cabinet .machine-frame, not .machine-frame: the cabinet rule sets
   max-height:100% at element+class specificity and a bare class cannot
   beat it. Same trap as the deck grid and the control-bar span rule —
   this codebase leans on descendant selectors, so an override has to
   match their shape rather than just come later. */
/* Superseded by the MACHINE FIRST block at the end of this file, which
   caps width and height together from the same figure. Left as a note
   rather than a rule: capping height alone let the frame keep a width
   set by whatever was beside it, which is how it ended up 414px left of
   centre. */


/* The socials come out of the deck.
   The reference has no links row down there, the footer row now serves
   the "secondary things" job they were sharing, and the same four links
   are already in the landing page footer — so this removes a duplicate
   rather than a feature. It is also the last 32px between the footer row
   and the bottom of a 900px screen.

   Deleting this one rule puts them back. */
.control-bar .deck-socials { display: none; }


/* =====================================================================
   MACHINE FIRST — the reels are the biggest thing on screen, centred.

   Three separate faults were making the machine small and off-centre on
   a wide, short window:

   1. `.machine-stage` is `flex-direction: row`. Moving the WIN/LEVEL
      strip "under the reels" put it BESIDE them instead, as a second
      column, taking half the stage. That alone pushed the frame 414px
      left of centre and held it to 745px of a 1512px stage.
   2. The cabinet grid's rails were asymmetric — 9vw left against 12vw
      right — so even a full-width stage is not centred on the viewport.
   3. The frame was capped on height only, so its width was decided by
      whatever happened to sit next to it.

   `overflow: hidden` on html and body means none of this scrolled: the
   parts that did not fit were silently clipped away, which is how the
   PAYS/BAG/HELP/SETTINGS row disappeared entirely rather than merely
   ending up below the fold.
   ===================================================================== */

/* The deck must never be squeezed below the height of its own buttons.
   At 1280x720 the page grid handed the deck a 158px track for 171px of
   content, so the footer row overflowed its own parent and — with
   `overflow: hidden` above it — was clipped out of existence rather than
   merely pushed below the fold. That is why the PAYS/BAG/HELP/SETTINGS
   row was missing entirely on a short window.

   The measurement that found this is worth keeping: making the machine
   SMALLER did not help at all — the deck still got 143px — which ruled
   out "not enough room" and showed the deck was losing a track-sizing
   argument it should never have been in. An auto grid row can be
   compressed below its content; with fit-content this one cannot. */
section.control-bar {
  min-height: fit-content;
}

/* The page has to claim the whole screen before anything inside it can
   be sized against the screen. On a 375x812 phone `.slot-machine` was
   732px tall — 88px of the viewport simply unclaimed — so the reels got
   130px and their symbols rendered 65x37, squashed to under half their
   proper height. With the full height claimed they get 210px and 65x63,
   which is square, as the art is.

   `svh`, not `vh` or `dvh`, and the choice matters on a phone. `vh` on
   iOS Safari measures the viewport with the URL bar HIDDEN, so the page
   is taller than what you can actually see and — under the
   `overflow: hidden` above — the bottom row gets clipped away, which is
   the original bug. `dvh` tracks the bar as it slides and would relayout
   the reels mid-scroll, which is the flicker. `svh` is the smallest
   state: always visible, never moves. */
.slot-machine {
  min-height: 100vh;
  min-height: 100svh;
  grid-template-rows: auto 1fr auto;
}

/* Equal rails, so the middle column's centre IS the viewport's centre.
   The stock rails are 9vw left against 12vw right, which leaves the
   machine 30px off-centre even when it has the whole stage.

   Scoped to min-width: 901px, and that scoping is the entire point of
   this comment. Below 900px the cabinet is NOT three columns at all —
   win-castle-account.css:1104 collapses it to `grid-template-columns:
   100%` with the rails laid out horizontally. Written bare, this rule
   sat in the last-loaded sheet at equal specificity and beat that media
   query at every width, forcing 120px rails onto a 375px phone and
   crushing the reels to 107px wide with 15x79 symbols.

   Media queries add no specificity. A later bare rule silently wins over
   every responsive override in the codebase, so anything in this file
   that touches a property some breakpoint also sets has to carry its own
   breakpoint. Sixth instance of this shape of bug in this layout pass. */
@media (min-width: 901px) {
  /* `1fr` rails around an `auto` centre, NOT a vw fraction.
     The rails used to take a share of the WIDTH while the machine took
     its size from the HEIGHT — two axes that agree at exactly one
     viewport shape and leave a gap at every other. The rails now take
     whatever the machine does not, so there is nothing left over to be a
     gap, at any shape. */
  .cabinet {
    grid-template-columns:
      minmax(150px, 18vw)
      minmax(0, 1fr)
      minmax(150px, 18vw);
  }
}

/* Reels above their readout strip, not beside it. */
.cabinet .machine-stage {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 0.8vh, 12px);
}

/* THE MACHINE TAKES THE ROOM THAT IS THERE.

   This used to cap the frame against a hand-counted chrome budget:
   `max-height: calc(100vh - 360px)`. On a 1280x720 window the cabinet
   row was already 439px tall and the cap held the frame to 360 — the
   machine was small because of the guess, not because of the space. A
   budget written as one number for every screen is wrong on all but one
   of them, and it was wrong by 79px on the commonest laptop size.

   No budget now. The grid hands the cabinet whatever the top bar and
   deck leave, the frame fills it, and `aspect-ratio` sets the width from
   the height it actually got. 5/3 is the reel grid itself — five columns
   of three — so the symbols come out square with no dead margin, and
   nothing needs re-tuning when the chrome above or below changes. */
.cabinet .machine-frame {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
  width: auto;
  max-width: 100%;
  max-height: none;
  aspect-ratio: 5 / 3;
  margin-inline: auto;
}

/* The name sits ON the frame, as the reference has it, instead of on its
   own line above. As a flow item it cost the reels its own height plus a
   gap — about 40px, which at 720px tall is most of the difference
   between a machine that dominates the screen and one that does not. */
.machine-stage { position: relative; }
.machine-stage > .machine-plaque {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translate(-50%, -52%);
  z-index: 6;
  margin: 0;
  max-width: 80%;
}


/* =====================================================================
   THE REFERENCE LAYOUT

   The second reference image, structurally. Everything here is position
   and chrome; the reels, symbols, names and art stay this game's own.

     top bar    identity | energy, coins, gift | account, help, settings
     left rail  inventory, stats, paytable, boosts, special offer
     centre     name plaque over the reels
     right rail jackpot banner over the prize ladder
     deck       bet panel, HOLD, SPIN, AUTO SPIN, readouts

   Colours come from the theme tokens above, not from the reference's
   purple-and-gold, so all five themes still work.
   ===================================================================== */

/* --- TOP BAR ------------------------------------------------------- */
.top-controls {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: clamp(8px, 1.4vw, 22px);
  width: 100%;
  max-width: none;
  padding: 0 clamp(6px, 1vw, 16px);
}

/* WHO YOU ARE */
.player-card {
  /* The containing block for #levelNext, the next-level-rewards popover.
     It is `position: absolute; top: calc(100% + 6px)`, and its old anchor
     (.hud-level) was dissolved when this block moved up here — so 100%
     started resolving against main.slot-machine, which is 100dvh tall.
     The popover opened six pixels BELOW the bottom of the screen on every
     click and was then clipped by two overflow:hidden ancestors. It was
     never broken; it was being drawn off-screen.

     Safe as a bare rule: no @media anywhere in slots/css sets `position`
     on .player-card, so this overrides no breakpoint. */
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px 6px 6px;
  border: 1px solid var(--cab-edge);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(10, 20, 34, 0.72), rgba(6, 12, 22, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 2px 10px rgba(0, 0, 0, 0.35);
  min-width: 0;
}
.player-avatar {
  flex: 0 0 auto;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 2px solid var(--violet);
  background: radial-gradient(circle at 50% 30%, rgba(168, 85, 247, 0.35), rgba(0, 0, 0, 0.5));
  box-shadow: 0 0 12px var(--violet-glow), inset 0 0 10px rgba(0, 0, 0, 0.6);
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: 0;
  cursor: pointer;
}
.player-avatar:hover { filter: brightness(1.12); }
.player-avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }
.player-avatar-letter {
  font-family: var(--font-display, inherit);
  font-size: 1.35rem;
  font-weight: 800;
  color: var(--violet-bright);
  line-height: 1;
}
.player-meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.player-name {
  font-family: var(--font-display, inherit);
  font-size: 0.86rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  color: var(--text, #eaf6ff);
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 15ch;
}
.player-level {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  color: var(--gold, #ffd76a);
  text-transform: uppercase;
  white-space: nowrap;
}

/* The level bar and its figures, now in the player card. Sized here
   rather than inherited: every rule that used to shape this block was
   scoped to `.control-bar`, and it does not live there any more. */
.player-meta .hud-level-bar {
  position: relative;
  width: clamp(120px, 12vw, 190px);
  height: 9px;
  min-width: 0;
  border-radius: var(--radius-pill);
  overflow: hidden;
  background: rgba(0, 0, 0, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.12);
}
.player-meta .hud-level-fill { height: 100%; }
.player-meta .hud-level-figures {
  position: static;
  display: block;
  font-size: 0.55rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
}
.player-meta .hud-level-perk {
  font-size: 0.5rem;
  color: var(--violet-bright);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 20ch;
}
/* font-size: 0, not 0.55rem. The button's glyph is background ARTWORK
   (deck-info-boosts.css sets the image and font-size: 0 to suppress the
   text node). This rule is (0,2,0) against that file's (0,1,0), so giving
   it a real font-size printed the literal "i" from the markup on top of
   the artwork — two i's, one over the other. */
.player-meta .hud-level-info {
  width: 15px;
  height: 15px;
  font-size: 0;
  line-height: 1;
  flex: 0 0 auto;
}

/* WHAT YOU HAVE — the pills across the middle. */
.top-pills {
  display: flex;
  align-items: stretch;
  justify-content: center;
  gap: clamp(6px, 0.9vw, 14px);
  flex-wrap: nowrap;
  min-width: 0;
  margin: 0;
  padding: 0;
}
.top-pills .hud-stat,
.top-pills .daily-timer {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 11px;
  border: 1px solid var(--cab-edge);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(10, 20, 34, 0.72), rgba(6, 12, 22, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 2px 10px rgba(0, 0, 0, 0.35);
  min-width: 0;
}
.top-pills .hud-icon,
.top-pills .daily-timer-icon {
  width: 30px;
  height: 30px;
  object-fit: contain;
  flex: 0 0 auto;
}
.top-pills .hud-stat-body,
.top-pills .daily-timer-body {
  display: flex;
  flex-direction: column;
  gap: 0;
  min-width: 0;
  line-height: 1.15;
}
.top-pills .hud-label,
.top-pills .daily-timer-label {
  font-size: 0.55rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
}
.top-pills .hud-value,
.top-pills .daily-timer-value {
  font-family: var(--font-display, inherit);
  font-size: 1.15rem;
  font-weight: 800;
  letter-spacing: 0.02em;
  color: var(--text, #eaf6ff);
  white-space: nowrap;
}
.top-pills .hud-max { font-size: 0.72em; color: var(--muted, #9fb6cc); }
.top-pills .hud-regen,
.top-pills .daily-timer-note {
  font-size: 0.5rem;
  font-weight: 600;
  color: var(--cyan-bright, #7ee8ff);
  white-space: nowrap;
}
.top-pills .daily-timer-note { color: var(--muted, #9fb6cc); }
.top-pills .hud-coins .hud-value { color: var(--gold, #ffd76a); }

/* The pill's own buttons, stacked at its right edge. */
.hud-actions {
  display: flex;
  flex-direction: column;
  gap: 3px;
  flex: 0 0 auto;
  align-items: stretch;
}
.hud-actions .hud-plus {
  width: 24px;
  height: 24px;
  align-self: center;
}
.top-pills .refill-pill {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
  padding: 2px 7px;
  border-radius: var(--radius-sm);
  line-height: 1.1;
  min-width: 0;
}
.top-pills .refill-pill-title { font-size: 0.45rem; letter-spacing: 0.08em; }
.top-pills .refill-pill-sub { font-size: 0.5rem; font-weight: 700; }

/* WHERE YOU GO — labelled icon tiles, as the reference has them. */
.top-controls-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 5px;
  flex: 0 0 auto;
}
.top-icons { display: flex; gap: 6px; }
.icon-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  width: 58px;
  padding: 6px 4px;
  border: 1px solid var(--cab-edge);
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, rgba(10, 20, 34, 0.72), rgba(6, 12, 22, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
  color: var(--text, #eaf6ff);
  cursor: pointer;
  text-decoration: none;
  transition: border-color 0.15s, transform 0.12s, box-shadow 0.15s;
}
.icon-tile:hover {
  border-color: var(--violet);
  box-shadow: 0 0 12px var(--violet-glow), inset 0 1px 0 rgba(255, 255, 255, 0.1);
  transform: translateY(-1px);
}
.icon-tile svg { width: 19px; height: 19px; flex: 0 0 auto; }
.icon-tile-label {
  font-size: 0.44rem;
  font-weight: 800;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
}
.top-toggles { display: flex; align-items: center; gap: 6px; }

/* --- LEFT RAIL ----------------------------------------------------- */
.rail-left {
  gap: 7px;
  justify-content: flex-start;
  align-items: stretch;
}
.rail-tile {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  width: 100%;
  padding: 9px 6px;
  border: 1px solid var(--cab-edge);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(10, 20, 34, 0.74), rgba(6, 12, 22, 0.9));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 2px 10px rgba(0, 0, 0, 0.35);
  color: var(--text, #eaf6ff);
  cursor: pointer;
  transition: border-color 0.15s, transform 0.12s, box-shadow 0.15s;
}
.rail-tile:hover {
  border-color: var(--violet);
  box-shadow: 0 0 14px var(--violet-glow), inset 0 1px 0 rgba(255, 255, 255, 0.1);
  transform: translateY(-1px);
}
.rail-tile svg { width: 24px; height: 24px; flex: 0 0 auto; color: var(--gold, #ffd76a); }
.rail-tile-label {
  font-family: var(--font-display, inherit);
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
}
.rail-tile .count-badge {
  position: absolute;
  top: -5px;
  right: -5px;
}
.boosts-dock.in-rail { position: relative; width: 100%; }

/* The one tile that is selling something looks like it. */
.rail-offer {
  border-color: var(--gold, #ffd76a);
  background: linear-gradient(180deg, rgba(60, 40, 10, 0.85), rgba(24, 14, 4, 0.92));
  overflow: hidden;
}
.rail-offer .rail-tile-label { color: var(--gold, #ffd76a); }
.rail-offer-art { width: 34px; height: 34px; object-fit: contain; }
.rail-offer-cta {
  display: block;
  width: 100%;
  padding: 3px 0;
  border-radius: var(--radius-xs);
  background: linear-gradient(180deg, #4ade80, #16a34a);
  color: #04210f;
  font-size: 0.46rem;
  font-weight: 900;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* --- THE MACHINE NAME ---------------------------------------------- */
.machine-stage > .machine-plaque {
  flex: 0 0 auto;
  align-self: center;
  max-width: 100%;
  margin-bottom: 4px;
  padding: 3px 26px;
  border: 1px solid var(--gold, #ffd76a);
  border-radius: var(--radius-pill);
  background: linear-gradient(180deg, rgba(48, 32, 8, 0.9), rgba(20, 12, 3, 0.95));
  box-shadow: 0 0 16px rgba(255, 215, 106, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.12);
  font-family: var(--font-display, inherit);
  font-size: clamp(0.7rem, 1.5vw, 1.1rem);
  font-weight: 900;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--gold, #ffd76a);
  text-shadow: 0 0 10px rgba(255, 215, 106, 0.45);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Nothing to announce on a machine with no label. */
.machine-plaque:not(:has(#machineTitle:not(:empty))) { display: none; }

/* --- RIGHT RAIL: THE JACKPOT BANNER -------------------------------- */
.jackpot-banner {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 5px;
  padding: 8px 7px 9px;
  border: 1px solid var(--gold, #ffd76a);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(72, 18, 40, 0.88), rgba(28, 8, 18, 0.94));
  box-shadow: 0 0 18px rgba(255, 215, 106, 0.16), inset 0 1px 0 rgba(255, 255, 255, 0.08);
  min-width: 0;
}
.jackpot-banner-title {
  font-family: var(--font-display, inherit);
  font-size: 0.62rem;
  font-weight: 900;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-align: center;
  color: var(--gold, #ffd76a);
  text-shadow: 0 0 10px rgba(255, 215, 106, 0.5);
}

/* --- DECK ---------------------------------------------------------- */
.cabinet-controls {
  gap: clamp(6px, 0.9vw, 14px);
}

/* Bet panel: multiplier on the left, what it costs on the right. */
.bet-panel {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
}
.bet-panel-col {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  min-width: 0;
}
.bet-panel .meter-label {
  font-size: 0.45rem;
  font-weight: 800;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
}
.bet-total { padding-left: 10px; border-left: 1px solid var(--cab-edge); }
.bet-total-value {
  display: flex;
  align-items: center;
  gap: 5px;
  font-family: var(--font-display, inherit);
  font-size: 1.05rem;
  font-weight: 800;
  color: var(--gold, #ffd76a);
  white-space: nowrap;
}
.bet-total-value.is-doubled { color: var(--violet-bright); }
.bet-total-coin { width: 19px; height: 19px; object-fit: contain; }

/* HOLD and AUTO SPIN carry the sublabel the reference gives them. */
.deck-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  line-height: 1.1;
}
.deck-btn-main {
  display: flex;
  align-items: center;
  gap: 5px;
  font-weight: 800;
}
.deck-btn-sub {
  font-size: 0.4rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
}
.hold-lock { width: 13px; height: 13px; flex: 0 0 auto; }
.spin-btn { position: relative; }
.spin-btn-sub {
  position: absolute;
  left: 50%;
  bottom: -12px;
  transform: translateX(-50%);
  color: var(--gold, #ffd76a);
  pointer-events: none;
}

/* The readout corner. */
.deck-info {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 3px;
  padding: 7px 13px;
  border: 1px solid var(--cab-edge);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(10, 20, 34, 0.72), rgba(6, 12, 22, 0.86));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
  min-width: 0;
}
.deck-info-row { display: flex; align-items: baseline; gap: 8px; white-space: nowrap; }
.deck-info-key {
  font-size: 0.48rem;
  font-weight: 800;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  min-width: 3.4em;
}
.deck-info-val {
  font-family: var(--font-display, inherit);
  font-size: 0.95rem;
  font-weight: 800;
  color: var(--text, #eaf6ff);
  margin-left: auto;
}

/* --- REFERENCE LAYOUT: CORRECTIONS AFTER MEASURING ------------------ */

/* The pills were 45, 56 and 58 tall because each sized to its own
   content. Three plates of three heights in a row reads as three
   unrelated widgets, which is the thing the reference's uniform pills
   avoid. */
.top-pills > * { align-self: stretch; }

/* "XP:" as a prefix, and only when there is a figure to prefix. Done in
   CSS rather than in renderHUD because that same string is written by
   shared code and the label belongs to this layout, not to the data. */
.player-meta .hud-level-figures:not(:empty)::before { content: 'XP: '; }

/* SPIN has to be the biggest control on the deck, and it was the
   SMALLEST: 168px against HOLD at 290 and AUTO SPIN at 355. Nothing set
   those widths — the sublabels did. "HOLD FOR SETTINGS" on one line is
   wider than the button it names, so each button sized to its own
   caption and the most-pressed control on the screen came last.

   The captions wrap instead, and the three widths are set explicitly in
   the order the reference gives them. */
.cabinet-controls .deck-btn {
  width: clamp(104px, 11vw, 152px);
  flex: 0 0 auto;
  padding-inline: 6px;
}
.cabinet-controls .spin-btn {
  width: clamp(150px, 17vw, 244px);
  flex: 0 0 auto;
}
.deck-btn-sub {
  white-space: normal;
  text-align: center;
  line-height: 1.15;
  max-width: 100%;
}
/* SPIN's caption hangs under the artwork, so it must not wrap into it. */
.spin-btn-sub { white-space: nowrap; }

/* The gift pill alone refused to stretch: `.top-pills > *` is one class
   and a child combinator, which `.top-pills .daily-timer` outranks. Set
   on the same selector that styles it, so it cannot lose again. */
.top-pills .daily-timer { align-self: stretch; height: auto; }

/* HOLD and AUTO SPIN came out 110 and 159 tall — a caption that wraps to
   two lines made one button half again as tall as its neighbour. They
   are a matched pair either side of SPIN in the reference, so they get a
   matched height and SPIN keeps being the tall one. */
.cabinet-controls .deck-btn {
  height: clamp(66px, 8.6vh, 96px);
  align-self: center;
}

/* The gift pill stretched correctly all along — `align-self: stretch`
   computed as `stretch`, and it was still 8px shorter than its
   neighbours. A stretched flex item fills the line MINUS its own
   margins, and this one carried `margin-bottom: 8px` from the days it
   sat in a stacked column. 64.45 - 8 = 56.45, exactly the gap. Worth
   the note: two rounds were spent adding more `align-self` to a
   property that was never the one at fault. */
.top-pills .daily-timer { margin: 0; }

/* --- THE TOP BAR ON A PHONE ---------------------------------------- */
/* Three columns need about 900px. On a 375px screen the gift pill ran
   to x=432 and the icon tiles to x=449 — off the side of the phone, and
   under `overflow: hidden` that means gone, not scrollable. Identity and
   the icons share the first line; the pills take the second. */
@media (max-width: 720px) {
  .top-controls {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "player icons"
      "pills  pills";
    row-gap: 6px;
    padding-inline: 6px;
  }
  .player-card    { grid-area: player; padding: 4px 8px 4px 4px; gap: 7px; }
  .top-controls-right { grid-area: icons; }
  .top-pills      { grid-area: pills; justify-content: space-between; gap: 5px; }

  .player-avatar { width: 38px; height: 38px; }
  .player-avatar-letter { font-size: 1rem; }
  .player-name { font-size: 0.7rem; max-width: 11ch; }
  .player-level { font-size: 0.52rem; }
  .player-meta .hud-level-bar { width: clamp(84px, 34vw, 150px); height: 7px; }
  .player-meta .hud-level-figures { font-size: 0.46rem; }
  /* The perk line is the first thing that can go: it repeats a number
     the level badge already implies, and it is the widest text here. */
  .player-meta .hud-level-perk { display: none; }

  .icon-tile { width: 42px; padding: 4px 2px; gap: 2px; }
  .icon-tile svg { width: 15px; height: 15px; }
  .icon-tile-label { font-size: 0.36rem; letter-spacing: 0.04em; }
  .top-icons { gap: 4px; }
  .top-toggles { gap: 4px; }

  .top-pills .hud-stat,
  .top-pills .daily-timer { padding: 5px 7px; gap: 6px; flex: 1 1 0; min-width: 0; }
  .top-pills .hud-icon,
  .top-pills .daily-timer-icon { width: 22px; height: 22px; }
  .top-pills .hud-value,
  .top-pills .daily-timer-value { font-size: 0.85rem; }
  .top-pills .hud-label,
  .top-pills .daily-timer-label { font-size: 0.44rem; letter-spacing: 0.06em; }
  .top-pills .hud-regen,
  .top-pills .daily-timer-note { font-size: 0.42rem; }
  .hud-actions .hud-plus { width: 20px; height: 20px; }
  .top-pills .refill-pill { padding: 1px 5px; }
  .top-pills .refill-pill-title { font-size: 0.38rem; }
  .top-pills .refill-pill-sub { font-size: 0.42rem; }
}

/* The toggles were a SECOND row under the icon tiles, making the right
   group 83px tall against the player card's 48 — so that group alone set
   the height of the whole first line, and it cost the reels 34px on a
   phone (168px tall instead of 202, symbols squashed to 65x49).

   `display: contents` dissolves the wrapper so the speaker joins the
   icon tiles on their own line. The theme switch is the one thing that
   does not fit beside them at 375px, and it is the only control here
   that is a preference rather than a game action: it is set once and
   persists, and the reference has no theme switcher at all. Desktop
   keeps it. If you would rather have it on phones than the 34px, delete
   this one `display: none`. */
@media (max-width: 720px) {
  .top-controls-right { flex-direction: row; align-items: center; gap: 4px; }
  .top-toggles { display: contents; }
  .top-toggles .theme-switch { display: none; }
  .player-meta .hud-level-bar { width: clamp(70px, 26vw, 130px); }
}

/* --- PORTRAIT: WIDTH IS THE SCARCE DIMENSION ------------------------ */
/* `aspect-ratio` only gets to choose the dimension that is still auto.
   In a column flex the frame's HEIGHT is flex-determined and therefore
   definite, so the ratio sets the width — right on a landscape screen,
   wrong on a portrait one, where the stage is 701px tall and only 797
   wide: the width hit its cap, the height kept growing, and the cells
   came out 153x227, two-thirds square.

   Portrait flips which dimension leads. The frame stops growing, takes
   the full width, and the ratio sets the height from it. The leftover
   vertical space simply goes unused, which is the correct trade: a
   phone has height to spare and no width at all. */
@media (max-aspect-ratio: 1 / 1) and (max-width: 900px) {
  .cabinet .machine-frame {
    flex: 0 1 auto;
    width: 100%;
    height: auto;
  }
}

/* --- DECK BUTTONS MUST FIT THEIR OWN CAPTIONS ----------------------- */
/* A fixed `height` on a button whose label wraps is a promise the text
   cannot keep: AUTO SPIN needed 72px inside 62, so "AUTO SPIN" and
   "HOLD FOR SETTINGS" printed on top of each other. `min-height` sets
   the floor and lets the button grow to whatever its caption needs, and
   the extra width keeps most captions to two lines in the first place. */
.cabinet-controls .deck-btn {
  width: clamp(118px, 12.5vw, 176px);
  height: auto;
  min-height: clamp(58px, 7.4vh, 86px);
  padding-block: 6px;
}

/* SPIN's caption hangs below the artwork, so the row needs room under it
   or the last line sits on the very edge of the screen. */
.cabinet-controls { padding-bottom: 6px; }

/* The deck sizes above are desktop figures. On a 375px phone a 118px
   floor on each button plus SPIN plus the bet panel forces the row to
   wrap, and the taller deck squeezed the cabinet until the frame was
   shrunk below its own aspect height — 359x159, cells 65x46. The frame
   had stopped being the thing that got the space again, one layer down
   from where it happened last time. */
@media (max-width: 720px) {
  .cabinet-controls .deck-btn {
    width: clamp(74px, 21vw, 112px);
    min-height: 50px;
    padding-block: 4px;
  }
  .cabinet-controls .spin-btn { width: clamp(92px, 25vw, 136px); }
  .cabinet-controls { padding-bottom: 9px; gap: 5px; }
  .deck-btn-sub { font-size: 0.34rem; letter-spacing: 0.03em; }
  .bet-panel { padding: 5px 8px; gap: 6px; }
  .bet-total { padding-left: 6px; }
  .deck-info { padding: 5px 8px; }
}

/* =====================================================================
   HOLD SPIN TO START AUTOPLAY
   ===================================================================== */

/* A ring, not a bar. The button's own box is the only place with
   guaranteed room: .spin-btn-sub already hangs below the artwork and the
   deck's bottom padding is the only reserve under it, so anything else
   down there is clipped by the overflow:hidden on html/body.
   `inset: -3px` costs no layout at all.

   The radius is copied from .spin-btn-overlay: the artwork is a wide
   rounded rectangle, not a circle, so a circular ring would not fit it. */
.spin-hold-ring {
  position: absolute;
  inset: -3px;
  border-radius: 50%;
  padding: 3px;
  background: conic-gradient(from -90deg,
    var(--gold, #ffd76a) calc(var(--hold-progress, 0) * 360deg),
    rgba(255, 255, 255, 0.10) 0);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  opacity: 0;
  transition: opacity 120ms ease;
  pointer-events: none;
  z-index: 3;
}
.spin-btn.is-charging .spin-hold-ring { opacity: 1; }

/* The progress is driven by a custom property from rAF, NOT by a CSS
   animation, and that is deliberate: tutorial.css kills .spin-btn
   animations under 820px and deck-info-boosts.css kills them under
   prefers-reduced-motion. A custom property survives both, so the
   feedback still works on phones and for reduced-motion users, where it
   is a signal rather than decoration. */

/* An autoplay run in progress. Reuses the orange the old AUTO button
   carried, so the run reads as the same feature that moved. No animation
   here, for the same 820px reason. */
.spin-btn.is-auto {
  filter:
    drop-shadow(0 6px 14px rgba(0, 0, 0, 0.55))
    drop-shadow(0 0 22px rgba(255, 138, 79, 0.6));
}
.spin-btn.is-auto .spin-btn-overlay { color: #ffe9d6; }

/* A long press must not select the artwork or pop the iOS image callout. */
.spin-btn { -webkit-touch-callout: none; }

/* The NEW badge halo, now that unlocks point at SPIN. `.has-new` is
   (0,1,0) and `.spin-btn:not(:disabled)` is (0,2,0) — :not() counts its
   argument — so the breathe animation would win and the halo would never
   run. Both are named here, and the 820px opt-out is mirrored, or this
   (0,2,0) rule would beat that breakpoint at every width. */
.spin-btn.has-new {
  animation: newHalo 1.8s ease-in-out infinite, spinBtnBreathe 2.6s ease-in-out infinite;
}
@media (max-width: 820px) {
  .spin-btn.has-new { animation: none; }
}

/* =====================================================================
   ACCOUNT FORM — the password rules, ticked live
   ===================================================================== */
.account-rules {
  list-style: none;
  margin: 4px 0 8px;
  padding: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px 10px;
}
.account-rules li {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.62rem;
  color: var(--muted, #9fb6cc);
  transition: color 140ms ease;
}
.account-rule-mark {
  width: 1em;
  text-align: center;
  font-weight: 800;
}
.account-rules li.is-met { color: #4ade80; }
/* Only after the server has actually refused it — an unmet rule you have
   not submitted yet is not an error, it is just not done. */
.account-rules li.is-rejected { color: #f87171; }
.account-rule {
  margin: 2px 0 6px;
  font-size: 0.62rem;
  color: #f87171;
}

/* =====================================================================
   NO DEAD SPACE

   Measured before writing any of this, at three sizes:

     dead zone                1920x1080   2560x1440   3440x1440
     under the jackpot banner     556px       916px       916px
     under the left rail tiles    425px       785px       785px
     each side of the frame        83px        32px       376px

   Three quarters of both rails were empty, and every label was a fixed
   pixel size — 7.2px bet labels, 8px tile labels, identical at 1280 and
   2560, i.e. unreadable on the screens this is meant to fill.
   ===================================================================== */

/* ---------------------------------------------------------------------
   THE FRAME FILLS THE STAGE; THE REELS HOLD THE RATIO

   The frame used to carry `aspect-ratio: 5/3` itself, so its width was
   derived from its height and it could never touch the rails — leaving a
   bare strip of page background down each side, 376px of it on an
   ultrawide.

   Moving the ratio onto `.reels` inverts that. The frame takes the whole
   stage, so the lit border and the corner brackets now enclose the full
   cabinet face; the leftover becomes cabinet INSIDE the machine rather
   than emptiness outside it. The symbols stay exactly square because the
   reel window still owns the ratio.
   --------------------------------------------------------------------- */
@media (min-width: 901px) {
  /* THE MACHINE KEEPS ITS SHARE, WHATEVER SHAPE THE WINDOW IS.

     Letting the rails take ALL the surplus (`1fr`) removed the gap, but
     only because the rails swallowed it — and whenever the machine is
     limited by HEIGHT rather than width, that surplus is enormous:

                        machine   rail    rail / machine
       1920x1080          61%w     335         0.29   fine
       1920x780           37%w     562         0.79   rails dominate
       2560x900           35%w     784         0.88   rails dominate
       1366x650           37%w     398         0.79   rails dominate

     A short window is not unusual — a browser that is not maximised on a
     1080p screen is about 780 tall. So the rails are capped instead, and
     the frame takes what is left over. Measured with the cap, the machine
     is 59-61% of the width and the rails are 0.29-0.30 of the machine at
     EVERY size above, which is the consistency the `1fr` version only had
     on tall windows.

     18vw specifically: the rails need 17.4vw at 1920x1080, so a smaller
     cap would bind there and starve the machine of the width its reels
     need. */
  .cabinet .machine-frame {
    flex: 1 1 auto;
    width: 100%;
    height: 100%;
    min-height: 0;
    max-width: none;
    max-height: none;
    aspect-ratio: auto;
    margin-inline: 0;
  }
  .cabinet .reels {
    flex: 0 1 auto;
    width: auto;
    height: 100%;
    max-width: 100%;
    aspect-ratio: 5 / 3;
    margin-inline: auto;
  }
}

/* Genuinely tall desktops (5:4, 4:3). There the rails hit their 150px
   floor first, the centre track can no longer grow, and a height-driven
   reel window is clamped by max-width with its height unchanged — 82%
   square at 1280x1024, measured. Below 1.4 the width leads instead.

   This threshold IS a guess, unlike the container-query version it
   replaces, so it is set well clear of the shapes that work: 1.6 and
   1.78 both measured 100% square height-driven, and 1.25 failed. It only
   has to catch the regime where the rails are already bottomed out. */
@media (min-width: 901px) and (max-aspect-ratio: 7 / 5) {
  .cabinet .machine-frame { width: 100%; }
  .cabinet .reels {
    width: 100%;
    height: auto;
    max-height: 100%;
  }
}

/* Which dimension is the scarce one depends on the FRAME's aspect, not
   the viewport's — and CSS cannot express a media query about an
   element. The first attempt guessed a viewport threshold of 8/5, which
   is wrong the moment the chrome changes and was wrong immediately at
   1440x900: that viewport IS exactly 1.6, so the query matched, the
   width-driven branch fired against a 1057x539 frame, and the symbols
   came out 84% square.

   Container query units answer it exactly. `100cqh` is the frame's own
   height, so `min(100%, 100cqh * 5/3)` takes whichever of width or
   height actually binds, at any frame shape, with no threshold to get
   wrong. Verified that `container-type: size` leaves .win-lines and
   .cabinet-corners measuring exactly as before. */
/* The container-query sizing that used to live here is gone with the
   frame that made it possible. `container-type: size` needs a definite
   width, and the frame's width is now derived from the reels inside it —
   declaring the frame a size container would have made that circular and
   collapsed the reel window to nothing. The aspect breakpoint above does
   the same job for the one viewport shape that needs it. */

/* The portrait override further down this file is (0,2,0) on
   .cabinet .machine-frame and sits LATER, so without a width guard it
   would win on any portrait DESKTOP monitor and undo the block above. */

/* ---------------------------------------------------------------------
   THE RIGHT RAIL — a column of the cabinet, not a header with a hole
   --------------------------------------------------------------------- */
@media (min-width: 901px) {
  .rail-right { justify-content: stretch; }
  .rail-right > .jackpot-banner { flex: 1 1 auto; min-height: 0; }
  .jackpot-banner > .prize-ladder {
    flex: 1 1 auto;
    min-height: 0;
    justify-content: space-between;
  }
  .prize-ladder > .prize-row { flex: 1 1 0; min-height: 0; }
  .prize-row img { width: clamp(24px, 2.6vmin, 56px); height: auto; aspect-ratio: 1; }
  /* The spin cluster is an empty box whenever no boost is running, and an
     empty box in a stretched column is a hole with a border round it. */
  .rail-right > .spin-cluster:not(:has(> :not([hidden]))) { display: none; }
}

/* ---------------------------------------------------------------------
   THE LEFT RAIL — tiles share the height instead of stacking at the top
   --------------------------------------------------------------------- */
@media (min-width: 901px) {
  .rail-left {
    justify-content: space-between;
    gap: clamp(7px, 1.1vh, 16px);
  }
  .rail-left > .rail-tile,
  .rail-left > .boosts-dock.in-rail {
    flex: 1 1 0;
    min-height: 0;
    /* Capped, or one tile becomes a 230px slab. What the caps leave over
       is spread as even gutters, which reads as spacing rather than as a
       gap at the bottom. */
    max-height: clamp(64px, 11vh, 148px);
  }
  /* Listed after .rail-tile deliberately: same specificity, so source
     order decides, and the offer is meant to be the bigger one. */
  .rail-left > .rail-offer {
    flex: 1.5 1 0;
    max-height: clamp(88px, 15vh, 200px);
  }
  .boosts-dock.in-rail > .rail-tile { height: 100%; }
}

/* ---------------------------------------------------------------------
   THE TOP BAR — the single biggest recovery, ~1440px of dead bar at 2560
   --------------------------------------------------------------------- */
@media (min-width: 901px) {
  /* `.top-controls .top-pills`, not a bare `.top-pills`: the bare version
     loses to `.slot-machine .hud { gap: 8px }`, which is why the pills sat
     8px apart however much this layer asked for. */
  .top-controls .top-pills {
    justify-content: space-evenly;
    gap: clamp(10px, 1.3vw, 30px);
  }
  .top-pills > .hud-stat,
  .top-pills > .daily-timer {
    flex: 1 1 0;
    max-width: clamp(220px, 19vw, 380px);
    /* Pins each pill's own buttons to its right edge, so a widened pill is
       not itself hollow. */
    justify-content: space-between;
  }
}

/* ---------------------------------------------------------------------
   THE DECK — both flanks grow equally, so SPIN stays centred
   --------------------------------------------------------------------- */
@media (min-width: 901px) {
  .cabinet-controls .meter-stake,
  .cabinet-controls .deck-info {
    flex: 1 1 0;
    max-width: clamp(240px, 17vw, 400px);
  }
}
/* This is NOT the mistake made earlier in this file, where giving
   `flex: 1 1 0` to the STATS BUTTON inflated it to 488px. These two are
   readout PANELS: a wider panel is an improvement, and growing both
   flanks by the same cap is what keeps SPIN on the centre line. */

/* ---------------------------------------------------------------------
   THE DECK'S TYPOGRAPHY LEAK

   `.control-bar span` is (0,1,1) and catches every sublabel, figure and
   readout in the deck. Each of these is (0,2,0) so it wins, and they are
   OUTSIDE any breakpoint because this is a correction, not scaling — the
   blanket rule was winning on phones too.
   --------------------------------------------------------------------- */
.control-bar .deck-btn-main   { font-size: 0.8rem; color: var(--text, #eaf6ff); }
.control-bar .deck-btn-sub    { font-size: 0.4rem; color: var(--muted, #9fb6cc); }
.control-bar .spin-btn-sub    { font-size: 0.4rem; color: var(--gold, #ffd76a); }
.control-bar .bet-total-value { font-size: 1.05rem; color: var(--gold, #ffd76a); }
.control-bar .deck-info-key   { font-size: 0.48rem; color: var(--muted, #9fb6cc); }
.control-bar .deck-info-val   { font-size: 0.95rem; color: var(--text, #eaf6ff); }
.control-bar .bet-panel .meter-label { font-size: 0.45rem; }
@media (max-width: 720px) {
  .control-bar .deck-btn-sub { font-size: 0.34rem; }
}

/* =====================================================================
   HIGH RESOLUTION

   Anchored on vmin, not vw, and that is the load-bearing choice: vmin is
   14.4px at BOTH 2560x1440 and 3440x1440, and 21.6px at 3840x2160 —
   which correctly says an ultrawide is not a bigger screen, it is a
   wider one. A vw anchor would inflate the deck on a 21:9 panel that has
   no extra vertical room to pay for it.

   Everything is `clamp(<today's value>, <n>vmin, <cap>)`, so nothing
   changes below the breakpoint, and the whole block is inside its own
   @media so it cannot leak down onto phones.
   ===================================================================== */
@media (min-width: 1600px) {
  /* Type */
  .icon-tile-label                  { font-size: clamp(0.44rem, 0.66vmin, 12px); }
  .rail-tile-label                  { font-size: clamp(0.5rem, 0.78vmin, 14px); }
  .top-pills .refill-pill-sub       { font-size: clamp(0.5rem, 0.75vmin, 13px); }
  .player-meta .hud-level-perk      { font-size: clamp(0.5rem, 0.75vmin, 13px); }
  .rail-offer-cta                   { font-size: clamp(0.46rem, 0.69vmin, 12px); }
  .control-bar .bet-panel .meter-label { font-size: clamp(0.45rem, 0.67vmin, 12px); }
  .top-pills .hud-label             { font-size: clamp(0.55rem, 0.82vmin, 15px); }
  .player-meta .hud-level-figures   { font-size: clamp(0.55rem, 0.82vmin, 15px); }
  .jackpot-banner-title             { font-size: clamp(0.62rem, 0.93vmin, 17px); }
  .player-level                     { font-size: clamp(0.62rem, 0.93vmin, 17px); }
  .player-name                      { font-size: clamp(0.86rem, 1.29vmin, 23px); }
  .top-pills .hud-value,
  .top-pills .daily-timer-value     { font-size: clamp(1.15rem, 1.72vmin, 31px); }
  .control-bar .deck-btn-main       { font-size: clamp(0.8rem, 1.2vmin, 21px); }
  .control-bar .deck-btn-sub        { font-size: clamp(0.4rem, 0.6vmin, 11px); }
  .control-bar .spin-btn-sub        { font-size: clamp(0.4rem, 0.6vmin, 11px); }
  .control-bar .deck-info-key       { font-size: clamp(0.48rem, 0.72vmin, 13px); }
  .control-bar .deck-info-val       { font-size: clamp(0.95rem, 1.42vmin, 26px); }
  .control-bar .bet-total-value     { font-size: clamp(1.05rem, 1.57vmin, 28px); }

  /* Boxes and glyphs */
  .player-avatar { width: clamp(52px, 3.6vmin, 78px); height: clamp(52px, 3.6vmin, 78px); }
  .player-avatar-letter { font-size: clamp(1.35rem, 2vmin, 2.4rem); }
  .player-meta .hud-level-bar { height: clamp(9px, 0.65vmin, 14px); width: clamp(120px, 13vmin, 300px); }
  .player-meta .hud-level-info { width: clamp(15px, 1.1vmin, 24px); height: clamp(15px, 1.1vmin, 24px); }
  /* (0,3,0): win-castle-account.css sets .slot-machine .hud-stat .hud-icon
     at the same specificity and is currently beating this layer's 30px. */
  .slot-machine .hud-stat .hud-icon,
  .top-pills .daily-timer-icon { width: clamp(30px, 2.1vmin, 46px); height: clamp(30px, 2.1vmin, 46px); }
  .hud-actions .hud-plus { width: clamp(24px, 1.7vmin, 36px); height: clamp(24px, 1.7vmin, 36px); }
  .icon-tile { width: clamp(58px, 4.2vmin, 92px); }
  .icon-tile svg { width: clamp(19px, 1.4vmin, 30px); height: clamp(19px, 1.4vmin, 30px); }
  .rail-tile svg { width: clamp(24px, 1.8vmin, 40px); height: clamp(24px, 1.8vmin, 40px); }
  .rail-offer-art { width: clamp(34px, 2.4vmin, 54px); }
  .bet-total-coin { width: clamp(19px, 1.4vmin, 30px); height: clamp(19px, 1.4vmin, 30px); }
  .hold-lock { width: clamp(13px, 0.95vmin, 21px); height: clamp(13px, 0.95vmin, 21px); }
  .machine-stage > .machine-plaque {
    font-size: clamp(0.7rem, 1.6vmin, 2.2rem);
    padding: clamp(3px, 0.3vmin, 7px) clamp(26px, 2.4vmin, 56px);
  }
  /* The prize ladder's own clamps are vw-anchored and already saturated,
     so the right rail was frozen at its 1280px size on a 4K screen. */
  .prize-row-tier  { font-size: clamp(0.44rem, 0.66vmin, 0.95rem); }
  .prize-row-value { font-size: clamp(0.62rem, 1vmin, 1.5rem); }

  /* Cabinet chrome — a 34px glow on a 1900px frame is invisible. */
  .reels { gap: clamp(2px, 0.22vmin, 6px); padding: clamp(6px, 0.55vmin, 14px); }
  .reel-col { border-right-width: clamp(1px, 0.12vmin, 3px); }
  .reel-col:last-child { border-right: none; }
  .cabinet-corners { inset: clamp(10px, 0.9vmin, 22px); }

  /* Deck controls sized off HEIGHT, not width. This is the ultrawide
     correctness point: 3440x1440 then produces exactly the same deck as
     2560x1440, which is right — the extra width buys no vertical room. */
  .cabinet-controls .spin-btn { width: clamp(244px, 19vh, 330px); }
  .cabinet-controls .deck-btn {
    width: clamp(176px, 13vh, 230px);
    min-height: clamp(86px, 7.4vh, 120px);
  }
  .cabinet-controls { gap: clamp(14px, 1.1vmin, 26px); }
  .deck-info { padding: clamp(7px, 0.6vmin, 14px) clamp(13px, 1.1vmin, 24px); }
}

/* Rails stop ballooning past ~260px, where a tile stops reading as a tile
   and starts reading as a slab. Same specificity as the base grid rule,
   later in the file, so it wins only at this width and cannot touch
   anything below it. */
@media (min-width: 1900px) {
  .cabinet { column-gap: clamp(14px, 1.2vw, 30px); }
  .rail-offer { flex-direction: column; justify-content: center; }
  .rail { gap: clamp(8px, 0.8vmin, 16px); }
}

/* =====================================================================
   REFILL — absent until it is actually charged
   ===================================================================== */

/* Hidden unless ready. `display` is the safe property to pick here: no
   @media anywhere in slots/css sets `display` on .refill-pill, so this
   one rule holds at every width. The markup ships without .is-ready, so
   the button is absent from the very first paint with no JS involved —
   which is what stops it flashing on before the server has answered. */
.top-pills .refill-pill:not(.is-ready) { display: none; }

/* The energy figure grows into the space the button leaves.
   Written as a clamp with a vmin term rather than a flat rem: at (0,4,0)
   this rule beats the high-resolution ladder above, so a flat value would
   freeze the energy number at 1.55rem on a 4K screen while the coins
   figure beside it scaled to 31px. One rule serving both purposes. */
.top-pills .hud-spins:not(.refill-armed) .hud-value {
  font-size: clamp(1.55rem, 2.1vmin, 34px);
}
@media (max-width: 720px) {
  .top-pills .hud-spins:not(.refill-armed) .hud-value { font-size: 1.05rem; }
}
/* Smooths the resize as the refill arms and is spent. Only the font
   changes; the rAF ticker writes textContent and is unaffected. */
.top-pills .hud-value { transition: font-size 180ms ease; }

/* NOT `display: contents` on .hud-actions, which was the obvious move and
   is wrong: .hud-plus is an ordinary flow child (deck-info-boosts.css
   and this file give it width, height and align-self, but never
   `position`), so dissolving its wrapper would promote it to a direct
   flex item of the pill and hand it the pill's 9px gap — and, with
   `justify-content: space-between` on the widened pill above, fling it to
   the far edge. Collapsing the wrapper's own gap is enough. */
.top-pills .hud-spins:not(.refill-armed) .hud-actions { gap: 0; }

/* =====================================================================
   NEXT THEME CARD
   Fixed height on purpose: the jackpot banner below it is what absorbs
   the rail's surplus (see the fill rules above). A card that stretched
   too would put the hole straight back into the rail.
   ===================================================================== */
.next-theme {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas:
    "label label"
    "art   name"
    "art   time";
  align-items: center;
  gap: 2px 9px;
  padding: 8px 10px;
  border: 1px solid var(--violet);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(38, 16, 60, 0.88), rgba(16, 8, 26, 0.94));
  box-shadow: 0 0 16px var(--violet-glow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
  min-width: 0;
}
.next-theme[hidden] { display: none; }
.next-theme-label {
  grid-area: label;
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  text-align: center;
}
.next-theme-art {
  grid-area: art;
  width: clamp(34px, 3vmin, 60px);
  height: clamp(34px, 3vmin, 60px);
  object-fit: contain;
}
.next-theme-name {
  grid-area: name;
  font-family: var(--font-display, inherit);
  font-size: clamp(0.58rem, 0.9vmin, 1rem);
  font-weight: 800;
  letter-spacing: 0.04em;
  color: var(--violet-bright);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.next-theme-time {
  grid-area: time;
  font-family: var(--font-display, inherit);
  font-size: clamp(0.72rem, 1.15vmin, 1.3rem);
  font-weight: 800;
  color: var(--text, #eaf6ff);
}

/* =====================================================================
   BET — one control, with its price under it
   ===================================================================== */
.bet-panel {
  flex-direction: column;
  align-items: center;
  gap: 3px;
}
.bet-stepper { width: 100%; }
/* The separate bordered TOTAL BET column is gone. The cost is an answer,
   not a second control, so it reads as a caption under the stepper
   instead of competing with it for the eye. */
.bet-cost {
  display: flex;
  align-items: center;
  gap: 5px;
  white-space: nowrap;
}
.bet-cost-label {
  font-size: 0.45rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
}
.control-bar .bet-cost .bet-total-value { font-size: 0.9rem; }
/* Double Down stakes twice the chosen bet without changing the bet, so
   the price says so rather than quietly understating it. */
.bet-total-value.is-doubled { color: var(--violet-bright); }

/* The stepper itself: bigger target, clearer hierarchy. The current value
   is the hero — it is the thing being changed — and the steppers read as
   controls around it rather than as three equal buttons. */
.control-bar .bet-group {
  display: flex;
  align-items: center;
  gap: 5px;
  flex-wrap: nowrap;
}
.control-bar .bet-step {
  width: clamp(26px, 2.4vmin, 40px);
  height: clamp(26px, 2.4vmin, 40px);
  padding: 0;
  border-radius: 50%;
  font-size: clamp(0.85rem, 1.3vmin, 1.4rem);
  font-weight: 800;
  line-height: 1;
  display: grid;
  place-items: center;
  flex: 0 0 auto;
}
.control-bar .bet-current {
  min-width: clamp(58px, 5.4vmin, 92px);
  font-family: var(--font-display, inherit);
  font-size: clamp(1rem, 1.6vmin, 1.7rem);
  font-weight: 800;
  color: var(--gold, #ffd76a);
  padding: 3px 8px;
  flex: 0 0 auto;
}
.control-bar .bet-x { font-size: 0.62em; opacity: 0.75; }
.control-bar .bet-max {
  font-size: clamp(0.5rem, 0.8vmin, 0.85rem);
  padding: 4px 9px;
  flex: 0 0 auto;
}

/* =====================================================================
   THE MIXER — per-source faders under the two buses
   ===================================================================== */
.sound-sub-header {
  margin: 10px 0 4px;
  font-size: 0.52rem;
  font-weight: 800;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted, #9fb6cc);
  border-top: 1px solid var(--cab-edge);
  padding-top: 8px;
}
.sound-channel-sub .sound-channel-label { font-size: 0.68rem; }
.sound-channel-sub.is-master .sound-channel-label {
  color: var(--gold, #ffd76a);
  font-weight: 800;
}
.sound-channel-pct {
  min-width: 3.2em;
  text-align: right;
  font-size: 0.6rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted, #9fb6cc);
}

/* =====================================================================
   THE RAILS NOW HOLD WHATEVER WIDTH THE MACHINE DID NOT TAKE

   That width is a function of the viewport's ASPECT, not its width: at
   1280x720 the rails come out 290px and at 1280x1024 they come out 150px
   — same viewport width, one tile layout would be wrong for one of them.
   So the tile layout is chosen by asking the RAIL how wide it is, not by
   asking the viewport. The rail's width is definite (a 1fr track), so
   `container-type: inline-size` is safe here in a way it was not on the
   frame, which is sized from its own contents.
   ===================================================================== */
@media (min-width: 901px) {
  .rail { container-type: inline-size; }

  /* Icon beside label, once there is room for it to read as a row. */
  @container (min-width: 230px) {
    .rail-tile {
      flex-direction: row;
      justify-content: flex-start;
      gap: clamp(9px, 4%, 20px);
      padding: clamp(9px, 5%, 20px) clamp(12px, 6%, 26px);
    }
    .rail-tile-label { white-space: nowrap; font-size: clamp(0.5rem, 5.5cqw, 1.05rem); }
    .rail-tile svg { width: clamp(24px, 11cqw, 52px); height: clamp(24px, 11cqw, 52px); }
    .rail-offer { flex-direction: column; justify-content: center; }
    .rail-offer-art { width: clamp(34px, 16cqw, 84px); }
    .rail-offer-cta { font-size: clamp(0.46rem, 4.6cqw, 0.9rem); }
  }

  /* A really wide rail: let the jackpot rows breathe rather than leaving
     the extra width as padding inside each row. */
  @container (min-width: 420px) {
    .prize-row img { width: clamp(34px, 13cqw, 90px); }
    .prize-row-tier  { font-size: clamp(0.5rem, 3.4cqw, 1.15rem); }
    .prize-row-value { font-size: clamp(0.7rem, 5cqw, 1.8rem); }
    .jackpot-banner-title { font-size: clamp(0.62rem, 4cqw, 1.4rem); }
    .next-theme-art { width: clamp(40px, 14cqw, 96px); height: clamp(40px, 14cqw, 96px); }
    .next-theme-name { font-size: clamp(0.58rem, 4cqw, 1.25rem); }
    .next-theme-time { font-size: clamp(0.72rem, 5.4cqw, 1.9rem); }
  }
}

/* =====================================================================
   THE CABINET FACE

   The surround beside the reel window cannot always be zero. The reel
   window is 5:3 and the frame is whatever shape the window leaves it, so
   on a short, wide screen there IS surround — 4px at 1920x1080, 327px at
   2560x900. The question was never how to delete it, it was why it read
   as a hole: it was flat, unlit, undifferentiated dark.

   So it gets treated as what it is — the body of the machine. Two
   symmetric side panels with a soft inner light and a hairline against
   the reel window, drawn on the frame's own background so they cost no
   element and no compositor layer, and sit behind everything.
   ===================================================================== */
@media (min-width: 901px) {
  .cabinet .machine-frame {
    background-image:
      /* the lit centre, brightest behind the reels */
      radial-gradient(ellipse 70% 90% at 50% 45%,
        var(--violet-glow) 0%, transparent 70%),
      /* the two side panels, darker at the outer edges like moulding */
      linear-gradient(90deg,
        rgba(0, 0, 0, 0.42) 0%,
        rgba(0, 0, 0, 0.12) 12%,
        transparent 30%,
        transparent 70%,
        rgba(0, 0, 0, 0.12) 88%,
        rgba(0, 0, 0, 0.42) 100%),
      /* a faint vertical grain, so a wide panel is not a flat field */
      repeating-linear-gradient(90deg,
        rgba(255, 255, 255, 0.018) 0 2px,
        transparent 2px 7px);
    background-blend-mode: screen, normal, normal;
  }

  /* A lit edge where the cabinet meets the reel window, so the window
     reads as inset into the machine rather than pasted onto it. */
  .cabinet .reels {
    box-shadow:
      inset 0 0 0 2px var(--cab-edge),
      inset 0 0 40px -12px var(--violet-glow),
      0 0 0 1px rgba(0, 0, 0, 0.75),
      0 0 22px -6px var(--cyan-glow);
  }
}

/* =====================================================================
   THE COLLAPSED LAYOUT (< 901px) — the machine keeps the height too

   Below 901px the cabinet is one column and the rails become rows ABOVE
   and BELOW the machine. Their tiles kept their full desktop size there,
   so at 460x550 they took 87px and 70px of a 241px cabinet and left the
   reel window 71px tall — a strip. It is the same fault as the desktop
   one (the rails outgrowing the machine), turned on its side.

   Compact chips here: icon beside a small label, sized to the row rather
   than to a column.
   ===================================================================== */
@media (max-width: 900px) {
  .rail-left,
  .rail-right {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 5px;
  }
  .rail-left > .rail-tile,
  .rail-left > .boosts-dock.in-rail,
  .rail-right > * {
    flex: 0 1 auto;
    width: auto;
    max-height: none;
  }
  .rail-tile {
    flex-direction: row;
    gap: 6px;
    padding: 5px 9px;
    border-radius: var(--radius-sm);
  }
  .rail-tile svg { width: 16px; height: 16px; }
  .rail-tile-label { font-size: 0.44rem; letter-spacing: 0.06em; }
  .rail-offer { flex-direction: row; flex-wrap: wrap; }
  .rail-offer-art { width: 22px; }
  .rail-offer-cta { width: auto; padding: 2px 8px; font-size: 0.4rem; }

  /* The two right-rail cards go horizontal as well, or they are two more
     tall blocks competing with the reels for the same pixels. */
  .next-theme {
    grid-template-columns: auto auto auto;
    grid-template-areas: "art name time";
    gap: 2px 8px;
    padding: 5px 9px;
  }
  .next-theme-label { display: none; }
  .next-theme-art { width: 24px; height: 24px; }
  .jackpot-banner { flex-direction: row; align-items: center; gap: 8px; padding: 5px 9px; }
  .jackpot-banner-title { font-size: 0.5rem; }
  .jackpot-banner > .prize-ladder { flex-direction: row; gap: 6px; }
  .prize-ladder > .prize-row { flex: 0 0 auto; }

  /* NO min-height floor here, and that is deliberate.
     A `min-height: 34vh` was tried, to stop the machine ever being
     squeezed back to a strip. It forced the stage taller than its own 1fr
     grid row, so the frame overflowed downward and painted straight over
     the deck — the bottom border cut through the BET label and the reels
     covered the right rail. It did not register as clipping because
     nothing left the viewport; it collided with a sibling instead.
     The compact chips above are what give the machine its height back,
     and they do it by taking less rather than by demanding more. */
}

/* Which dimension binds is shape-dependent here too: at 375x812 the
   width is scarce and at 460x550 the height is, and a rule that picks
   one squashes the symbols on the other (68% square at 460x550,
   measured). Unlike the desktop frame, the STAGE has a definite size in
   this layout — it is the 1fr row of a single-column cabinet — so it can
   be a size container without circularity, and the reel window takes
   whichever of the two actually binds. */
@media (max-width: 900px) {
  .cabinet .machine-stage { container-type: size; }
  .cabinet .reels {
    width: min(100cqw, calc(100cqh * 5 / 3));
    height: auto;
    max-height: 100%;
    aspect-ratio: 5 / 3;
    margin-inline: auto;
  }
}

/* =====================================================================
   SHORT WINDOWS — the machine gains height by the OTHER rows taking less

   Forcing the stage taller is what overlapped the deck. The correct
   lever is the opposite one: on a short viewport the chrome gives ground
   and the cabinet's 1fr row collects it automatically, so nothing is ever
   pushed past its own track.
   ===================================================================== */
@media (max-height: 620px) {
  .top-pills .hud-stat,
  .top-pills .daily-timer { padding: 3px 7px; gap: 6px; }
  .top-pills .hud-icon,
  .top-pills .daily-timer-icon { width: 20px; height: 20px; }
  .top-pills .hud-value,
  .top-pills .daily-timer-value { font-size: 0.8rem; }
  .top-pills .hud-label,
  .top-pills .daily-timer-label { font-size: 0.4rem; }
  .player-avatar { width: 34px; height: 34px; }
  .player-name { font-size: 0.64rem; }
  .player-meta .hud-level-bar { height: 6px; }
  .icon-tile { padding: 3px 4px; gap: 1px; }
  .icon-tile svg { width: 14px; height: 14px; }
  .icon-tile-label { font-size: 0.34rem; }

  .rail-tile { padding: 3px 7px; }
  .rail-tile svg { width: 14px; height: 14px; }
  .rail-tile-label { font-size: 0.38rem; }

  .cabinet-controls { padding-bottom: 4px; gap: 6px; }
  .cabinet-controls .deck-btn { min-height: 42px; padding-block: 3px; }
  .cabinet-controls .spin-btn { width: clamp(88px, 20vh, 150px); }
  .bet-panel { padding: 4px 7px; gap: 4px; }
  .control-bar .bet-step { width: 22px; height: 22px; }
  .control-bar .bet-current { min-width: 46px; font-size: 0.85rem; padding: 2px 6px; }
  .deck-info { padding: 4px 7px; }
  .control-bar .deck-info-val { font-size: 0.78rem; }
}

/* A balance can never burst its pill again, whatever the server sends.
   The compaction in hud-tickers.js is the real fix; this is the net
   under it. `.hud-value` is `white-space: nowrap` and the pills row is
   `flex-wrap: nowrap` inside an `overflow: hidden` page, so an over-wide
   figure was not wrapping or scrolling — it was being clipped, taking
   its own "/max" with it. */
.top-pills .hud-stat { min-width: 0; }
.top-pills .hud-stat-body { min-width: 0; }
.top-pills .hud-value {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* =====================================================================
   THE BOOSTS PANEL — one button, everything behind it

   The rescues and BUY SYMBOL were loose chips in #modifierBar, in the
   left rail, above the shop tile. Owning three of them added three rows
   to a column that already held four tiles and the offer — and the rail
   does not scroll, so what came off the bottom was ENTER SHOP!.
   ===================================================================== */
.rail-left > .modifier-bar { display: none; }

/* The label is longer than the other tiles', so it is allowed to wrap
   rather than forcing the tile wider than the rail. */
#boostsButton .rail-tile-label {
  white-space: normal;
  line-height: 1.15;
  text-align: center;
}
@container (min-width: 230px) {
  #boostsButton .rail-tile-label { text-align: left; }
}

.boost-section {
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  color: var(--muted, #9fb6cc);
  margin: 6px 2px 3px;
}
.boost-section:first-child { margin-top: 0; }

.boost-use {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 7px 10px;
  margin-bottom: 4px;
  border: 1px solid var(--cab-edge);
  border-radius: var(--radius-sm);
  background: linear-gradient(180deg, rgba(10, 20, 34, 0.8), rgba(6, 12, 22, 0.9));
  color: var(--text, #eaf6ff);
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s, transform 0.12s;
}
.boost-use:hover:not(:disabled) {
  border-color: var(--violet);
  transform: translateY(-1px);
}
.boost-use:disabled { opacity: 0.45; cursor: default; }
.boost-use-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; flex: 1 1 auto; }
.boost-use-label {
  font-family: var(--font-display, inherit);
  font-size: 0.62rem;
  font-weight: 800;
  letter-spacing: 0.06em;
}
.boost-use-note {
  font-size: 0.5rem;
  color: var(--muted, #9fb6cc);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.boost-use-count {
  flex: 0 0 auto;
  min-width: 1.6em;
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: var(--violet);
  color: #fff;
  font-size: 0.6rem;
  font-weight: 800;
  text-align: center;
}

/* The next-theme card before the rotation unlocks. Shown rather than
   hidden: the rail has a slot for it, and a level to play toward reads
   better than a gap. Muted so it does not compete with the live one. */
.next-theme.is-locked {
  border-color: var(--cab-edge);
  background: linear-gradient(180deg, rgba(12, 20, 32, 0.8), rgba(8, 12, 20, 0.9));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.next-theme.is-locked .next-theme-name { color: var(--muted, #9fb6cc); }
.next-theme.is-locked .next-theme-time {
  color: var(--muted, #9fb6cc);
  font-size: clamp(0.6rem, 0.95vmin, 1rem);
  letter-spacing: 0.12em;
}
/* No art to show, so the grid closes the column up rather than leaving
   an empty square where the picture would be. */
.next-theme.is-locked .next-theme-art { display: none; }
.next-theme.is-locked {
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas: "label" "name" "time";
  text-align: center;
}

/* =====================================================================
   THE SHOP TILE'S SHINE

   It was written for the old SHOP button, which was small. The gradient
   puts white from 40% to 60% of a background sized at 300%, i.e. a band
   0.6x the element's width, at 0.85 alpha — a quick glint across a
   120px button, but the tile is now a full rail card, so the same band
   is a white wash over most of it. With the 4.4s cycle from
   deck-info-boosts.css and keyframes that HOLD the end position from
   85% to 100%, it sat there looking stuck.

   Narrow, faint, quick, and mostly at rest: a glint is a highlight
   travelling over a surface, not a sheet laid across it.
   ===================================================================== */
.rail-offer .shop-cta-shine {
  background: linear-gradient(100deg,
    transparent 46%,
    rgba(255, 255, 255, 0.30) 50%,
    transparent 54%);
  background-size: 300% 100%;
  background-position: 200% 0;
  animation-name: railOfferShine;
  animation-duration: 5.2s;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  mix-blend-mode: screen;
}

/* The sweep is 12% of the cycle — about 0.6s — and the rest is genuinely
   idle with the band parked off the edge, rather than easing to a stop
   on top of the artwork. */
@keyframes railOfferShine {
  0%, 88%  { background-position: 200% 0; }
  100%     { background-position: -60% 0; }
}

/* Nothing moving at all where motion is unwanted. The base sheet already
   does this for .shop-cta-shine; restated because the rule above sets
   animation-name and would otherwise revive it. */
@media (prefers-reduced-motion: reduce) {
  .rail-offer .shop-cta-shine { animation: none; opacity: 0; }
}

/* =====================================================================
   READABILITY AND REACH — the last word, deliberately
   ---------------------------------------------------------------------
   This is the final stylesheet in the load order, which is the only
   reason these overrides take effect. The same rules written in an
   earlier sheet are silently beaten by the ones already downstream —
   that mistake cost a whole pass on the curtains.
   ===================================================================== */

/* --- THE BET LADDER, FREED FROM ITS CLIPPING ANCESTORS ---------------
   It was absolutely positioned inside .bet-stepper and .slot-machine,
   both of which are `overflow: hidden`. It opened correctly every time
   and was clipped away before it could be seen, so the button read as
   dead. Fixed positioning ignores ancestor overflow entirely; hud.js
   supplies the coordinates, since CSS has no way to say "under that
   button" once an element is out of flow. */
.bet-menu {
  position: fixed;
  /* Every one of these resets matters. The original rule anchored the
     menu to its BUTTON with `bottom: calc(100% + 8px)`, `left: 50%` and
     `transform: translateX(-50%)`. Switching to fixed re-points all
     three at the viewport instead:
       bottom   became 100% of the SCREEN, so with the top that hud.js
                sets, the box had both edges pinned and collapsed to a
                20px sliver — measured, before this line existed
       left     is supplied by hud.js and must not be fought over
       transform would drag the result half a menu-width to the left */
  inset: auto;
  bottom: auto;
  right: auto;
  transform: none;
  z-index: 10100;          /* over the control bar and the machine */
  max-height: min(60vh, 420px);
  overflow-y: auto;
}

/* --- THE JACKPOT LADDER ---------------------------------------------
   GRAND / MAJOR / MINOR were set at a size that made the biggest
   numbers in the game the smallest text on the screen. Scaled to the
   viewport so it fills the panel it is given. */
/* The banner heading was 0.62rem — under 10px — over the largest
   numbers on the screen. .meter-win was NOT this: that is the small
   "Win" readout in the deck, a different control entirely. */
.jackpot-banner-title {
  font-size: clamp(0.86rem, 1.7vw, 1.35rem);
  letter-spacing: 0.2em;
}
/* Room for the bigger type, and the ladder given the space it needs
   rather than the leftovers. */
.jackpot-banner {
  gap: clamp(5px, 0.9vh, 10px);
  padding: clamp(9px, 1.3vh, 15px) clamp(8px, 1vw, 14px) clamp(10px, 1.4vh, 16px);
}
.prize-row {
  font-size: clamp(0.82rem, 1.45vw, 1.2rem);
  padding-block: clamp(3px, 0.5vh, 7px);
}
.prize-row-tier {
  font-size: clamp(0.74rem, 1.2vw, 1.02rem);
  letter-spacing: 0.1em;
}
/* The figure itself is the point of the row — given the most room, and
   tabular so the three values line up as a column rather than drifting. */
.prize-row-value {
  font-size: clamp(0.92rem, 1.75vw, 1.42rem);
  font-variant-numeric: tabular-nums;
  font-weight: 700;
}

/* --- THE RAIL LABELS ------------------------------------------------
   INVENTORY, STATS and SHOP HERE! are all .rail-tile-label — one class,
   not the three different ones I first guessed at. This is the game's
   primary navigation, and navigation that has to be squinted at does
   not get used. */
.rail-tile-label {
  font-size: clamp(0.74rem, 1.25vw, 1.05rem);
  letter-spacing: 0.09em;
  font-weight: 700;
}

/* SHOP HERE! is the way into everything you can spend on, so it gets a
   step more than its neighbours rather than matching them. */
.rail-tile.shop .rail-tile-label,
#shopButton .rail-tile-label,
.rail-tile-label:is([data-shop], .shop-label) {
  font-size: clamp(0.86rem, 1.55vw, 1.25rem);
  letter-spacing: 0.12em;
}

/* --- EVERYTHING ELSE THAT WAS TOO SMALL ----------------------------- */
.control-bar .btn,
.cabinet-controls .btn,
.bag-tab,
.account-tab,
.shop-tab,
.shop-subtab {
  /* Capped at 0.92rem rather than 1rem: at the full 1rem the HOLD
     button's content ran 4px past its box. Measured, not guessed. */
  font-size: clamp(0.74rem, 1.05vw, 0.92rem);
  letter-spacing: 0.07em;
}

/* .hud-label was computing to 8.8px — small enough that SPINS and COINS
   were guesswork. Raised, but only to about 12px: the first attempt took
   it to 14.4px and pushed both HUD tiles 6px past their own width. The
   tile is sized around the big figure beside it, so the label has less
   room to grow than it looks. */
.hud-stat .hud-label,
.control-bar .hud-label,
.hud-label,
.meter-label {
  font-size: clamp(0.66rem, 0.85vw, 0.78rem);
  letter-spacing: 0.1em;
}

/* Lets the tiles absorb the slightly wider text instead of spilling. */
.hud-stat { min-width: 0; }
.hud-stat .hud-label { white-space: nowrap; }

/* Only genuinely small layouts lose her now. */
@media (max-width: 759px), (max-height: 559px) {
}

/* --- THE CONFETTI CANVAS --------------------------------------------
   Its own element rather than the library's, so it can be sized and
   stacked deliberately. The instance in fx/win-tiers.js is created with
   `resize: true`, which keeps the drawing buffer matched to the window;
   these two lines keep the ELEMENT matched to it as well.
   z-index sits above the machine and the edge pulse but below the gain
   tokens (1500) and the win text (10000), so a burst never obscures the
   number it is celebrating. */
.confetti-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 1400;
}

/* =====================================================================
   HIERARCHY PASS — the reels lead, everything else supports
   ===================================================================== */

/* --- THE MENU POPOVER ----------------------------------------------- */
.rail-menu { position: relative; }

.rail-menu-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 10100;          /* the rails sit low; this must clear them */
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: max(100%, 11rem);
  padding: 5px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--cyan, #22e3ff);
  background: rgba(4, 10, 18, 0.97);
  box-shadow: 0 10px 34px rgba(0, 0, 0, 0.75);
}
.rail-menu-list[hidden] { display: none !important; }

.rail-menu-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 11px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text, #e8eaed);
  font: inherit;
  font-size: clamp(0.74rem, 1.1vw, 0.95rem);
  letter-spacing: 0.07em;
  cursor: pointer;
  text-align: left;
  white-space: nowrap;
}
.rail-menu-item svg { width: 17px; height: 17px; flex: none; }
.rail-menu-item:hover { background: rgba(34, 227, 255, 0.14); }
.rail-menu-item:focus-visible { outline: 2px solid var(--cyan, #22e3ff); outline-offset: -2px; }

/* --- ONE PRIMARY GLOW, ONE ACCENT -----------------------------------
   The shop bar was neon green against yellow highlights and cyan
   borders — three competing glows, none of which read as "the important
   one". Cyan is the interface colour; amber is reserved for money.
   MENU and BOOSTS step back to plain surfaces so WHEEL and SHOP are the
   only two things on the rails that glow at all. */
.rail-tile-menu,
#boostsButton {
  border-color: rgba(255, 255, 255, 0.14);
  box-shadow: none;
}
.rail-tile-menu.has-items { border-color: var(--cyan, #22e3ff); }

.btn.wheel {
  border-color: var(--cyan, #22e3ff);
  box-shadow: 0 0 16px rgba(34, 227, 255, 0.34);
}

/* The shop keeps the accent, because it is the one about money — but
   amber, not green, so it belongs to the same family as the jackpot
   figures directly above it. */
.rail-offer { border-color: var(--gold, #ffd76a); }
.rail-offer-cta {
  background: linear-gradient(180deg, #ffcf5a, #f0a020);
  color: #2a1600;
  border-color: transparent;
  text-shadow: none;
  font-size: clamp(0.8rem, 1.45vw, 1.15rem);
  letter-spacing: 0.1em;
  font-weight: 800;
}
.rail-offer:hover { box-shadow: 0 0 22px rgba(255, 200, 80, 0.4); }

/* --- THE PRIZE LADDER, ALL FOUR TIERS, SHRUNK TO FIT -----------------
   MINI and MINOR stay. A ladder showing only its top half stops being a
   ladder — the small tiers are what give the big ones scale. The rows
   are tightened instead, and the figure keeps the size because the
   figure is the point. */
.prize-row {
  padding-block: clamp(1px, 0.28vh, 4px);
  gap: clamp(5px, 0.7vw, 9px);
  line-height: 1.12;
}
.prize-row img {
  width: clamp(19px, 2.1vw, 30px);
  height: clamp(19px, 2.1vw, 30px);
  /* Higher contrast so the icon reads against the banner rather than
     sinking into it, and sized to sit on the same baseline as its text
     instead of floating above it. */
  filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.9)) contrast(1.18) saturate(1.15);
  align-self: center;
}
.prize-row-tier { font-size: clamp(0.66rem, 1vw, 0.86rem); opacity: 0.92; }
.prize-row-value { font-size: clamp(0.86rem, 1.5vw, 1.22rem); }
.jackpot-banner-title { font-size: clamp(0.76rem, 1.4vw, 1.1rem); }

/* --- FONT SCALING SO FIGURES FIT THEIR BORDER -----------------------
   Long numbers (874m, 1.9b, 208b) used to run past the frames holding
   them. Every figure now shrinks to fit its own box rather than
   overflowing it, and never wraps mid-number. */
.prize-row-value,
.hud-value,
.deck-info-val,
.bet-current,
.balance,
#winMeter {
  min-width: 0;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* Let the figures inside the currency tiles shrink instead of pushing
   the tile wider. NOT `overflow: hidden` on the tile: the + button is
   `position: absolute; right: -6px` (deck-info-boosts.css) and hangs off
   the corner on purpose, so clipping the tile clips a live button. That
   overhang is the whole "6px overflow" -- it was never a bug. */
.hud-stat,
.hud-stat-body,
.hud-stat > * { min-width: 0; }

/* --- THE REELS ARE THE POINT ----------------------------------------
   The rails were competing with the machine for attention. They are
   support: still legible, no longer glowing at the same intensity as
   the thing being played. */
.rail { align-content: start; }
.rail-tile:not(.rail-offer) { background: rgba(255, 255, 255, 0.035); }
.machine-frame {
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.6),
    0 0 42px -6px rgba(34, 227, 255, 0.42),
    0 0 90px -26px rgba(168, 85, 247, 0.38) inset;
}

/* --- USE ALL ITEMS ---------------------------------------------------
   Sits above the list rather than beside the title so it reads as an
   action ON the list, and so both inventory containers (the shop section
   and the overlay) get it from the one render path. Amber, not the cyan
   the individual USE buttons wear: it does more than they do, and the
   armed state is loud on purpose. */
.bag-useall-row {
  display: flex;
  margin-bottom: 8px;
}
.bag-useall {
  flex: 1 1 auto;
  padding: 9px 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--gold-deep, #a97400);
  background: linear-gradient(180deg, #ffcf5a, #f0a020);
  color: #241500;
  font-family: inherit;
  font-weight: 800;
  font-size: clamp(0.66rem, 1.1vw, 0.82rem);
  letter-spacing: 1.4px;
  cursor: pointer;
  transition: filter 140ms ease, transform 120ms ease;
}
.bag-useall:hover:not(:disabled) { filter: brightness(1.08); }
.bag-useall:active:not(:disabled) { transform: translateY(1px); }
.bag-useall:disabled { opacity: 0.55; cursor: default; }

/* Armed: the next tap spends the inventory, so it stops looking like the
   same button it was a moment ago. */
.bag-useall.is-armed {
  background: linear-gradient(180deg, #ff8f6a, #e0452a);
  border-color: #8d2412;
  color: #fff3ee;
  animation: bagUseAllPulse 900ms ease-in-out infinite;
}
@keyframes bagUseAllPulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(224, 69, 42, 0.55); }
  50%      { box-shadow: 0 0 14px 3px rgba(224, 69, 42, 0.55); }
}
@media (prefers-reduced-motion: reduce) {
  .bag-useall.is-armed { animation: none; }
}

/* =====================================================================
   THE SHOP CARD
   Everything below lands last on purpose: 25 sheets style these cards
   and this is the only place an override survives the cascade.
   ===================================================================== */

/* --- LESS CHROME ABOVE THE GOODS ------------------------------------
   The shop opened on 271px of heading before a single product. The
   standing paragraph is gone from the markup; the category blurb now
   only speaks while searching, where it carries the match count. */
.shop-tab-blurb:empty { display: none; margin: 0; }
.shop-header { padding-block: 10px; }
.shop-tabs { margin-block: 4px; }
.shop-search { margin-block: 6px; }

/* --- RARITY RIBBON ---------------------------------------------------
   Top-left, opposite the info button. EVERY card carries one now, not
   just the chests, and the rank is derived from the item's price by the
   server so the ribbon and the number on the buy button can never
   disagree.

   ONE TREATMENT, FOUR COLOURS. Each tier gets the identical extruded
   text, the identical sweep and the identical spark; only the four
   custom properties below change between them. That is deliberate — if
   LEGENDARY had a grander animation than COMMON the animation would be
   doing the ranking, and then the colour and the word would be
   decoration rather than information. The tier is told by the palette
   and the word; the treatment is the shop's house style.

   The old scale had five ranks including UNCOMMON and was declared by
   hand on each chest. Four now, named by the player, derived from one
   price ladder.
   --------------------------------------------------------------------- */
.shop-card-ribbon {
  --rar-a: #b8c6d6;
  --rar-b: #7e8fa3;
  --rar-ink: #10131a;
  --rar-glow: rgba(180, 200, 220, 0.5);

  position: absolute;
  top: 10px;
  left: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  padding: 4px 11px 4px 9px;
  border-radius: 0 5px 5px 0;
  background: linear-gradient(180deg, var(--rar-a), var(--rar-b));
  box-shadow:
    0 1px 5px rgba(0, 0, 0, 0.55),
    0 0 14px var(--rar-glow),
    inset 0 1px 0 rgba(255, 255, 255, 0.45),
    inset 0 -1px 0 rgba(0, 0, 0, 0.28);
  pointer-events: none;
  overflow: hidden;
  isolation: isolate;
}

/* THE 3D TEXT. The extrusion is a stack of 1px shadows stepping down and
   right, capped with a light edge on top — cheap, and it survives being
   rendered at 0.54rem where a real transform would just blur. */
.shop-card-ribbon-text {
  position: relative;
  z-index: 2;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.54rem;
  font-weight: 800;
  letter-spacing: 1.3px;
  line-height: 1;
  color: var(--rar-ink);
  text-shadow:
    0 -1px 0 rgba(255, 255, 255, 0.55),
    0  1px 0 rgba(0, 0, 0, 0.30),
    0  2px 0 rgba(0, 0, 0, 0.22),
    0  3px 3px rgba(0, 0, 0, 0.35);
}

/* THE SWEEP. A band of light crossing the ribbon every few seconds. On
   ::before of the ribbon so it sits under the text but over the fill. */
.shop-card-ribbon::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(100deg,
    transparent 0%, transparent 38%,
    rgba(255, 255, 255, 0.75) 48%,
    rgba(255, 255, 255, 0.28) 55%,
    transparent 66%, transparent 100%);
  transform: translateX(-120%);
  animation: rarSweep 3800ms ease-in-out infinite;
}
@keyframes rarSweep {
  0%,  62% { transform: translateX(-120%); }
  86%      { transform: translateX(120%); }
  100%     { transform: translateX(120%); }
}

/* THE PARTICLES. Three sparks rising off the ribbon, built from one
   element with two pseudo-elements so no card gains three extra nodes.
   They escape the ribbon's own overflow by living outside it. */
.shop-card-ribbon-spark {
  position: absolute;
  left: 78%;
  bottom: 2px;
  z-index: 2;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 0 6px 1px var(--rar-glow);
  animation: rarSpark 2600ms ease-out infinite;
}
.shop-card-ribbon-spark::before,
.shop-card-ribbon-spark::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: inherit;
  box-shadow: inherit;
  animation: rarSpark 2600ms ease-out infinite;
}
.shop-card-ribbon-spark::before { animation-delay: 870ms;  left: -26px; }
.shop-card-ribbon-spark::after  { animation-delay: 1740ms; left: -52px; }
@keyframes rarSpark {
  0%   { opacity: 0; transform: translate(0, 0) scale(0.6); }
  18%  { opacity: 1; }
  100% { opacity: 0; transform: translate(3px, -16px) scale(0.2); }
}

/* The four tiers. Only these four properties differ. */
.shop-card-ribbon[data-rank="1"] {          /* COMMON — worn steel */
  --rar-a: #cdd8e4; --rar-b: #8a9bad; --rar-ink: #161c24;
  --rar-glow: rgba(190, 210, 230, 0.42);
}
.shop-card-ribbon[data-rank="2"] {          /* RARE — cold blue */
  --rar-a: #86d8ff; --rar-b: #2a7fc4; --rar-ink: #04202f;
  --rar-glow: rgba(90, 190, 255, 0.6);
}
.shop-card-ribbon[data-rank="3"] {          /* EPIC — violet */
  --rar-a: #e0b3ff; --rar-b: #8b46d6; --rar-ink: #22062f;
  --rar-glow: rgba(180, 90, 255, 0.65);
}
.shop-card-ribbon[data-rank="4"] {          /* LEGENDARY — molten gold */
  --rar-a: #ffe08a; --rar-b: #e08a12; --rar-ink: #2e1a00;
  --rar-glow: rgba(255, 190, 60, 0.75);
}

/* The card border picks up its own tier, so the ranking reads from
   across the grid and not only from the corner label. The chest-specific
   border colours further down are more specific and still win on those
   cards, which is intended — a Bronze Chest should look bronze. */
.shop-card:has(.shop-card-ribbon[data-rank="2"]) { border-color: rgba(90, 190, 255, 0.55); }
.shop-card:has(.shop-card-ribbon[data-rank="3"]) { border-color: rgba(180, 90, 255, 0.55); }
.shop-card:has(.shop-card-ribbon[data-rank="4"]) { border-color: rgba(255, 190, 60, 0.7); }

@media (prefers-reduced-motion: reduce) {
  .shop-card-ribbon::before,
  .shop-card-ribbon-spark,
  .shop-card-ribbon-spark::before,
  .shop-card-ribbon-spark::after { animation: none; }
  .shop-card-ribbon-spark { opacity: 0; }
}
/* Forty animated ribbons is forty compositor layers. On the lean path
   the ribbon keeps its colour and its 3D text and loses the motion. */
.lean-fx .shop-card-ribbon::before { animation: none; opacity: 0; }
.lean-fx .shop-card-ribbon-spark { display: none; }

/* =====================================================================
   ONE RHYTHM FOR EVERY CARD
   ---------------------------------------------------------------------
   The complaint was that the buttons and the information do not sit at
   the same level from card to card, and the cause is that a shop card is
   assembled from five OPTIONAL blocks: the loot strip (chests only), the
   quantity picker (bulkable items only), the "next: N" step note (two
   chests only), the lock badge and the ribbon. A card missing three of
   them is three blocks shorter, so its buy button floats up the card
   while its neighbour's sits at the bottom.

   Grid stretch was already equalising the cards WITHIN a row, which is
   why this looked intermittent rather than broken — it lined up until a
   row happened to contain only short cards.

   Three rules fix it, and they have to work together:

     1. grid-auto-rows gives every row the same minimum, so cards match
        across the whole list and not just across a row.
     2. The icon, the label and the blurb get RESERVED heights, so the
        top of every card keeps step regardless of how long the name is
        or whether the blurb wraps to two lines.
     3. The buy button takes the slack with margin-top:auto, so whatever
        optional blocks a card does or does not have, the price is
        pinned to the bottom edge. That is the row the eye actually
        scans, and it is now the row that is guaranteed to line up.

   The step note is always in the markup now (see renderShopGrid) and
   holds one line whether or not it has anything to say, so a card that
   gains an escalation does not shove its own button upward.
   ===================================================================== */
.shop-grid {
  grid-auto-rows: minmax(var(--shop-card-min-h, 268px), auto);
  align-items: stretch;
}

.shop-card {
  /* The card is the flex column the rhythm below depends on. Restated
     here because .shop-card-buy's margin-top:auto is meaningless unless
     this element is the flex container the slack belongs to. */
  display: flex;
  flex-direction: column;
  position: relative;
}

.shop-card-icon {
  flex: 0 0 auto;
  height: 46px;
  display: grid;
  place-items: center;
}
.shop-card-icon img { max-height: 44px; }

.shop-card-label {
  flex: 0 0 auto;
  /* Two lines reserved. "High Roller Pass" wraps and "XP Chest" does
     not, and without this the one-line cards start their blurb 18px
     higher than the two-line ones. */
  min-height: 2.4em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.2;
}

/* --- WHAT IS ACTUALLY IN THERE --------------------------------------
   Fills the hole the blurb used to stretch across. The blurb no longer
   absorbs the card's slack; the buy button does, so the middle of the
   card holds the guaranteed contents instead of nothing. */
.shop-card-blurb {
  flex: 0 0 auto;
  min-height: 3em;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.shop-qty { flex: 0 0 auto; }

/* THE SLACK-TAKER. Every card's spare height collects above this button
   and nowhere else, which is what puts the price row of every card on
   the same line.

   min-height because the button's own height was varying by 2px between
   a coin price and a spin price — the two inline currency icons do not
   measure the same — and since the step note below pins the bottom, a
   2px taller button starts 2px higher. Fixing the icons alone would work
   until the next icon; a floor on the button holds whatever goes in it. */
.shop-card-buy {
  flex: 0 0 auto;
  margin-top: auto;
  min-height: 40px;
}
.shop-card-buy .inline-coin { width: 15px; height: 15px; object-fit: contain; }

/* One reserved line, occupied or not. Empty on most cards; present on
   all of them, so the two chests that escalate do not sit a row lower
   than everything else. */
.shop-card-step {
  flex: 0 0 auto;
  min-height: 1.05em;
  margin-top: 0.4rem;
}
.shop-card-loot {
  /* WAS flex: 1 1 auto, and that is what tilted the grid: the loot strip
     swallowed all of a chest card's slack while a card without one had
     nothing to swallow it, so the chest's buy button sank to the bottom
     and everything else's floated. The slack belongs to exactly one
     element on every card, and that element is the buy button. */
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: stretch;
  gap: 4px;
  width: 100%;
  margin-block: 2px;
}
.shop-card-loot-item {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 3px 7px;
  border-radius: var(--radius-xs);
  background: rgba(255, 255, 255, 0.055);
  border: 1px solid rgba(255, 255, 255, 0.08);
  font-size: 0.66rem;
  font-weight: 700;
  color: #e9dfe2;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.shop-card-loot-item img { width: 15px; height: 15px; object-fit: contain; flex: 0 0 auto; }
.shop-card-loot-item.is-maybe {
  background: none;
  border-style: dashed;
  border-color: rgba(255, 255, 255, 0.14);
  color: #b9a7ad;
  font-weight: 600;
}

/* --- THE BUTTON SAYS WHY ---------------------------------------------
   The blocked reason used to sit on its own line below a disabled button
   showing a price, which left the player joining the two up. */
.shop-card-buy.says-why {
  font-size: clamp(0.6rem, 1vw, 0.72rem);
  letter-spacing: 0.4px;
  line-height: 1.2;
  white-space: normal;
  padding-inline: 8px;
  /* TWO LINES, AND NOT A PIXEL MORE. The longest reasons — "Your spins
     are already full", "Only spins you have earned can be cashed in" —
     wrap to a third line in a narrow column and made the button 5px
     taller than its neighbour's, which lifted it off the shared price
     line. Capping the box and clamping the text keeps every button the
     same 40px whatever it has to say. */
  max-height: 40px;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  text-align: center;
}

/* --- LOCKED CARDS ----------------------------------------------------
   One badge, not two: the button already reads LEVEL n, so the corner
   badge says what state the card is in rather than repeating the number.
   Held at 40% so the card reads as out of reach at a glance while still
   showing what is waiting up there. */
.shop-card.is-level-locked { opacity: 0.4; }
.shop-card.is-level-locked:hover { opacity: 0.62; }
.shop-card-lock {
  position: absolute;
  top: 10px;
  left: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  gap: 5px;
  padding: 3px 9px 3px 8px;
  border-radius: 0 4px 4px 0;
  font-size: 0.54rem;
  font-weight: 800;
  letter-spacing: 1.3px;
  line-height: 1;
  text-transform: uppercase;
  color: #f4e9d8;
  background: rgba(20, 12, 6, 0.92);
  border: 1px solid rgba(255, 200, 80, 0.45);
  border-left: none;
}
.shop-card-lock-icon { font-size: 0.7rem; line-height: 1; }
/* A locked card carries BOTH corner labels, so the ribbon steps down to
   sit under the lock rather than through it. */
.shop-card.is-level-locked .shop-card-ribbon { top: 34px; }

/* --- EVERY CHEST GETS THE GLOW --------------------------------------
   The bloom was on the two rarest only, which made the other nine read
   as a different, lesser kind of object. Each tier now gets the same
   treatment in its own colour, and the rare ones keep their motes. */
.shop-card[data-chest]::before {
  content: '';
  position: absolute;
  inset: -8% 8% 32%;
  z-index: 0;
  border-radius: 50%;
  background: radial-gradient(ellipse at 50% 45%, var(--chest-glow, rgba(255, 255, 255, 0.2)), transparent 70%);
  filter: blur(12px);
  opacity: 0.75;
  pointer-events: none;
}
.shop-card[data-chest="bronze"] { --chest-glow: rgba(205, 130, 70, 0.5); }
.shop-card[data-chest="silver"] { --chest-glow: rgba(200, 215, 230, 0.45); }
.shop-card[data-chest="gold"]   { --chest-glow: rgba(255, 195, 60, 0.5); }
.shop-card[data-chest="energy"] { --chest-glow: rgba(120, 210, 255, 0.45); }
.shop-card[data-chest="royal"]  { --chest-glow: rgba(160, 130, 255, 0.45); }
.shop-card[data-chest="joker"]  { --chest-glow: rgba(120, 230, 160, 0.42); }
.shop-card[data-chest="haunted"]{ --chest-glow: rgba(150, 105, 220, 0.5); }
.shop-card[data-chest="vegas"]  { --chest-glow: rgba(255, 120, 190, 0.45); }
.shop-card[data-chest="mystery"]{ --chest-glow: rgba(255, 235, 140, 0.45); }
.shop-card[data-chest="mythic"] { --chest-glow: rgba(190, 110, 255, 0.55); }
.shop-card[data-chest="jk1x"]   { --chest-glow: rgba(255, 110, 80, 0.55); }
/* The icon and everything under it must clear the bloom. */
.shop-card[data-chest] > .shop-card-icon,
.shop-card[data-chest] > .shop-card-label,
.shop-card[data-chest] > .shop-card-blurb,
.shop-card[data-chest] > .shop-card-loot { position: relative; z-index: 2; }
@media (prefers-reduced-motion: reduce) {
  .shop-card[data-chest]::before { filter: blur(10px); }
}

/* --- THE SPIN CEILING STOPS BEING SHOWN ONCE IT IS PASSED ------------
   maxSpinsForLevel is a REGENERATION ceiling: spins stop accruing at it,
   but bought and won spins deliberately go past it (the server has a
   note saying so). The figure was rendered "2000/462" regardless, which
   reads as a cap being violated rather than a limit that no longer
   applies — and invites the obvious question of why the game is letting
   you exceed its own maximum.
   .overflowing is already set for exactly this state; it only restyled
   the colour. Above the ceiling the count now stands on its own, and
   the "/ N" comes back the moment the pile drops back under it. */
.hud-stat.overflowing .hud-max,
.shop-wallet-item.overflowing .hud-max { display: none; }

.sound-group {
  background: rgba(4, 10, 18, 0.55);
  border: 1px solid var(--cab-edge, rgba(120, 190, 220, 0.18));
  border-radius: var(--radius-md);
  padding: 2px 12px;
  margin-bottom: 10px;
}
.sound-group-master {
  background: rgba(10, 26, 36, 0.72);
  border-color: rgba(34, 227, 255, 0.34);
  box-shadow: inset 0 0 22px rgba(34, 227, 255, 0.07);
}

.sound-row {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 7px;
  padding: 10px 0;
  border-bottom: 1px solid rgba(120, 190, 220, 0.11);
}
.sound-row:last-child { border-bottom: none; }

.sound-row-label {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-weight: 700;
  font-size: 0.78rem;
  color: #eaf6ff;
  letter-spacing: 0.02em;
}
.sound-row-label svg { width: 16px; height: 16px; flex: 0 0 auto; color: var(--cyan-bright, #7bf3ff); }
.sound-row-label span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.sound-row-parent .sound-row-label { font-size: 0.88rem; }
.sound-group-master .sound-row-label { font-size: 0.92rem; color: #ffffff; }

.sound-row-child .sound-row-label {
  font-size: 0.72rem;
  font-weight: 600;
  color: var(--cyan-bright, #7bf3ff);
  padding-left: 16px;
  position: relative;
}
.sound-row-child .sound-row-label::before {
  content: '';
  position: absolute;
  left: 3px;
  top: 50%;
  width: 7px;
  height: 1px;
  background: rgba(123, 243, 255, 0.42);
}

.sound-row-controls {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}

.sound-mute-btn {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 34px;
  height: 34px;
  padding: 0;
  border-radius: var(--radius-pill);
  border: 1px solid rgba(34, 227, 255, 0.34);
  background: rgba(34, 227, 255, 0.09);
  display: grid;
  place-items: center;
  cursor: pointer;
  line-height: 1;
  transition: border-color 160ms ease, background 160ms ease, opacity 160ms ease;
  touch-action: manipulation;
}
.sound-mute-icon { font-size: 15px; line-height: 1; }
.sound-mute-btn:hover { border-color: var(--cyan-bright, #7bf3ff); background: rgba(34, 227, 255, 0.16); }
.sound-mute-btn.is-muted {
  border-color: rgba(255, 120, 120, 0.5);
  background: rgba(255, 90, 90, 0.1);
  opacity: 0.75;
}

.sound-pct {
  flex: 0 0 auto;
  min-width: 3.1em;
  text-align: right;
  font-size: 0.68rem;
  font-variant-numeric: tabular-nums;
  color: var(--cyan-bright, #7bf3ff);
}
.sound-row.is-muted .sound-pct { color: rgba(255, 150, 150, 0.8); }

.sound-slider {
  -webkit-appearance: none;
  appearance: none;
  flex: 1 1 auto;
  min-width: 80px;
  height: 30px;
  margin: 0;
  padding: 0;
  background: transparent;
  cursor: pointer;
  touch-action: manipulation;
}
.sound-slider:focus { outline: none; }
.sound-slider:focus-visible { outline: 2px solid var(--cyan-bright, #7bf3ff); outline-offset: 3px; border-radius: var(--radius-pill); }

.sound-slider::-webkit-slider-runnable-track {
  height: 5px;
  border-radius: var(--radius-pill);
  background: rgba(120, 190, 220, 0.22);
  border: none;
}
.sound-slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 17px;
  height: 17px;
  margin-top: -6px;
  border-radius: 50%;
  background: var(--cyan-bright, #7bf3ff);
  border: 2px solid #05202b;
  box-shadow: 0 0 8px rgba(34, 227, 255, 0.55);
  transition: transform 120ms ease;
}
.sound-slider:active::-webkit-slider-thumb { transform: scale(1.15); }

.sound-slider::-moz-range-track {
  height: 5px;
  border-radius: var(--radius-pill);
  background: rgba(120, 190, 220, 0.22);
  border: none;
}
.sound-slider::-moz-range-progress {
  height: 5px;
  border-radius: var(--radius-pill);
  background: rgba(34, 227, 255, 0.55);
}
.sound-slider::-moz-range-thumb {
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background: var(--cyan-bright, #7bf3ff);
  border: 2px solid #05202b;
  box-shadow: 0 0 8px rgba(34, 227, 255, 0.55);
}

.sound-row.is-muted .sound-slider::-webkit-slider-thumb {
  background: #8ea3b2;
  box-shadow: none;
}
.sound-row.is-muted .sound-slider::-moz-range-thumb {
  background: #8ea3b2;
  box-shadow: none;
}

@media (pointer: coarse) {
  .sound-row { padding: 13px 0; }
  .sound-mute-btn { width: 40px; height: 40px; }
  .sound-mute-icon { font-size: 17px; }
  .sound-slider { height: 40px; }
  .sound-slider::-webkit-slider-thumb { width: 22px; height: 22px; margin-top: -8.5px; }
  .sound-slider::-moz-range-thumb { width: 20px; height: 20px; }
}

@media (max-width: 430px) {
  .sound-row-child .sound-row-label { padding-left: 12px; }
}

.top-pills .hud-stat,
.top-pills .daily-timer {
  justify-content: flex-start;
  gap: 10px;
}
.top-pills .hud-actions {
  margin-left: auto;
  align-items: flex-end;
}

.top-pills .hud-plus {
  position: static;
  right: auto;
  left: auto;
  top: auto;
  bottom: auto;
  margin: 0;
  align-self: flex-end;
  flex: 0 0 auto;
}

.top-pills .hud-spins:not(.refill-armed) .hud-value,
.top-pills .hud-coins .hud-value,
.daily-timer-value {
  font-size: clamp(1.08rem, 1.55vmin, 23px);
}
@media (max-width: 720px) {
  .top-pills .hud-spins:not(.refill-armed) .hud-value,
  .top-pills .hud-coins .hud-value,
  .daily-timer-value { font-size: 1rem; }
}

.top-pills .hud-label,
.daily-timer-label {
  color: var(--cyan-bright, #7bf3ff);
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.top-pills .hud-value,
.top-pills .hud-spins .hud-value,
.top-pills .hud-coins .hud-value,
.daily-timer-value {
  color: var(--gold-bright, #ffd94d);
}

.top-pills .hud-max,
.top-pills .hud-regen,
.daily-timer-note {
  color: rgba(190, 214, 230, 0.72);
  font-weight: 600;
}

.daily-timer-icon {
  width: 26px;
  height: 26px;
  filter: saturate(1.2) brightness(1.14) contrast(1.06)
          drop-shadow(0 0 6px rgba(255, 198, 88, 0.5));
}
.daily-timer.is-ready .daily-timer-icon {
  filter: saturate(1.4) brightness(1.28) contrast(1.08)
          drop-shadow(0 0 11px rgba(255, 212, 116, 0.9));
}

.refill-pill.is-full,
.refill-pill:disabled { opacity: 0.55; }

.top-pills .hud-stat,
.top-pills .daily-timer { flex: 0 1 auto; }

.top-pills .daily-timer-label {
  color: var(--cyan-bright, #7bf3ff);
  font-weight: 700;
  letter-spacing: 0.1em;
}
.top-pills .daily-timer-value { color: var(--gold-bright, #ffd94d); }
.top-pills .daily-timer-note { color: rgba(190, 214, 230, 0.72); }

.top-pills .daily-timer-value { font-size: clamp(1.08rem, 1.55vmin, 23px); }
@media (max-width: 720px) {
  .top-pills .daily-timer-value { font-size: 1rem; }
}

.chest-headline {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  margin-bottom: 14px;
}
.chest-headline[hidden] { display: none; }

.chest-headline-main {
  font-family: 'Orbitron', sans-serif;
  font-weight: 900;
  font-size: clamp(1.5rem, 4.6vw, 2.5rem);
  line-height: 1.05;
  letter-spacing: 0.02em;
  background: linear-gradient(180deg, #fff6d2 0%, #ffd94d 42%, #f5a623 78%, #c9761a 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  filter: drop-shadow(0 3px 0 rgba(120, 62, 0, 0.55))
          drop-shadow(0 8px 22px rgba(255, 178, 44, 0.5));
  animation: chestHeadlinePop 620ms cubic-bezier(0.18, 1.25, 0.32, 1.15) both;
}
@keyframes chestHeadlinePop {
  0%   { opacity: 0; transform: scale(0.72) translateY(14px); }
  60%  { opacity: 1; transform: scale(1.06) translateY(0); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}

.chest-headline-sub {
  max-width: 30ch;
  font-size: clamp(0.8rem, 2.4vw, 0.98rem);
  font-weight: 500;
  line-height: 1.45;
  letter-spacing: 0.01em;
  text-transform: none;
  color: #e8dfe6;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.7);
  animation: chestSubFade 620ms ease 180ms both;
}
@keyframes chestSubFade {
  from { opacity: 0; transform: translateY(7px); }
  to   { opacity: 1; transform: translateY(0); }
}

.chest-bokeh {
  position: absolute;
  inset: -12%;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}
.chest-bokeh::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 44%;
  width: 130%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle,
    rgba(255, 214, 130, 0.30) 0%,
    rgba(255, 178, 44, 0.16) 28%,
    rgba(255, 150, 30, 0.06) 46%,
    transparent 66%);
  filter: blur(14px);
  animation: chestGlowBreathe 4.6s ease-in-out infinite;
}
@keyframes chestGlowBreathe {
  0%, 100% { opacity: 0.75; transform: translate(-50%, -50%) scale(1); }
  50%      { opacity: 1;    transform: translate(-50%, -50%) scale(1.09); }
}
.chest-dust {
  position: absolute;
  bottom: -8%;
  width: var(--dust-size, 5px);
  height: var(--dust-size, 5px);
  border-radius: 50%;
  background: radial-gradient(circle, #fff7dc 0%, rgba(255, 205, 90, 0.9) 45%, transparent 72%);
  filter: blur(0.4px);
  opacity: 0;
  animation: chestDustRise var(--dust-dur, 7s) linear var(--dust-delay, 0s) infinite;
}
@keyframes chestDustRise {
  0%   { transform: translateY(0) translateX(0) scale(0.6); opacity: 0; }
  14%  { opacity: var(--dust-peak, 0.7); }
  78%  { opacity: var(--dust-peak, 0.7); }
  100% { transform: translateY(-118%) translateX(var(--dust-drift, 20px)) scale(1); opacity: 0; }
}

.chest-ribbons {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}
.chest-ribbon {
  position: absolute;
  top: -10px;
  width: 128px;
  height: 128px;
  background:
    repeating-linear-gradient(58deg,
      rgba(34, 227, 255, 0.55) 0 3px, transparent 3px 13px),
    repeating-linear-gradient(-42deg,
      rgba(255, 214, 130, 0.55) 0 3px, transparent 3px 15px);
  -webkit-mask-image: radial-gradient(circle at 50% 0%, #000 0%, transparent 72%);
          mask-image: radial-gradient(circle at 50% 0%, #000 0%, transparent 72%);
  opacity: 0.55;
}
.chest-ribbon-tl { left: -18px; animation: chestRibbonDrift 7s ease-in-out infinite; }
.chest-ribbon-tr { right: -18px; animation: chestRibbonDriftMirror 7s ease-in-out -3.5s infinite; }
@keyframes chestRibbonDrift {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(7px) rotate(2.5deg); }
}
@keyframes chestRibbonDriftMirror {
  0%, 100% { transform: scaleX(-1) translateY(0) rotate(0deg); }
  50%      { transform: scaleX(-1) translateY(7px) rotate(2.5deg); }
}

.chest-overlay.is-gift .chest-rays { opacity: 0.35; }
.chest-overlay.is-gift .chest-bokeh::before {
  background: radial-gradient(circle,
    rgba(255, 224, 150, 0.42) 0%,
    rgba(255, 186, 60, 0.24) 26%,
    rgba(255, 140, 30, 0.09) 48%,
    transparent 68%);
}

.chest-rewards.is-bursting .gift-icon {
  animation: chestRewardBurst 760ms cubic-bezier(0.16, 1.1, 0.3, 1.18) both;
}
@keyframes chestRewardBurst {
  0%   { opacity: 0; transform: translateY(74px) scale(0.34); filter: blur(3px); }
  46%  { opacity: 1; transform: translateY(-16px) scale(1.14); filter: blur(0); }
  72%  { transform: translateY(4px) scale(0.97); }
  100% { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}

.gift-icon {
  position: relative;
  isolation: isolate;
  background: linear-gradient(180deg, rgba(58, 44, 12, 0.92), rgba(20, 13, 4, 0.95));
  border: 1px solid rgba(255, 217, 77, 0.6);
  box-shadow:
    inset 0 1px 0 rgba(255, 245, 210, 0.4),
    inset 0 -10px 20px rgba(0, 0, 0, 0.55),
    0 8px 22px rgba(0, 0, 0, 0.6),
    0 0 26px rgba(255, 190, 70, 0.28);
}
.gift-icon::before {
  content: '';
  position: absolute;
  inset: -2px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from 0deg,
    rgba(255, 236, 170, 0.9), rgba(255, 170, 40, 0.25),
    rgba(255, 236, 170, 0.9), rgba(255, 170, 40, 0.25), rgba(255, 236, 170, 0.9));
  animation: giftIconShimmer 3.4s linear infinite;
  opacity: 0.75;
}
@keyframes giftIconShimmer { to { transform: rotate(360deg); } }
.gift-icon::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  background: linear-gradient(160deg,
    rgba(255, 255, 255, 0.22) 0%,
    rgba(255, 255, 255, 0.05) 26%,
    transparent 52%);
}

.gift-icon-art img {
  filter:
    drop-shadow(0 2px 0 rgba(120, 70, 0, 0.6))
    drop-shadow(0 5px 10px rgba(0, 0, 0, 0.65))
    saturate(1.18) contrast(1.06) brightness(1.06);
}

.gift-icon-amount {
  font-weight: 900;
  letter-spacing: 0.01em;
  background: linear-gradient(180deg, #fffbe8 0%, #ffd94d 48%, #f0a417 100%);
  -webkit-background-clip: text;
          background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  filter:
    drop-shadow(0 2px 0 rgba(110, 58, 0, 0.65))
    drop-shadow(0 6px 14px rgba(255, 178, 44, 0.45));
}

.chest-done,
.daily-gift-collect {
  border: none;
  background: linear-gradient(180deg, #ffe9a8 0%, #ffc23d 34%, #f7921e 72%, #d96f0c 100%);
  color: #34190a;
  text-shadow: 0 1px 0 rgba(255, 245, 210, 0.6);
  transition: transform 140ms ease, box-shadow 200ms ease, filter 160ms ease;
  animation: ctaPulse 2.4s ease-in-out infinite;
}
@keyframes ctaPulse {
  0%, 100% {
    box-shadow:
      inset 0 1px 0 rgba(255, 252, 235, 0.85),
      inset 0 -3px 0 rgba(150, 74, 4, 0.55),
      0 8px 22px rgba(0, 0, 0, 0.55),
      0 0 22px rgba(255, 176, 48, 0.38);
  }
  50% {
    box-shadow:
      inset 0 1px 0 rgba(255, 252, 235, 0.85),
      inset 0 -3px 0 rgba(150, 74, 4, 0.55),
      0 8px 22px rgba(0, 0, 0, 0.55),
      0 0 40px rgba(255, 196, 74, 0.75);
  }
}
.chest-done:hover,
.daily-gift-collect:hover {
  transform: translateY(-2px) scale(1.04);
  filter: brightness(1.07);
}
.chest-done:active,
.daily-gift-collect:active {
  transform: translateY(1px) scale(0.985);
  animation: none;
  box-shadow:
    inset 0 3px 8px rgba(120, 58, 0, 0.5),
    0 3px 10px rgba(0, 0, 0, 0.5);
}

@media (prefers-reduced-motion: reduce) {
  .chest-headline-main,
  .chest-headline-sub,
  .chest-rewards.is-bursting .gift-icon,
  .chest-bokeh::before,
  .chest-ribbon-tl,
  .chest-ribbon-tr,
  .gift-icon::before,
  .chest-done,
  .daily-gift-collect { animation: none; }
  .chest-dust { display: none; }
}

.top-pills .hud-plus {
  position: relative;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  font-size: 0;
  color: transparent;
  border: 2px solid #ffeaa8;
  background:
    radial-gradient(circle at 50% 30%,
      #fff6cf 0%,
      #ffd94d 36%,
      #f7a91f 68%,
      #d97b06 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.95),
    inset 0 -4px 7px rgba(148, 72, 4, 0.55),
    0 3px 9px rgba(0, 0, 0, 0.55),
    0 0 14px rgba(255, 186, 52, 0.55);
  animation: hudPlusPulse 2.6s ease-out infinite;
  transition: transform 140ms ease, filter 160ms ease;
}

.top-pills .hud-plus::before,
.top-pills .hud-plus::after {
  content: '';
  grid-area: 1 / 1;
  background: #46220a;
  border-radius: 2px;
  box-shadow: 0 1px 0 rgba(255, 244, 205, 0.6);
}
.top-pills .hud-plus::before { width: 17px; height: 4px; }
.top-pills .hud-plus::after  { width: 4px;  height: 17px; }

@keyframes hudPlusPulse {
  0% {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.95),
      inset 0 -4px 7px rgba(148, 72, 4, 0.55),
      0 3px 9px rgba(0, 0, 0, 0.55),
      0 0 14px rgba(255, 186, 52, 0.5),
      0 0 0 0 rgba(255, 190, 60, 0.6);
  }
  70% {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.95),
      inset 0 -4px 7px rgba(148, 72, 4, 0.55),
      0 3px 9px rgba(0, 0, 0, 0.55),
      0 0 18px rgba(255, 200, 70, 0.75),
      0 0 0 11px rgba(255, 190, 60, 0);
  }
  100% {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.95),
      inset 0 -4px 7px rgba(148, 72, 4, 0.55),
      0 3px 9px rgba(0, 0, 0, 0.55),
      0 0 14px rgba(255, 186, 52, 0.5),
      0 0 0 0 rgba(255, 190, 60, 0);
  }
}

.top-pills .hud-plus:hover {
  transform: scale(1.14);
  filter: brightness(1.08);
}
.top-pills .hud-plus:active {
  transform: scale(0.94);
  animation: none;
  box-shadow:
    inset 0 3px 7px rgba(120, 58, 0, 0.6),
    0 2px 5px rgba(0, 0, 0, 0.5);
}

.top-pills .hud-spins.refill-armed .hud-plus {
  width: 26px;
  height: 26px;
  border-width: 2px;
}
.top-pills .hud-spins.refill-armed .hud-plus::before { width: 13px; height: 3px; }
.top-pills .hud-spins.refill-armed .hud-plus::after  { width: 3px;  height: 13px; }

@media (pointer: coarse) {
  .top-pills .hud-plus { width: 42px; height: 42px; }
  .top-pills .hud-plus::before { width: 21px; height: 5px; }
  .top-pills .hud-plus::after  { width: 5px;  height: 21px; }
  .top-pills .hud-spins.refill-armed .hud-plus { width: 32px; height: 32px; }
}

@media (max-width: 720px) {
  .top-pills .hud-plus { width: 30px; height: 30px; }
  .top-pills .hud-plus::before { width: 15px; height: 4px; }
  .top-pills .hud-plus::after  { width: 4px;  height: 15px; }
  .top-pills .hud-spins.refill-armed .hud-plus { width: 24px; height: 24px; }
}

@media (prefers-reduced-motion: reduce) {
  .top-pills .hud-plus { animation: none; }
}

.spin-btn .spin-btn-face {
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  box-sizing: border-box;
  position: relative;
  isolation: isolate;
  border: none;
  border-radius: 50%;
  background:
    repeating-conic-gradient(from 0deg,
      #6f4207 0deg 2.2deg,
      #dda63d 2.2deg 4.4deg),
    radial-gradient(circle, #c98a1c 0%, #865006 100%);
  box-shadow:
    0 2px 0 rgba(255, 242, 205, 0.3),
    inset 0 0 0 1px rgba(48, 22, 0, 0.6);
  font-family: 'Orbitron', sans-serif;
  font-weight: 900;
  font-size: clamp(0.8rem, 2.3vmin, 1.7rem);
  letter-spacing: 0.1em;
  text-indent: 0.1em;
  line-height: 1;
  color: #2f1c04;
  text-shadow:
    0 1px 0 rgba(255, 249, 219, 0.9),
    0 2px 1px rgba(255, 228, 156, 0.45),
    0 -1px 0 rgba(58, 26, 0, 0.6),
    0 3px 7px rgba(0, 0, 0, 0.45);
}

.spin-btn .spin-btn-face::before {
  content: '';
  position: absolute;
  inset: 7.5%;
  z-index: -1;
  border-radius: 50%;
  background:
    radial-gradient(circle at 50% 22%,
      #fff6d0 0%,
      #ffdd63 22%,
      #f2a81c 52%,
      #c1700a 78%,
      #93500a 100%);
  box-shadow:
    inset 0 3px 0 rgba(255, 255, 255, 0.9),
    inset 0 -14px 22px rgba(110, 52, 0, 0.6),
    inset 0 0 0 2px rgba(255, 236, 176, 0.82),
    0 0 0 1px rgba(66, 32, 0, 0.72);
}

.spin-btn .spin-btn-face::after {
  content: '';
  position: absolute;
  inset: 7.5%;
  z-index: -1;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0.32;
  background:
    repeating-conic-gradient(from 12deg,
      rgba(255, 255, 255, 0.17) 0deg 6deg,
      rgba(120, 60, 0, 0.13) 6deg 12deg),
    radial-gradient(circle at 50% 50%,
      transparent 40%,
      rgba(255, 245, 200, 0.3) 51%,
      transparent 61%);
  -webkit-mask-image: radial-gradient(circle, #000 56%, transparent 78%);
          mask-image: radial-gradient(circle, #000 56%, transparent 78%);
}

.spin-btn:disabled .spin-btn-face {
  filter: saturate(0.4) brightness(0.68);
}

.spin-btn.free .spin-btn-face { color: #04222c; text-shadow: 0 1px 0 rgba(220, 250, 255, 0.85), 0 -1px 0 rgba(4, 40, 52, 0.45); }
.spin-btn.free .spin-btn-face::before {
  background:
    radial-gradient(circle at 50% 22%,
      #e9feff 0%, #7fe9ff 22%, #22b6d8 54%, #0d6d8c 80%, #063f52 100%);
  box-shadow:
    inset 0 3px 0 rgba(255, 255, 255, 0.85),
    inset 0 -14px 22px rgba(3, 44, 58, 0.6),
    inset 0 0 0 2px rgba(190, 245, 255, 0.8),
    0 0 0 1px rgba(3, 34, 44, 0.72);
}

.spin-btn.is-auto .spin-btn-face { color: #3a1102; }
.spin-btn.is-auto .spin-btn-face::before {
  background:
    radial-gradient(circle at 50% 22%,
      #fff0e2 0%, #ff9f5c 24%, #f2621f 56%, #a8330a 82%, #6b1d02 100%);
  box-shadow:
    inset 0 3px 0 rgba(255, 255, 255, 0.85),
    inset 0 -14px 22px rgba(90, 26, 0, 0.6),
    inset 0 0 0 2px rgba(255, 214, 178, 0.8),
    0 0 0 1px rgba(60, 16, 0, 0.72);
}

.spin-hold-ring,
.spin-btn-overlay,
.spin-btn::after {
  border-radius: 50%;
}
.spin-btn::after { inset: 8%; }

.control-bar .spin-btn-sub {
  color: #ffffff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

.reel-col.reel-tease {
  animation: reelTeaseSurge 260ms ease-out both;
}
@keyframes reelTeaseSurge {
  0%   { filter: brightness(1); transform: scale(1); }
  40%  { filter: brightness(1.7) saturate(1.3); transform: scale(1.03); }
  100% { filter: brightness(1); transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .reel-col.reel-dun,
  .reel-col.reel-tease { animation: none; }
}

.sound-panel {
  height: 100dvh;
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  max-height: 100dvh;
}
.sound-panel::-webkit-scrollbar { width: 10px; }
.sound-panel::-webkit-scrollbar-track { background: rgba(0, 0, 0, 0.35); }
.sound-panel::-webkit-scrollbar-thumb {
  background: linear-gradient(180deg, #ffd94d, #b8860b);
  border-radius: var(--radius-pill);
  border: 2px solid rgba(0, 0, 0, 0.4);
}

.sound-slider {
  touch-action: none;
  cursor: pointer;
}
.sound-slider::-webkit-slider-thumb { cursor: grab; }
.sound-slider:active::-webkit-slider-thumb { cursor: grabbing; }

@media (pointer: coarse) {
  .sound-slider { height: 34px; }
  .sound-mute-btn { min-width: 40px; min-height: 40px; }
}

.terror-timer {
  position: fixed;
  top: calc(var(--topbar-h, 64px) + 8px + env(safe-area-inset-top, 0px));
  left: 50%;
  transform: translateX(-50%);
  z-index: 11850;
  margin: 0;
  width: min(94vw, 27rem);
  max-width: none;
  display: grid;
  grid-template-columns: auto 1fr;
  grid-template-areas:
    "skull text"
    "bar   bar";
  align-items: center;
  gap: 5px 9px;
  padding: 7px 15px 8px;
  pointer-events: none;
  backdrop-filter: blur(3px);
}
.terror-timer-skull { grid-area: skull; }
.terror-timer-text  { grid-area: text; }
.terror-timer-track {
  grid-area: bar;
  flex: none;
  width: 100%;
}

.terror-timer.cold {
  border-color: #4aa6c8;
  background: linear-gradient(180deg, rgba(12, 60, 86, 0.88), rgba(4, 22, 34, 0.92));
  box-shadow: 0 0 22px rgba(74, 166, 200, 0.4);
  color: #d7f3ff;
}
.terror-timer.cold .terror-timer-fill {
  background: linear-gradient(90deg, #4aa6c8, #a8ecff);
}
.terror-timer.cold .terror-timer-text strong {
  text-shadow: 0 0 10px #7fd8ff;
}

.terror-timer.urgent {
  border-color: #ff5a5a;
}
.terror-timer.urgent .terror-timer-text strong {
  font-size: 1.2rem;
  color: #fff;
}

@media (max-width: 768px) {
  .terror-timer {
    width: min(96vw, 22rem);
    padding: 5px 11px 6px;
    gap: 3px 7px;
  }
  .terror-timer-skull { font-size: 1rem; }
  .terror-timer-text { font-size: 0.68rem; }
  .terror-timer-text strong { font-size: 0.92rem; }
  .terror-timer-track { height: 5px; }
}

.terror-transition.active .terror-transition-fog {
  animation: terrorFog 2000ms ease-out forwards;
}
.terror-transition-fog::before,
.terror-transition-fog::after {
  content: '';
  position: absolute;
  inset: -20%;
  pointer-events: none;
}
.terror-transition-fog::before {
  background:
    radial-gradient(ellipse 55% 30% at 20% 70%, rgba(190, 30, 50, 0.4), transparent 70%),
    radial-gradient(ellipse 45% 28% at 80% 30%, rgba(60, 0, 40, 0.5), transparent 70%);
  animation: terrorFogCounter 2600ms ease-out forwards;
}
.terror-transition-fog::after {
  background: radial-gradient(circle at 50% 55%, rgba(255, 60, 60, 0.22), transparent 58%);
  animation: terrorFogPulse 1300ms ease-in-out infinite;
}
@keyframes terrorFogCounter {
  0%   { transform: translateX(22%) scale(1.25); opacity: 0; }
  35%  { opacity: 0.5; }
  100% { transform: translateX(-22%) scale(1.55); opacity: 0.16; }
}
@keyframes terrorFogPulse {
  0%, 100% { opacity: 0.35; transform: scale(1); }
  50%      { opacity: 0.7;  transform: scale(1.08); }
}

.terror-transition.closing .terror-transition-art {
  animation: terrorArtRise 1400ms cubic-bezier(0.2, 0.9, 0.3, 1) both,
             terrorArtBreathe 2200ms ease-in-out 1400ms infinite;
  filter: drop-shadow(0 0 26px rgba(255, 40, 40, 0.55))
          drop-shadow(0 12px 30px rgba(0, 0, 0, 0.8));
}
@keyframes terrorArtBreathe {
  0%, 100% { transform: scale(1) rotate(0deg); }
  50%      { transform: scale(1.045) rotate(-0.8deg); }
}

@media (prefers-reduced-motion: reduce) {
  .terror-transition-fog::before,
  .terror-transition-fog::after { animation: none; }
  .terror-transition.closing .terror-transition-art { animation: none; }
}

.edge-pulse[data-tone="terror"] { --pulse-color: rgba(214, 30, 40, 0.9); }

.reel-col.reel-dun {
  animation: reelDunKick calc(340ms + var(--dun-step, 1) * 55ms) cubic-bezier(0.2, 0.9, 0.25, 1) both;
  z-index: 2;
}
@keyframes reelDunKick {
  0%   { transform: translateY(0) scale(1) rotate(0deg); filter: brightness(1); }
  18%  { transform: translateY(calc(4px * var(--dun-step, 1))) scale(1.02) rotate(-0.9deg);
         filter: brightness(1.55) saturate(1.3) drop-shadow(0 0 14px rgba(220, 40, 40, 0.75)); }
  44%  { transform: translateY(calc(-2px * var(--dun-step, 1))) scale(0.985) rotate(0.7deg);
         filter: brightness(1.2) drop-shadow(0 0 8px rgba(220, 40, 40, 0.45)); }
  70%  { transform: translateY(1px) scale(1.005) rotate(-0.25deg); filter: brightness(1.06); }
  100% { transform: translateY(0) scale(1) rotate(0deg); filter: brightness(1); }
}

.reel-track.reel-dun-shift {
  animation: reelDunShift 540ms cubic-bezier(0.18, 1.1, 0.3, 1) both;
}
@keyframes reelDunShift {
  0%   { translate: 0 0; }
  22%  { translate: 0 var(--dun-shift, 12px); }
  58%  { translate: 0 calc(var(--dun-shift, 12px) * -0.28); }
  80%  { translate: 0 calc(var(--dun-shift, 12px) * 0.1); }
  100% { translate: 0 0; }
}

.reel-col.reel-dun .reel-track img {
  animation: dunSymbolTwitch 460ms ease-out both;
}
.reel-col.reel-dun .reel-track img:nth-child(even) { animation-delay: 40ms; }
.reel-col.reel-dun .reel-track img:nth-child(3n)   { animation-delay: 80ms; }
@keyframes dunSymbolTwitch {
  0%   { transform: scale(1) rotate(0deg); }
  30%  { transform: scale(1.12) rotate(-4deg); }
  60%  { transform: scale(0.96) rotate(2.5deg); }
  100% { transform: scale(1) rotate(0deg); }
}

.machine-frame.dun-dread {
  animation: dunDread 2200ms ease-in forwards;
}
@keyframes dunDread {
  0%   { filter: none; }
  35%  { filter: brightness(0.72) saturate(0.75) contrast(1.1); }
  85%  { filter: brightness(0.6) saturate(0.6) contrast(1.2); }
  100% { filter: none; }
}

body.dun-beat::after {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9998;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 45%,
    rgba(255, 40, 40, 0.16) 0%,
    rgba(140, 0, 20, 0.1) 35%,
    transparent 70%);
  animation: dunBeatFlash 240ms ease-out both;
}
@keyframes dunBeatFlash {
  0%   { opacity: 0; }
  25%  { opacity: 1; }
  100% { opacity: 0; }
}

.reels.dun-finale {
  animation: dunFinale 760ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
@keyframes dunFinale {
  0%   { box-shadow: 0 0 0 0 rgba(255, 40, 40, 0); }
  15%  { box-shadow: 0 0 60px 16px rgba(255, 50, 50, 0.55), inset 0 0 70px rgba(180, 0, 20, 0.5); }
  100% { box-shadow: 0 0 0 0 rgba(255, 40, 40, 0); }
}

@media (prefers-reduced-motion: reduce) {
  .reel-col.reel-dun,
  .reel-track.reel-dun-shift,
  .reel-col.reel-dun .reel-track img,
  .machine-frame.dun-dread,
  .reels.dun-finale,
  body.dun-beat::after { animation: none; }
}

/* =====================================================================
   THE CASTLE SPIN — EXTRA DREAD
   ---------------------------------------------------------------------
   Applied only inside the Castle of Terror, by the `spooky` flag on the
   terror_castle cue. The four dun-dun beats above are the opening bars;
   these carry the four seconds AFTER them, which used to look like an
   ordinary spin while the recording was at its most menacing.

   Everything here is time-boxed by the JS (classes are removed at
   stopAtMs + 700) rather than looping forever, and scheduleCueFlourishes
   returns early under prefers-reduced-motion, so none of it is ever
   applied to someone who asked for stillness. The @media block at the
   end is the second line of defence for a class left behind by an
   interrupted spin.
   ===================================================================== */

/* THE FOG. Rises from the bottom of the reels and thins out again, so it
   reads as breath in cold air rather than as a filter someone left on. */
.reels.terror-fog::before {
  content: '';
  position: absolute;
  inset: -12% -6% -18%;
  z-index: 3;
  pointer-events: none;
  border-radius: inherit;
  background:
    radial-gradient(120% 60% at 50% 108%, rgba(150, 190, 200, 0.30) 0%,
                                          rgba(90, 130, 150, 0.16) 38%,
                                          transparent 72%),
    radial-gradient(80% 44% at 18% 96%, rgba(180, 210, 220, 0.22) 0%, transparent 70%),
    radial-gradient(80% 44% at 82% 100%, rgba(160, 200, 215, 0.20) 0%, transparent 70%);
  filter: blur(6px);
  animation: terrorFog 5400ms ease-in-out both;
}
@keyframes terrorFog {
  0%   { opacity: 0; transform: translateY(26%) scale(1.06); }
  22%  { opacity: 1; }
  70%  { opacity: 0.85; transform: translateY(-4%) scale(1.02); }
  100% { opacity: 0; transform: translateY(-14%) scale(1); }
}

/* THE HEARTBEAT. Two thumps a beat, quickening: the animation-duration
   shortens across the spin using two overlapping cycles rather than a
   single even pulse, which is what stops it reading as a slow strobe. */
.reels.terror-pulse::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  border-radius: inherit;
  background: radial-gradient(ellipse at 50% 50%,
    transparent 42%,
    rgba(150, 0, 20, 0.18) 74%,
    rgba(90, 0, 12, 0.42) 100%);
  animation: terrorHeartbeat 900ms ease-out infinite;
}
@keyframes terrorHeartbeat {
  0%   { opacity: 0.32; }
  8%   { opacity: 0.95; }
  16%  { opacity: 0.40; }
  26%  { opacity: 0.78; }
  40%  { opacity: 0.30; }
  100% { opacity: 0.32; }
}

/* THE FLICKER. A failing bulb over the whole machine. Deliberately
   irregular — an even flicker looks like a refresh-rate fault rather
   than something going wrong in the room. */
.machine-frame.terror-flicker {
  animation: terrorFlicker 5400ms steps(1, end) both;
}
@keyframes terrorFlicker {
  0%,   6%   { filter: none; }
  7%,   8%   { filter: brightness(0.42) saturate(0.5); }
  9%,  21%   { filter: brightness(0.94) saturate(0.9); }
  22%, 23%   { filter: brightness(1.5) saturate(1.25) contrast(1.1); }
  24%, 43%   { filter: brightness(0.88) saturate(0.85); }
  44%, 45%   { filter: brightness(0.36) saturate(0.42); }
  46%, 57%   { filter: brightness(0.9) saturate(0.88); }
  58%, 59%   { filter: brightness(1.35) saturate(1.2); }
  60%, 79%   { filter: brightness(0.84) saturate(0.8) contrast(1.06); }
  80%, 82%   { filter: brightness(0.3) saturate(0.35); }
  83%, 100%  { filter: none; }
}

/* THE BOLT. One strike, on the last dun, appended and removed by the JS
   so nothing is left painted over the machine if a spin is interrupted. */
.terror-bolt {
  position: absolute;
  inset: 0;
  z-index: 9;
  pointer-events: none;
  background: linear-gradient(101deg,
    transparent 0%, transparent 43%,
    rgba(255, 255, 255, 0.92) 47%,
    rgba(190, 225, 255, 0.75) 50%,
    transparent 54%, transparent 100%);
  mix-blend-mode: screen;
  animation: terrorBolt 820ms ease-out both;
}
@keyframes terrorBolt {
  0%   { opacity: 0; }
  4%   { opacity: 1; }
  9%   { opacity: 0.15; }
  14%  { opacity: 0.95; }
  22%  { opacity: 0; }
  100% { opacity: 0; }
}

/* THE SLAM. Each reel lands like a lid closing, and --slam-weight makes
   the right-hand reels land harder — the last one is holding the result,
   so it should hit hardest. */
.reel-col.reel-slam {
  animation: terrorSlam 440ms cubic-bezier(0.16, 1.2, 0.3, 1) both;
}
@keyframes terrorSlam {
  0%   { transform: translateY(calc(-5px * var(--slam-weight, 1))) scale(1.015);
         filter: brightness(1.4) drop-shadow(0 0 12px rgba(220, 40, 40, 0.6)); }
  38%  { transform: translateY(calc(3px * var(--slam-weight, 1))) scale(0.982);
         filter: brightness(0.82); }
  66%  { transform: translateY(calc(-1px * var(--slam-weight, 1))) scale(1.004); }
  100% { transform: translateY(0) scale(1); filter: none; }
}

/* THE ROOM. A cold vignette over everything for the length of the spin,
   so the dread is not confined to the cabinet. */
body.terror-spinning::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 9997;
  pointer-events: none;
  background: radial-gradient(ellipse at 50% 45%,
    transparent 30%,
    rgba(40, 0, 8, 0.22) 72%,
    rgba(18, 0, 4, 0.58) 100%);
  animation: terrorRoom 5400ms ease-in-out both;
}
@keyframes terrorRoom {
  0%   { opacity: 0; }
  18%  { opacity: 1; }
  76%  { opacity: 1; }
  100% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .reels.terror-fog::before,
  .reels.terror-pulse::after,
  .machine-frame.terror-flicker,
  .terror-bolt,
  .reel-col.reel-slam,
  body.terror-spinning::before { animation: none; display: none; }
}

/* The Castle is the heaviest screen in the game already. On a phone the
   fog and the bolt are the two that cost the most to composite, so they
   go and the cheap ones stay. */
.lean-fx .reels.terror-fog::before,
.lean-fx .terror-bolt { display: none; }

.prize-row.prize-row-solo {
  grid-template-columns: auto minmax(0, 1fr);
  justify-content: center;
  gap: clamp(8px, 1.1vw, 18px);
  padding: clamp(7px, 1vh, 14px) clamp(9px, 1vw, 16px);
}
.prize-row-solo img {
  width: clamp(36px, 4.4vw, 76px);
  height: clamp(36px, 4.4vw, 76px);
}
.prize-row-solo .prize-row-value {
  font-size: clamp(1rem, 2.1vw, 2rem);
  line-height: 1.05;
}

@media (max-width: 768px) {
  .prize-row-solo img { width: 28px; height: 28px; }
  .prize-row-solo .prize-row-value { font-size: 0.95rem; }
  .prize-row.prize-row-solo { padding: 4px 8px; gap: 8px; }
}

.next-theme.is-soon {
  position: relative;
  z-index: 4;
  border-color: #ff6b6b;
  background: linear-gradient(180deg, rgba(72, 14, 22, 0.9), rgba(28, 6, 10, 0.95));
  box-shadow:
    0 0 26px rgba(255, 80, 80, 0.55),
    inset 0 1px 0 rgba(255, 255, 255, 0.12);
  transform-origin: center;
  will-change: transform;
  padding: clamp(11px, 1.5vh, 16px) clamp(12px, 1.3vw, 18px);
  gap: 4px 11px;
  animation: nextThemeUrgent 1000ms ease-in-out infinite;
}
.next-theme.is-soon .next-theme-art {
  width: clamp(34px, 3.2vw, 58px);
  height: clamp(34px, 3.2vw, 58px);
}
.next-theme.is-soon .next-theme-time { font-size: clamp(1rem, 1.9vw, 1.7rem); }
.next-theme.is-soon .next-theme-name { font-size: clamp(0.72rem, 1.1vw, 1rem); }
@keyframes nextThemeUrgent {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.045); }
}
.next-theme.is-soon .next-theme-label { color: #ff9c9c; }
.next-theme.is-soon .next-theme-time {
  color: #fff;
  text-shadow: 0 0 12px rgba(255, 90, 90, 0.9);
  font-variant-numeric: tabular-nums;
}
.next-theme.is-soon .next-theme-name { color: #ffd7d7; }

@media (prefers-reduced-motion: reduce) {
  .next-theme.is-soon { animation: none; transform: none; }
}

.control-bar .cabinet-controls {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  gap: clamp(6px, 1.2vw, 24px);
}

.deck-col { display: flex; align-items: center; min-width: 0; }
.deck-col-left   { justify-content: flex-start; gap: clamp(6px, 0.9vw, 18px); }
.deck-col-centre { justify-content: center; align-items: flex-end; gap: clamp(6px, 0.9vw, 20px); }
.deck-col-right  { justify-content: flex-end; }

.deck-shortcuts {
  display: flex;
  flex-direction: column;
  gap: 5px;
  flex: 0 0 auto;
  min-width: 0;
}

.control-bar .deck-shortcut {
  width: clamp(82px, 8.6vw, 138px);
  min-width: 0;
  min-height: 0;
  margin: 0;
  flex: 0 0 auto;
  box-sizing: border-box;
}
.control-bar .btn.wheel.deck-shortcut {
  padding: 6px 8px;
  font-size: inherit;
}
.control-bar .rail-tile.rail-offer.deck-shortcut {
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 5px 8px;
}
.control-bar .rail-offer-art { width: 24px; height: 24px; flex: 0 0 auto; }
.control-bar .rail-offer-cta {
  font-size: clamp(0.46rem, 0.68vw, 0.7rem);
  color: #2a1600;
  width: auto;
  min-width: 0;
  letter-spacing: 0.3px;
  white-space: normal;
  line-height: 1.03;
  text-align: center;
}
.control-bar .deck-shortcut-label {
  font-size: clamp(0.52rem, 0.75vw, 0.78rem);
  color: var(--gold, #ffd76a);
  width: auto;
  letter-spacing: 1px;
  white-space: nowrap;
}

.control-bar .deck-col-centre .spin-btn {
  width: clamp(96px, 16vh, 240px);
  max-width: 240px;
}

.deck-col-left .btn.hold { width: clamp(88px, min(10vw, 12.5vh), 140px); }

.control-bar .deck-col-right .deck-info { margin-left: auto; }

#copyright {
  left: 10px;
  right: auto;
  bottom: 8px;
  font-size: 11px;
  opacity: 0.55;
  pointer-events: none;
}

@media (max-width: 900px) {
  .control-bar .deck-col-centre .spin-btn { max-width: 130px; }
}
@media (max-width: 720px) {
  .control-bar .cabinet-controls { gap: 5px; }
  .control-bar .deck-shortcut { width: clamp(62px, 17vw, 96px); }
  .control-bar .deck-col-centre .spin-btn { max-width: 104px; }
  .control-bar .rail-offer-art { width: 18px; height: 18px; }
}

.deck-shortcuts { order: 0; }
.deck-col-left .meter-stake { order: 1; flex: 0 1 auto; min-width: 0; max-width: clamp(180px, 27vw, 380px); }
.deck-col-left .btn.hold { order: 2; margin-left: auto; }
.deck-col-centre { flex: 0 0 auto; }

@media (max-width: 760px) {
  .control-bar .cabinet-controls {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    gap: 6px 8px;
  }
  .deck-col { flex: 0 1 auto; min-width: 0; }
  .deck-col-left {
    flex-wrap: wrap;
    justify-content: center;
    gap: 6px;
  }
  .deck-col-left .btn.hold { margin-left: 0; }
  .deck-shortcuts { flex-direction: row; gap: 6px; }
  .deck-col-right { justify-content: center; }
}

.lean-fx .cabinet .machine-frame::after { animation: none; }
.lean-fx .falling-coin { filter: none; }
.lean-fx .spin-btn::after,
.lean-fx .rail-offer .shop-cta-shine { animation: none; opacity: 0; }
.lean-fx .room::after { animation: none; }

.control-bar .spin-btn-overlay { color: #a8f4ff; font-size: inherit; }
.control-bar .spin-btn.is-auto .spin-btn-overlay { color: #ffe9d6; }

.lean-fx .spin-btn:not(:disabled) { animation: none; }

.meter-win.paid .deck-info-val { animation: meterPaid 700ms ease-out; }

@media (max-width: 720px) {
  .control-bar .deck-shortcut { width: clamp(72px, 20vw, 104px); }
  .control-bar .rail-offer-art { display: none; }
  .control-bar .rail-tile.rail-offer.deck-shortcut { padding: 6px 5px; }
  .control-bar .rail-offer-cta { font-size: clamp(0.42rem, 2.3vw, 0.62rem); letter-spacing: 0; }
}

@media (max-width: 720px) {
  .control-bar .rail-offer-cta {
    white-space: normal;
    line-height: 1.02;
    text-align: center;
    word-break: normal;
  }
  .control-bar .rail-tile.rail-offer.deck-shortcut { padding: 5px 4px; }
}

:root {
  --room-deep: #0b0320;
  --room-mid:  #2b0b4e;
  --room-tint: rgba(126, 42, 190, 0.34);
  --room-gilt: rgba(214, 158, 255, 0.13);
  --room-veil: rgba(4, 0, 12, 0.62);
}

.room::before {
  opacity: 0.85;
  background-image:
    radial-gradient(circle at 18% 24%, rgba(255,255,255,0.95) 0 1.1px, transparent 2px),
    radial-gradient(circle at 72% 11%, rgba(214,196,255,0.8) 0 1.4px, transparent 2.4px),
    radial-gradient(circle at 46% 68%, rgba(255,255,255,0.7) 0 0.9px, transparent 1.8px),
    radial-gradient(circle at 88% 54%, rgba(255,226,180,0.75) 0 1.2px, transparent 2.2px),
    radial-gradient(circle at 8% 82%, rgba(255,255,255,0.6) 0 0.8px, transparent 1.6px),
    radial-gradient(circle at 61% 37%, rgba(190,220,255,0.7) 0 1px, transparent 2px);
  background-size:
    340px 340px, 470px 470px, 260px 260px,
    610px 610px, 190px 190px, 410px 410px;
}

.room {
  background:
    radial-gradient(ellipse 46% 34% at 22% 30%, rgba(190, 60, 220, 0.22), transparent 70%),
    radial-gradient(ellipse 40% 30% at 80% 62%, rgba(90, 60, 240, 0.20), transparent 72%),
    radial-gradient(ellipse 70% 55% at 50% 38%, var(--room-tint), transparent 72%),
    radial-gradient(ellipse 120% 90% at 50% 42%, var(--room-mid), var(--room-deep) 78%);
}

.payline-rail {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  height: 72%;
  z-index: 4;
  pointer-events: none;
}
.payline-rail-left  { left: clamp(-46px, -3.2vw, -30px); align-items: flex-end; }
.payline-rail-right { right: clamp(-46px, -3.2vw, -30px); align-items: flex-start; }

.payline-badge {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(24px, 2.1vw, 34px);
  height: clamp(26px, 2.3vw, 38px);
}
.payline-badge b {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  clip-path: polygon(50% 0%, 100% 26%, 100% 74%, 50% 100%, 0% 74%, 0% 26%);
  background: linear-gradient(180deg, var(--pl-a), var(--pl-b));
  font-family: 'Orbitron', sans-serif;
  font-weight: 900;
  font-size: clamp(0.66rem, 1vw, 1rem);
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.85);
}
.payline-badge::after {
  content: '';
  position: absolute;
  top: 50%;
  width: clamp(12px, 1.2vw, 18px);
  height: 3px;
  border-radius: 2px;
  transform: translateY(-50%);
}
.payline-rail-left .payline-badge::after {
  left: 100%;
  background: linear-gradient(90deg, var(--pl-a), transparent);
}
.payline-rail-right .payline-badge::after {
  right: 100%;
  background: linear-gradient(270deg, var(--pl-a), transparent);
}

.payline-badge[data-line="1"] { --pl-a: #ff5ad4; --pl-b: #9c1a6e; }
.payline-badge[data-line="2"] { --pl-a: #4ad0ff; --pl-b: #0f5ba4; }
.payline-badge[data-line="3"] { --pl-a: #ffd94d; --pl-b: #9c6706; }

@media (max-width: 900px) {
  .payline-rail { display: none; }
}

#responsibly {
  position: fixed;
  left: 10px;
  bottom: 24px;
  z-index: 9999;
  font-family: 'Orbitron', sans-serif;
  font-size: 10px;
  letter-spacing: 1px;
  color: #fff;
  opacity: 0.45;
  pointer-events: none;
}

.control-bar .btn.wheel.deck-shortcut .deck-shortcut-label {
  color: #2a1600;
  font-weight: 800;
}

.control-bar .bet-panel {
  flex-direction: row;
  align-items: center;
  gap: clamp(6px, 0.7vw, 14px);
  padding: clamp(4px, 0.5vh, 8px) clamp(8px, 0.8vw, 15px);
  border: 1px solid var(--gold-deep, #8a5a12);
  border-radius: var(--radius-md);
  background: linear-gradient(180deg, rgba(38, 12, 62, 0.7), rgba(14, 4, 26, 0.85));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
}
.control-bar .bet-panel .bet-stepper {
  border: none;
  padding: 0;
  background: none;
  box-shadow: none;
}
.control-bar .bet-panel .bet-cost {
  margin: 0;
  flex: 0 0 auto;
  white-space: nowrap;
}

.control-bar .deck-btn .hold-count {
  position: absolute;
  top: -6px;
  right: -6px;
  margin: 0;
  z-index: 4;
}

.control-bar .machine-frame,
.cabinet .machine-frame {
  border-color: var(--gold, #e6b325);
}

#copyright, #responsibly {
  position: static;
  bottom: auto;
  right: auto;
  left: auto;
  font-size: inherit;
  color: inherit;
  opacity: 1;
  z-index: auto;
}
.deck-footnote {
  position: fixed;
  left: 12px;
  bottom: 4px;
  z-index: 9999;
  display: flex;
  gap: 10px;
  align-items: baseline;
  font-family: 'Orbitron', sans-serif;
  font-size: 10px;
  letter-spacing: 1px;
  color: #fff;
  opacity: 0.42;
  pointer-events: none;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
}
.control-bar { padding-bottom: 16px; }

.control-bar .bet-panel .bet-stepper {
  flex: 0 0 auto;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  min-width: 0;
}
.control-bar .bet-panel .bet-cost {
  flex: 0 0 auto;
  width: auto;
  min-width: 0;
  gap: 5px;
  justify-content: flex-start;
}

.rail-right > .jackpot-banner { flex: 0 0 auto; }
.prize-ladder > .prize-row.prize-row-solo { flex: 0 0 auto; min-height: 0; }
.jackpot-banner > .prize-ladder { flex: 0 0 auto; }

.deck-footnote {
  bottom: 3px;
  font-size: 9px;
  opacity: 0.38;
}
.control-bar { padding-bottom: 18px; }

.control-bar .bet-panel .bet-cost { min-width: 0; overflow: hidden; }
.deck-col-left { column-gap: clamp(5px, 0.8vw, 16px); }

.cabinet .machine-frame {
  border-color: var(--gold, #e6b325);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.6),
    0 0 30px -4px rgba(255, 190, 60, 0.42),
    0 10px 30px rgba(0, 0, 0, 0.5),
    inset 0 0 60px -18px rgba(150, 60, 220, 0.55);
}
.cabinet .machine-frame::after {
  --frame-paint: conic-gradient(from var(--frame-angle),
    #ffd94d, #b249ff 33%, #2b0b4e 66%, #ffd94d);
}

/* =====================================================================
   UI SYSTEM — tokens, surfaces, and the three call-to-action buttons.
   Everything below keys off selectors that already exist, so no markup
   moves. arcade.css is sheet 26 of 27, which is why these win.
   ===================================================================== */

:root {

  --stroke: 1px;
  --stroke-strong: 2px;

  --edge-cyan:   rgba(34, 227, 255, 0.42);
  --edge-gold:   rgba(230, 179, 37, 0.52);
  --edge-violet: rgba(168, 85, 247, 0.48);
  --edge-pink:   rgba(255, 90, 212, 0.48);

  --halo-cyan:   0 0 18px rgba(34, 227, 255, 0.26);
  --halo-gold:   0 0 18px rgba(255, 190, 60, 0.28);
  --halo-violet: 0 0 18px rgba(168, 85, 247, 0.26);

  --glass:      rgba(16, 7, 32, 0.58);
  --glass-lift: rgba(28, 13, 52, 0.62);
  --glass-blur: blur(11px) saturate(1.18);

  --lift-1: 0 2px 8px rgba(0, 0, 0, 0.45);
  --lift-2: 0 8px 22px rgba(0, 0, 0, 0.52);
  --lift-3: 0 16px 40px rgba(0, 0, 0, 0.6);

  --cta-font: 'Orbitron', sans-serif;
  --cta-weight: 800;
  --cta-tracking: 0.09em;
}

/* ---------- Surfaces: one glass recipe for every floating panel ------ */

.hud-stat,
.jackpot-banner,
.next-theme,
.deck-info,
.boosts-dock .rail-tile,
.rail-menu > button,
.top-controls-right button,
.control-bar {
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}

.hud-stat,
.deck-info {
  border-radius: var(--radius-md);
  border-width: var(--stroke);
  border-color: var(--edge-cyan);
  background: var(--glass);
  box-shadow: var(--lift-1), var(--halo-cyan);
}

.jackpot-banner {
  border-radius: var(--radius-md);
  border-width: var(--stroke);
  border-color: var(--edge-gold);
  background: var(--glass-lift);
  box-shadow: var(--lift-2), var(--halo-gold);
}

.next-theme {
  border-radius: var(--radius-md);
  border-width: var(--stroke);
  border-color: var(--edge-violet);
  background: var(--glass-lift);
  box-shadow: var(--lift-2), var(--halo-violet);
}

.boosts-dock .rail-tile,
.rail-menu > button {
  border-radius: var(--radius-md);
  border-width: var(--stroke);
  background: var(--glass);
  box-shadow: var(--lift-1);
}

/* ---------- The deck and the cabinet share the large radius --------- */

.control-bar {
  border-radius: var(--radius-lg);
  border-width: var(--stroke-strong);
  border-color: var(--edge-cyan);
  background: var(--glass);
  box-shadow: var(--lift-2), var(--halo-cyan);
}

.cabinet .machine-frame {
  border-radius: var(--radius-lg);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.65),
    var(--lift-3),
    0 0 34px -6px rgba(255, 190, 60, 0.4),
    inset 0 0 70px -18px rgba(150, 60, 220, 0.5);
}

.reels {
  border-radius: calc(var(--radius-lg) - 6px);
  box-shadow:
    inset 0 0 0 1px rgba(0, 0, 0, 0.7),
    inset 0 12px 30px -14px rgba(0, 0, 0, 0.9);
}

/* ---------- Call to action: SPIN, WHEEL, ENTER SHOP! ---------------- */

.control-bar .btn.wheel.deck-shortcut,
.control-bar .rail-tile.rail-offer.deck-shortcut,
.spin-btn {
  font-family: var(--cta-font);
  font-weight: var(--cta-weight);
  letter-spacing: var(--cta-tracking);
  transition: transform 140ms ease, box-shadow 200ms ease, filter 180ms ease;
}

.control-bar .btn.wheel.deck-shortcut,
.control-bar .rail-tile.rail-offer.deck-shortcut {
  border-radius: var(--radius-sm);
  border-width: var(--stroke);
  border-color: var(--edge-gold);
  box-shadow: var(--lift-1), inset 0 1px 0 rgba(255, 255, 255, 0.28);
}

.control-bar .btn.wheel.deck-shortcut:hover:not(:disabled),
.control-bar .rail-tile.rail-offer.deck-shortcut:hover:not(:disabled) {
  transform: translateY(-2px);
  box-shadow: var(--lift-2), var(--halo-gold), inset 0 1px 0 rgba(255, 255, 255, 0.34);
  filter: brightness(1.06);
}

.control-bar .btn.wheel.deck-shortcut:active:not(:disabled),
.control-bar .rail-tile.rail-offer.deck-shortcut:active:not(:disabled) {
  transform: translateY(1px);
  box-shadow: inset 0 3px 7px rgba(90, 45, 0, 0.45);
}

.spin-btn { border-radius: 50%; }
.spin-btn:not(:disabled) { filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.55)); }
.spin-btn:hover:not(:disabled) { transform: translateY(-2px) scale(1.02); }
.spin-btn:active:not(:disabled) { transform: translateY(1px) scale(0.98); }

/* ---------- Controls that are not calls to action ------------------- */

.control-bar .deck-btn,
.control-bar .bet-panel {
  border-radius: var(--radius-sm);
  border-width: var(--stroke);
}
.control-bar .deck-btn { border-color: rgba(255, 255, 255, 0.22); }

/* ---------- BET: symmetric around the multiplier -------------------- */

.control-bar .bet-panel {
  overflow: hidden;
  max-width: none;
  gap: clamp(8px, 0.9vw, 16px);
  padding: clamp(4px, 0.5vh, 8px) clamp(10px, 1vw, 16px);
}
.control-bar .bet-panel .bet-cost {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}
.control-bar .bet-group {
  gap: clamp(7px, 0.7vw, 12px);
  align-items: center;
}
.control-bar .bet-group #betCurrent { margin: 0; }
.control-bar .bet-group .bet-step {
  margin: 0;
  flex: 0 0 auto;
}

/* ---------- Footer -------------------------------------------------- */

.deck-footnote {
  bottom: 6px;
  font-size: 10px;
  opacity: 0.4;
}

.top-pills .hud-stat,
.hud-stat.hud-stat {
  border-radius: var(--radius-md);
  border-width: var(--stroke);
  border-color: var(--edge-cyan);
}

section.control-bar,
.control-bar.control-bar {
  border-radius: var(--radius-lg);
  border-width: var(--stroke-strong);
  border-color: var(--edge-cyan);
}

.cabinet .machine-frame.machine-frame {
  border-radius: var(--radius-lg);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, 0.65),
    var(--lift-3),
    0 0 34px -6px rgba(255, 190, 60, 0.4),
    inset 0 0 70px -18px rgba(150, 60, 220, 0.5);
}

.control-bar .bet-panel {
  flex-wrap: wrap;
  justify-content: center;
  row-gap: 2px;
}
.control-bar .bet-panel .bet-cost {
  overflow: visible;
  text-overflow: clip;
  flex: 0 0 auto;
}

/* =====================================================================
   FINAL POLISH LAYER
   ===================================================================== */

/* ---------- 1a. Optional metallic side frames ----------------------
   Absolutely positioned inside .machine-stage and pointer-events:none,
   so toggling them can never move a single pixel of layout. They sit
   between the cabinet edge and the payline badges, which start at
   -30px, so the two never meet. */

.side-frame {
  position: absolute;
  top: 8%;
  bottom: 8%;
  width: 14px;
  z-index: 5;
  pointer-events: none;
  display: grid;
  place-items: center;
}
.side-frame-left  { left: -7px; }
.side-frame-right { right: -7px; }

html.no-side-frames .side-frame { display: none; }

.side-frame-rail {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-pill);
  background:
    linear-gradient(90deg,
      rgba(10, 6, 20, 0.9) 0%,
      rgba(150, 160, 185, 0.75) 22%,
      rgba(226, 236, 255, 0.95) 42%,
      rgba(120, 132, 160, 0.8) 58%,
      rgba(8, 5, 16, 0.92) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.55),
    inset 0 -1px 0 rgba(0, 0, 0, 0.75),
    0 0 14px rgba(120, 90, 200, 0.35);
}
.side-frame-rail::before,
.side-frame-rail::after {
  content: '';
  position: absolute;
  left: 50%;
  width: 22px;
  height: 10px;
  transform: translateX(-50%);
  border-radius: var(--radius-xs);
  background: linear-gradient(180deg, #e6ecff, #8b93ad 55%, #2a2c3c);
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.7);
}
.side-frame-rail::before { top: 6%; }
.side-frame-rail::after  { bottom: 6%; }

.side-frame-core {
  position: relative;
  z-index: 1;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 38% 32%,
      #fff6ff 0%,
      var(--violet-mid) 34%,
      #6d28d9 66%,
      #24103f 100%);
  box-shadow:
    0 0 16px rgba(var(--violet-mid-rgb), 0.75),
    inset 0 1px 2px rgba(255, 255, 255, 0.6),
    inset 0 -3px 6px rgba(0, 0, 0, 0.6);
  animation: sideCorePulse 3.6s ease-in-out infinite;
}
@keyframes sideCorePulse {
  0%, 100% { box-shadow: 0 0 14px rgba(var(--violet-mid-rgb), 0.6),
                          inset 0 1px 2px rgba(255, 255, 255, 0.6),
                          inset 0 -3px 6px rgba(0, 0, 0, 0.6); }
  50%      { box-shadow: 0 0 26px rgba(var(--violet-mid-rgb), 0.95),
                          inset 0 1px 2px rgba(255, 255, 255, 0.7),
                          inset 0 -3px 6px rgba(0, 0, 0, 0.6); }
}

@media (max-width: 900px) {
  .side-frame { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .side-frame-core { animation: none; }
}

/* ---------- 1b. Reels blend into the space behind them -------------- */

.reels {
  background: linear-gradient(180deg, rgba(10, 12, 24, 0.85), rgba(5, 6, 15, 0.95));
}

/* ---------- 2a. COSTS sits below the bet box, never beside it ------- */

.control-bar .bet-panel {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  flex-wrap: nowrap;
  gap: 0;
  padding: 0;
  border: none;
  background: none;
  box-shadow: none;
  overflow: visible;
}
.control-bar .bet-panel .bet-stepper {
  border: var(--stroke) solid var(--edge-gold);
  border-radius: var(--radius-sm);
  padding: clamp(4px, 0.5vh, 8px) clamp(10px, 1vw, 16px);
  background: linear-gradient(180deg, rgba(38, 12, 62, 0.7), rgba(14, 4, 26, 0.85));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
}
.control-bar .bet-panel .bet-cost {
  margin-top: 8px;
  flex: 0 0 auto;
  justify-content: center;
  white-space: nowrap;
}

/* ---------- 2b. Legal line clears the deck padding ------------------ */

.deck-footnote {
  left: 14px;
  bottom: 8px;
  padding: 0 2px;
  max-width: calc(100vw - 28px);
  white-space: nowrap;
}

/* ---------- 2c. One tone for every secondary label ------------------ */

.deck-info-key,
.next-theme-label,
.meter-label,
.deck-btn-sub,
.terror-timer-text,
.stat-label {
  color: rgba(180, 220, 240, 0.7);
}

/* ---------- 3a. One glow for every primary control ------------------ */

.control-bar .btn.wheel.deck-shortcut:hover:not(:disabled),
.control-bar .rail-tile.rail-offer.deck-shortcut:hover:not(:disabled),
.control-bar .btn.wheel.deck-shortcut:focus-visible,
.control-bar .rail-tile.rail-offer.deck-shortcut:focus-visible,
.control-bar .btn.wheel.deck-shortcut:active:not(:disabled),
.control-bar .rail-tile.rail-offer.deck-shortcut:active:not(:disabled) {
  box-shadow:
    0 0 12px rgba(255, 180, 0, 0.4),
    var(--lift-2),
    inset 0 1px 0 rgba(255, 255, 255, 0.34);
  outline: none;
}

.spin-btn:hover:not(:disabled),
.spin-btn:focus-visible,
.spin-btn:active:not(:disabled) {
  outline: none;
}
.spin-btn:hover:not(:disabled) .spin-btn-face,
.spin-btn:focus-visible .spin-btn-face,
.spin-btn:active:not(:disabled) .spin-btn-face {
  box-shadow:
    0 0 12px rgba(255, 180, 0, 0.4),
    0 2px 0 rgba(255, 242, 205, 0.3),
    inset 0 0 0 1px rgba(48, 22, 0, 0.6);
}

.control-bar .btn.wheel.deck-shortcut:focus-visible,
.control-bar .rail-tile.rail-offer.deck-shortcut:focus-visible,
.spin-btn:focus-visible {
  border-color: var(--gold-bright, #ffd94d);
}

.control-bar .deck-info-key,
.control-bar .meter-label,
.control-bar .deck-btn-sub,
.next-theme .next-theme-label,
.terror-timer .terror-timer-text,
.stats-row .stat-label {
  color: rgba(180, 220, 240, 0.7);
}

.boost-empty {
  padding: 14px 16px;
  text-align: center;
  font-size: 0.78rem;
  line-height: 1.35;
  color: rgba(180, 220, 240, 0.7);
  white-space: normal;
}

.wheel-overlay {
  padding: clamp(8px, 1.5vmin, 20px);
  align-items: center;
  justify-content: center;
}
.wheel-panel {
  width: min(58rem, 96vw);
  max-width: 96vw;
  max-height: 97vh;
  padding: clamp(10px, 1.6vmin, 22px);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(6px, 1.2vmin, 16px);
}
.wheel-panel .wheel-stage {
  width: min(78vmin, 88vw, calc(97vh - 190px));
  height: auto;
  aspect-ratio: 1;
  margin-inline: auto;
}
.wheel-ring { inset: -4.2%; }

@media (max-width: 720px) {
  .wheel-panel .wheel-stage { width: min(86vmin, 92vw, calc(97vh - 170px)); }
  .wheel-panel { padding: 8px; gap: 6px; }
}

.wheel-panel {
  width: fit-content;
  max-width: 96vw;
  position: relative;
  padding-bottom: clamp(10px, 1.6vmin, 22px);
}
.wheel-panel .wheel-stage {
  width: min(84vmin, 90vw, calc(97vh - 172px));
}
.wheel-panel .wheel-actions {
  margin-top: clamp(4px, 0.8vmin, 10px);
}

.wheel-panel {
  width: min(44rem, 94vw);
  max-width: 94vw;
  overflow: hidden;
  padding: clamp(14px, 2vmin, 26px) clamp(12px, 2vmin, 24px);
}
.wheel-panel .wheel-stage {
  width: min(72vmin, 82vw, calc(97vh - 205px));
}
@media (max-width: 720px) {
  .wheel-panel .wheel-stage { width: min(80vmin, 86vw, calc(97vh - 175px)); }
}

.auth-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin-bottom: 12px;
}
.auth-tab {
  padding: 9px 10px;
  border-radius: var(--radius-sm);
  border: var(--stroke) solid rgba(255, 255, 255, 0.16);
  background: rgba(255, 255, 255, 0.05);
  color: rgba(180, 220, 240, 0.7);
  font-family: 'Orbitron', sans-serif;
  font-weight: 700;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease, box-shadow 200ms ease;
}
.auth-tab:hover { background: rgba(255, 255, 255, 0.09); }
.auth-tab.active {
  color: #2a0a45;
  background: linear-gradient(180deg, #e9d5ff, var(--violet-mid));
  border-color: #f3e8ff;
  box-shadow: 0 0 12px rgba(var(--violet-mid-rgb), 0.45);
}
.auth-error {
  margin: 8px 0 0;
  padding: 8px 10px;
  border-radius: var(--radius-xs);
  border: var(--stroke) solid rgba(255, 120, 120, 0.5);
  background: rgba(120, 20, 30, 0.35);
  color: #ffc9c9;
  font-size: 0.76rem;
  line-height: 1.35;
}
.auth-error[hidden] { display: none; }

.chat-button {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 11500;
  width: 52px;
  height: 52px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: var(--stroke-strong) solid var(--edge-cyan);
  background: var(--glass-lift);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  color: var(--cyan, #22e3ff);
  cursor: pointer;
  box-shadow: var(--lift-2), var(--halo-cyan);
  transition: transform 140ms ease, box-shadow 200ms ease;
}
.chat-button svg { width: 24px; height: 24px; }
.chat-button:hover { transform: translateY(-2px); box-shadow: var(--lift-3), 0 0 12px rgba(255, 180, 0, 0.4); }
.chat-button:active { transform: translateY(1px); }

.chat-badge {
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 20px;
  height: 20px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  display: grid;
  place-items: center;
  background: linear-gradient(180deg, #ff6b6b, #a01010);
  color: #fff;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.62rem;
  font-weight: 800;
  border: 1px solid rgba(255, 255, 255, 0.5);
}
.chat-badge[hidden] { display: none; }

.chat-panel {
  position: fixed;
  right: 14px;
  bottom: 76px;
  z-index: 11500;
  width: min(23rem, calc(100vw - 28px));
  /* A DEFINITE HEIGHT, not a maximum.

     With only max-height the panel sized itself to its CONTENT, and
     .chat-list's `flex: 1 1 auto` had nothing to fill — so the BIG WINS
     tab, which usually holds a handful of rows, collapsed the whole panel
     into a small box sitting on the bottom edge of the screen instead of
     occupying the chat it belongs to. It also meant the panel changed
     size as messages arrived, which moved the thing you were reading.

     Fixed height, list scrolls inside it: the panel is the same shape on
     both tabs and whatever is in them. */
  height: min(30rem, 70vh);
  display: flex;
  flex-direction: column;
  border-radius: var(--radius-lg);
  border: var(--stroke-strong) solid var(--edge-cyan);
  background: var(--glass-lift);
  backdrop-filter: var(--glass-blur);
  -webkit-backdrop-filter: var(--glass-blur);
  box-shadow: var(--lift-3), var(--halo-cyan);
  overflow: hidden;
}
.chat-panel[hidden] { display: none; }

.chat-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 12px;
  border-bottom: var(--stroke) solid rgba(255, 255, 255, 0.12);
}
.chat-title {
  font-family: 'Orbitron', sans-serif;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  color: var(--gold-bright, #ffd94d);
}
.chat-close {
  border: none;
  background: none;
  color: rgba(180, 220, 240, 0.7);
  font-size: 0.85rem;
  cursor: pointer;
  padding: 2px 4px;
}

.chat-list {
  flex: 1 1 auto;
  margin: 0;
  padding: 10px 12px;
  list-style: none;
  overflow-y: auto;
  overscroll-behavior: contain;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.chat-empty {
  color: rgba(180, 220, 240, 0.7);
  font-size: 0.76rem;
  text-align: center;
  padding: 18px 6px;
}

.chat-msg { display: flex; gap: 9px; align-items: flex-start; }
.chat-msg.is-editing { outline: 1px dashed var(--gold, #e6b325); outline-offset: 4px; border-radius: var(--radius-xs); }

.chat-avatar {
  position: relative;
  flex: 0 0 auto;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  overflow: hidden;
  background: linear-gradient(180deg, rgba(60, 30, 96, 0.9), rgba(18, 8, 32, 0.95));
  border: 1px solid rgba(255, 255, 255, 0.18);
  color: #fff;
  font-size: 0.72rem;
  font-weight: 800;
}
.chat-avatar img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.chat-avatar-admin { border-color: var(--gold, #e6b325); box-shadow: 0 0 10px rgba(255, 190, 60, 0.5); }
.chat-avatar-admin img { object-fit: contain; padding: 3px; }

.chat-msg-body { min-width: 0; flex: 1 1 auto; }
.chat-msg-head { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.chat-msg-name {
  font-family: 'Orbitron', sans-serif;
  font-size: 0.68rem;
  font-weight: 800;
  color: var(--cyan-bright, #7bf3ff);
}
.chat-msg.is-system .chat-msg-name { color: var(--gold-bright, #ffd94d); }
.chat-msg-time { font-size: 0.6rem; color: rgba(180, 220, 240, 0.55); }
.chat-edited { font-size: 0.6rem; color: rgba(180, 220, 240, 0.55); font-style: italic; }
.chat-admin-badge {
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  background: linear-gradient(180deg, #ffe9a8, #f0a417);
  color: #3a1d02;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.54rem;
  font-weight: 800;
  letter-spacing: 0.06em;
}
.chat-edit-btn {
  margin-left: auto;
  border: none;
  background: none;
  color: rgba(180, 220, 240, 0.55);
  font-size: 0.6rem;
  cursor: pointer;
  padding: 0 2px;
}
.chat-edit-btn:hover { color: var(--gold-bright, #ffd94d); }

.chat-msg-text {
  margin: 2px 0 0;
  font-size: 0.78rem;
  line-height: 1.4;
  color: #eaf6ff;
  overflow-wrap: anywhere;
}
.chat-msg.is-system .chat-msg-text { color: #ffe9c7; }

.chat-pop { animation: chatPop 380ms cubic-bezier(0.18, 1.2, 0.32, 1) both; }
@keyframes chatPop {
  0%   { opacity: 0; transform: translateY(10px) scale(0.94); }
  60%  { opacity: 1; transform: translateY(-2px) scale(1.02); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

.chat-note {
  margin: 0;
  padding: 7px 12px;
  font-size: 0.68rem;
  color: rgba(180, 220, 240, 0.7);
  border-top: var(--stroke) solid rgba(255, 255, 255, 0.1);
}
.chat-note[hidden] { display: none; }

.chat-composer {
  display: flex;
  gap: 7px;
  padding: 9px 10px;
  border-top: var(--stroke) solid rgba(255, 255, 255, 0.12);
}
.chat-input {
  flex: 1 1 auto;
  min-width: 0;
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  border: var(--stroke) solid rgba(255, 255, 255, 0.18);
  background: rgba(0, 0, 0, 0.4);
  color: #eaf6ff;
  font-size: 0.78rem;
}
.chat-input.is-editing { border-color: var(--gold, #e6b325); }
.chat-input:disabled { opacity: 0.55; cursor: not-allowed; }
.chat-send {
  flex: 0 0 auto;
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  border: var(--stroke) solid var(--edge-gold);
  background: linear-gradient(180deg, #ffe9a8, #f0a417);
  color: #3a1d02;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.66rem;
  font-weight: 800;
  cursor: pointer;
}
.chat-send:disabled { opacity: 0.5; cursor: not-allowed; }

@media (max-width: 560px) {
  .chat-panel { right: 8px; left: 8px; width: auto; bottom: 70px; height: 62vh; }
  .chat-button { right: 10px; bottom: 10px; width: 46px; height: 46px; }
}
@media (prefers-reduced-motion: reduce) {
  .chat-pop { animation: none; }
}

.lean-fx .hud-plus,
.lean-fx .prize-row-value,
.lean-fx .daily-gift-collect,
.lean-fx .chest-done,
.lean-fx .chest-rays,
.lean-fx .chest-ribbon,
.lean-fx .chest-visual,
.lean-fx .gift-icon::before,
.lean-fx .shop-cta-shine,
.lean-fx .spin-btn::after,
.lean-fx .next-theme.is-soon,
.lean-fx .side-frame-core,
.lean-fx .room-mote,
.lean-fx .terror-bg-piece,
.lean-fx .room::after,
.lean-fx .cabinet .machine-frame::after,
.lean-fx .marquee-bulb,
.lean-fx .wheel-gloss::before,
.lean-fx .wheel-rimlights {
  animation: none !important;
}

.lean-fx .room-mote,
.lean-fx .terror-bg-piece:nth-child(n + 4) { display: none; }

.lean-fx .falling-coin { filter: none; }
.lean-fx .machine-frame,
.lean-fx .control-bar,
.lean-fx .hud-stat,
.lean-fx .jackpot-banner,
.lean-fx .next-theme,
.lean-fx .deck-info,
.lean-fx .chat-panel,
.lean-fx .chat-button {
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
}
.lean-fx .hud-stat,
.lean-fx .deck-info,
.lean-fx .jackpot-banner,
.lean-fx .next-theme,
.lean-fx .control-bar,
.lean-fx .chat-panel,
.lean-fx .chat-button { background-color: rgba(12, 5, 24, 0.92); }

.top-pills .hud-value,
.deck-info-val,
.prize-row-value,
.bet-total-value,
#winMeter,
#waysCount {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

@media (max-width: 620px) {
  .deck-info-val, #waysCount, #winMeter { min-width: 3.2em; text-align: right; }
  .top-pills .hud-value { max-width: none; }
}

.lean-fx *,
.lean-fx *::before,
.lean-fx *::after {
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}
.lean-fx .hud-stat,
.lean-fx .deck-info,
.lean-fx .jackpot-banner,
.lean-fx .next-theme,
.lean-fx .control-bar,
.lean-fx .chat-panel,
.lean-fx .chat-button,
.lean-fx .rail-menu > button,
.lean-fx .boosts-dock .rail-tile,
.lean-fx .top-controls-right button,
.lean-fx .terror-timer,
.lean-fx .bet-stepper {
  background-color: rgba(12, 5, 24, 0.94);
}

@media (max-width: 620px) {
  .top-pills .hud-stat {
    padding-left: 8px;
    padding-right: 8px;
    gap: 6px;
  }
  .top-pills .hud-stat-body {
    flex: 1 1 auto;
    min-width: 0;
  }
  .top-pills .hud-value {
    max-width: none;
    overflow: visible;
    text-overflow: clip;
  }
}

@media (max-width: 620px) {
  .top-pills .hud-stat {
    padding-left: 6px;
    padding-right: 6px;
    gap: 5px;
  }
  .top-pills .hud-plus {
    width: 24px;
    height: 24px;
  }
  .top-pills .hud-plus::before { width: 12px; height: 3px; }
  .top-pills .hud-plus::after  { width: 3px;  height: 12px; }
  .top-pills .hud-label {
    font-size: 0.5rem;
    letter-spacing: 0.02em;
  }
  .top-pills .hud-value {
    font-size: clamp(0.78rem, 3.9vw, 1.05rem);
    line-height: 1.05;
  }
  .top-pills .hud-icon-wrap { width: 18px; }
  .top-pills .hud-icon-wrap img { width: 18px; height: 18px; }

  .rail-tile-label,
  #boostsButton .rail-tile-label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  #boostsButton { min-width: 0; }

  .rail-right,
  .rail-left { gap: 6px; }
  .cabinet { row-gap: 6px; }
  .machine-stage { margin-bottom: 0; }
}

@media (max-width: 420px) {
  .top-pills .hud-stat { padding-left: 5px; padding-right: 5px; gap: 4px; }
  .top-pills .hud-plus { width: 22px; height: 22px; }
  .top-pills .hud-plus::before { width: 11px; height: 3px; }
  .top-pills .hud-plus::after  { width: 3px;  height: 11px; }
  .top-pills .hud-label { font-size: 0.44rem; letter-spacing: 0; }
  .top-pills .hud-value { font-size: clamp(0.7rem, 3.5vw, 0.92rem); }
  .top-pills .hud-icon-wrap { width: 16px; }
  .top-pills .hud-icon-wrap img { width: 16px; height: 16px; }
  .top-pills .hud-max { font-size: 0.52rem; }
}

/* =====================================================================
   MACHINE BACKDROP PHOTOGRAPHS
   ---------------------------------------------------------------------
   #roomPhoto is the first child of #room and carries z-index -1. #room
   is itself a stacking context (fixed, z-index -1), so -1 on the child
   puts the photograph ABOVE the room's own purple gradient and BELOW
   .room::before (the starfield) and .room::after (the rays and
   vignette). Every existing room effect keeps painting on top of it,
   which is the whole point: this adds a floor, it does not replace the
   scene.

   The scrim rides in the same background shorthand as the picture
   rather than on the element's opacity, so the machine keeps its
   contrast without dimming the room lighting behind it.
   ===================================================================== */

.room-photo {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 900ms ease-out;
}

body[data-machine="generic"] .room-photo,
body[data-machine="fruits"] .room-photo,
body[data-machine="aliceWonderland"] .room-photo,
body[data-machine="terrorCastle"] .room-photo {
  opacity: 1;
}

/* The three scrims are not interchangeable. vegas.jpg is a bright,
   saturated aerial of the Strip and needs holding back; terror.jpg is
   already a night scene and needs almost nothing; the fruit wallpaper
   is a repeating pattern, which competes with the reels harder than
   either photograph, so it sits between the two. Each was set by eye
   against the machine, not by a shared formula. */

body[data-machine="generic"] .room-photo {
  background:
    linear-gradient(180deg,
      rgba(6, 2, 16, .62) 0%,
      rgba(6, 2, 16, .42) 26%,
      rgba(6, 2, 16, .40) 68%,
      rgba(6, 2, 16, .72) 100%),
    url("/assets/ui/jk1x/vegas.jpg") center / cover no-repeat;
}

body[data-machine="terrorCastle"] .room-photo {
  background:
    linear-gradient(180deg,
      rgba(4, 0, 10, .52) 0%,
      rgba(4, 0, 10, .26) 28%,
      rgba(4, 0, 10, .24) 68%,
      rgba(4, 0, 10, .62) 100%),
    url("/assets/ui/jk1x/terror.jpg") center / cover no-repeat;
}

/* Fruits has a photograph now: a real slot floor. It replaces a
   wallpaper built from six tiled reel PNGs — which was a decent stand-in
   but repeated the machine's own symbols behind the machine, so a cherry
   on the reels and a cherry on the wall were the same picture and the
   eye kept mistaking one for the other.

   It carries the heaviest scrim of the four. vegas.jpg is bright but
   distant; this one is a wall of lit cabinets at close range, and every
   one of them is competing with the reels for exactly the same kind of
   attention. */
body[data-machine="fruits"] .room-photo {
  background:
    linear-gradient(180deg,
      rgba(8, 2, 18, .80) 0%,
      rgba(8, 2, 18, .66) 26%,
      rgba(8, 2, 18, .66) 68%,
      rgba(8, 2, 18, .86) 100%),
    url("/assets/ui/jk1x/slots.jpg") center / cover no-repeat;
}

/* Wonderland was in none of these lists, so its room photo sat at
   opacity 0 for good and the machine played against a bare panel.

   The scrim is asymmetric on purpose. The artwork puts the whole cast in
   a lit cluster on the right and leaves the left two thirds as empty
   navy — so the vertical wash is kept light (the dark half needs no
   help) and a horizontal one holds back the right, which is exactly
   where the jackpot rail sits. */
body[data-machine="aliceWonderland"] .room-photo {
  background:
    linear-gradient(90deg,
      rgba(6, 2, 24, .30) 0%,
      rgba(6, 2, 24, .34) 46%,
      rgba(6, 2, 24, .62) 100%),
    linear-gradient(180deg,
      rgba(6, 2, 24, .54) 0%,
      rgba(6, 2, 24, .22) 30%,
      rgba(6, 2, 24, .26) 70%,
      rgba(6, 2, 24, .66) 100%),
    url("/assets/ui/jk1x/alice.jpg") center / cover no-repeat;
}

/* =====================================================================
   THE SHOP FLOOR
   The red gradients that were the shop's whole background become a
   translucent grade over a photograph. .secret-found is swapped on
   behind closed curtains, so the change is never seen happening.
   ===================================================================== */

.shop-screen {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(140, 20, 30, 0.42), transparent 60%),
    linear-gradient(180deg, rgba(26, 5, 8, .80), rgba(10, 2, 3, .86) 60%, rgba(0, 0, 0, .92)),
    url("/assets/ui/jk1x/bg_shop.jpg") center / cover no-repeat,
    #0a0203;
}

.shop-screen.secret-found {
  background:
    radial-gradient(ellipse at 50% 0%, rgba(90, 20, 140, 0.40), transparent 60%),
    linear-gradient(180deg, rgba(14, 4, 26, .78), rgba(6, 2, 14, .86) 60%, rgba(0, 0, 0, .92)),
    url("/assets/ui/jk1x/bg_shop_alternate.jpg") center / cover no-repeat,
    #06020e;
}

/* =====================================================================
   THE IDLE SECRET
   Nothing here is discoverable by looking. The veil is the only thing
   that ever announces it, and it only exists once the sting is playing.
   ===================================================================== */

/* THE hidden ATTRIBUTE IS LOAD-BEARING. Any author rule that sets
   `display` on these two beats the UA stylesheet's [hidden]{display:none},
   which leaves a full-viewport element at z-index 3000+ sitting invisible
   over the whole game and swallowing every click. Both therefore restate
   [hidden] explicitly, and neither takes pointer events until it is open,
   so a display leak alone can no longer eat a press. */
.idle-secret-veil[hidden],
.idle-secret-popup[hidden] { display: none !important; }

.idle-secret-veil {
  position: fixed;
  inset: 0;
  z-index: 3000;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(ellipse 80% 60% at 50% 45%, rgba(0, 0, 0, .72), rgba(0, 0, 0, .96) 78%);
  transition: opacity 620ms ease-in;
}
.idle-secret-veil.open { opacity: 1; pointer-events: auto; }

/* Each dun lands as a hit of red light from the edges, growing with the
   beat. --dun-step is written by the same code that drives the reels'
   version, so the two read as one effect. */
.idle-secret-veil.beat {
  animation: idleSecretBeat 260ms ease-out;
}
@keyframes idleSecretBeat {
  0% {
    box-shadow: inset 0 0 0 rgba(190, 30, 40, 0);
    background:
      radial-gradient(ellipse 80% 60% at 50% 45%, rgba(0, 0, 0, .72), rgba(0, 0, 0, .96) 78%);
  }
  35% {
    box-shadow: inset 0 0 calc(28px + var(--dun-step, 1) * 26px) rgba(200, 30, 44, .72);
    background:
      radial-gradient(ellipse 80% 60% at 50% 45%, rgba(38, 0, 4, .70), rgba(0, 0, 0, .95) 78%);
  }
  100% {
    box-shadow: inset 0 0 0 rgba(190, 30, 40, 0);
    background:
      radial-gradient(ellipse 80% 60% at 50% 45%, rgba(0, 0, 0, .72), rgba(0, 0, 0, .96) 78%);
  }
}

.shop-inner.idle-secret-jolt {
  animation: idleSecretJolt 260ms ease-out;
}
@keyframes idleSecretJolt {
  0%   { transform: translate3d(0, 0, 0); }
  30%  { transform: translate3d(calc(var(--dun-step, 1) * -3px), calc(var(--dun-step, 1) * 2px), 0); }
  62%  { transform: translate3d(calc(var(--dun-step, 1) * 2px), calc(var(--dun-step, 1) * -1px), 0); }
  100% { transform: translate3d(0, 0, 0); }
}

.idle-secret-popup {
  position: fixed;
  inset: 0;
  z-index: 3100;
  display: grid;
  place-items: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 380ms var(--ease, ease);
}
.idle-secret-popup.open { opacity: 1; pointer-events: auto; }

.idle-secret-card {
  display: grid;
  justify-items: center;
  gap: 14px;
  padding: 30px 38px 26px;
  border: var(--stroke, 2px) solid var(--edge-gold, #ffd94d);
  border-radius: var(--radius-lg, 18px);
  background: linear-gradient(180deg, rgba(30, 8, 52, .96), rgba(10, 2, 20, .98));
  box-shadow: 0 0 60px rgba(0, 0, 0, .9), inset 0 1px 0 rgba(255, 255, 255, .10);
  transform: scale(.9);
  transition: transform 380ms cubic-bezier(.2, 1.5, .4, 1);
}
.idle-secret-popup.open .idle-secret-card { transform: scale(1); }

.idle-secret-logo {
  width: 78px;
  height: auto;
  filter: drop-shadow(0 0 14px rgba(192, 132, 252, .7));
}

.idle-secret-line {
  margin: 0;
  font-family: var(--font-display, 'Orbitron', sans-serif);
  font-size: clamp(1.3rem, 3.4vw, 2rem);
  letter-spacing: .04em;
  color: #fff;
  text-shadow: 0 0 18px rgba(192, 132, 252, .8);
}

.idle-secret-btn {
  padding: 10px 26px;
  border: var(--stroke, 2px) solid var(--edge-gold, #ffd94d);
  border-radius: var(--radius-pill, 999px);
  background: linear-gradient(180deg, #ffd94d, #d69a12);
  color: #2a1600;
  font-family: var(--font-display, 'Orbitron', sans-serif);
  font-weight: 700;
  letter-spacing: .08em;
  cursor: pointer;
}
.idle-secret-btn:hover { filter: brightness(1.08); }

@media (prefers-reduced-motion: reduce) {
  .idle-secret-veil.beat,
  .shop-inner.idle-secret-jolt { animation: none; }
  .idle-secret-card { transition: none; transform: none; }
}

/* =====================================================================
   THE HOLD BUTTON, IN RED
   ---------------------------------------------------------------------
   It was amber, chosen so it would not collide with SPIN. SPIN is gold
   here, not green, so red is free and it is the louder colour — which
   suits a control that is only worth pressing at a specific moment.

   Four states, and they must be told apart at a glance:

     locked    no charges and below the unlock level. Drained of colour
               entirely, so it never reads as merely "not right now".
     ready     the player OWNS holds (level unlock or bought charges) but
               the machine has not offered one yet. Full red with a
               steady halo: the button is yours, it is just waiting.
     offered   owned AND a hold is on the table. The same red, brighter,
               pulsing — the only state that asks to be pressed.
     armed     hold mode on, picking reels. Inverted to a pale face with
               dark ink so it reads as depressed rather than lit.

   These rules live in arcade.css, which loads after holds-tabs-modifiers.css,
   so equal-specificity selectors here win on source order. Nothing in the
   earlier sheet needs deleting.
   ===================================================================== */

.btn.hold {
  --hold-red-bright: #ff5a63;
  --hold-red-mid: #d21421;
  --hold-red-deep: #7a0710;
  --hold-red-edge: #ff9aa1;
  --hold-red-rgb: 255, 45, 63;

  background: linear-gradient(180deg,
    var(--hold-red-bright) 0%,
    var(--hold-red-mid) 50%,
    var(--hold-red-deep) 100%);
  border-color: var(--hold-red-edge);
}

.btn.hold .hold-label {
  color: #fff;
  text-shadow: 0 1px 2px rgba(60, 0, 4, 0.75);
}

.btn.hold .deck-btn-sub {
  color: rgba(255, 226, 228, 0.82);
}

/* Locked: no colour at all. */
.btn.hold.locked {
  background: linear-gradient(180deg, #6e6a63, #4a4741 60%, #2c2a26);
  border-color: rgba(255, 255, 255, 0.25);
}
.btn.hold.locked .deck-btn-sub {
  color: rgba(220, 220, 220, 0.7);
}

/* Ready: owned, not yet offered. A steady halo rather than a pulse —
   the pulse belongs to the moment there is actually something to hold,
   and if both animated the difference would be lost. */
.btn.hold.ready {
  border-color: #ffd0d4;
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, 0.45),
    inset 0 -3px 6px rgba(0, 0, 0, 0.28),
    0 3px 6px rgba(0, 0, 0, 0.35),
    0 0 16px 3px rgba(var(--hold-red-rgb), 0.42);
}

/* Offered: the one state that asks to be pressed. */
@keyframes holdOfferRed {
  0%, 100% {
    box-shadow:
      inset 0 2px 0 rgba(255, 255, 255, 0.45),
      inset 0 -3px 6px rgba(0, 0, 0, 0.28),
      0 3px 6px rgba(0, 0, 0, 0.35),
      0 0 0 0 rgba(var(--hold-red-rgb), 0.75);
  }
  50% {
    box-shadow:
      inset 0 2px 0 rgba(255, 255, 255, 0.45),
      inset 0 -3px 6px rgba(0, 0, 0, 0.28),
      0 3px 6px rgba(0, 0, 0, 0.35),
      0 0 26px 8px rgba(var(--hold-red-rgb), 0.68);
  }
}
.btn.hold.offered {
  background: linear-gradient(180deg, #ff7a82 0%, #e81828 50%, #8d0812 100%);
  border-color: #ffe0e2;
  animation: holdOfferRed 1.1s ease-in-out infinite;
}

/* Armed: pale face, dark ink. Depressed, not lit. */
.btn.hold.armed {
  background: linear-gradient(180deg, #fff0f1, #ff6670 55%, #c00d1a);
  border-color: #fff3f4;
  box-shadow:
    inset 0 3px 8px rgba(90, 0, 6, 0.45),
    0 0 20px 5px rgba(var(--hold-red-rgb), 0.55);
}
.btn.hold.armed .hold-label {
  color: #4a0206;
  text-shadow: none;
}
.btn.hold.armed .deck-btn-sub {
  color: rgba(74, 2, 6, 0.75);
}

/* The charge badge follows the button. */
.btn.hold .hold-count {
  background: #2c0407;
  border-color: #ffb3b8;
  color: #ffd6d9 !important;
}
/* The lock badge stays neutral: it is a requirement, not a charge, and
   it must not read as one more red thing lighting up. */
.btn.hold .hold-count.is-lock {
  background: #2b2b2b;
  border-color: rgba(255, 255, 255, 0.4);
  color: #e6e6e6 !important;
}

/* ---- The reels the button drives ------------------------------------
   Left amber, these chips would sit beside a red button as if they
   belonged to a different control. They are the same feature. */

.reel-col.holdable::after {
  border-color: rgba(255, 90, 99, 0.75);
  color: #ffd0d4;
}
.reel-col.held {
  box-shadow: inset 0 0 0 3px #ff2d3f, 0 0 20px 3px rgba(255, 45, 63, 0.55);
}
.reel-col.held::after {
  background: #ff2d3f;
  border-color: #ffd9dc;
  color: #3a0206;
}
@keyframes heldGlowRed {
  0%, 100% { box-shadow: inset 0 0 0 3px #ff2d3f, 0 0 14px 2px rgba(255, 45, 63, 0.45); }
  50%      { box-shadow: inset 0 0 0 3px #ffc2c6, 0 0 26px 6px rgba(255, 45, 63, 0.72); }
}
.reel-col.was-held {
  animation: heldGlowRed 700ms ease-in-out infinite;
}

/* The shop's miniature of the button has to match it. */
.shop-icon-hold {
  background: linear-gradient(180deg, #ff5a63, #d21421 50%, #7a0710);
  border-color: #ff9aa1;
  color: #fff;
}

@media (prefers-reduced-motion: reduce) {
  .btn.hold.offered,
  .reel-col.was-held { animation: none; }
  .btn.hold.offered {
    box-shadow:
      inset 0 2px 0 rgba(255, 255, 255, 0.45),
      0 0 22px 6px rgba(var(--hold-red-rgb), 0.6);
  }
}

/* The Level metric already shows the level in its value column, so the fixed Lvl
   column was printing it twice — "L80" next to "80". Those rows drop the cell, and
   the grid drops with them. */
.lb-head.lb-no-lvl,
.lb-row.lb-no-lvl {
  grid-template-columns: 2.2rem 1fr 5.5rem;
}

/* =====================================================================
   THE DECK, AFTER Win/Ways
   ---------------------------------------------------------------------
   The right column held a two-line readout pinned to the far edge. It
   holds INVENTORY now, pulled to the INSIDE edge so the button sits
   beside SPIN rather than out on the rail.

   Deliberately NOT inside .deck-col-centre. SPIN is optically centred
   only because the centre grid track is `auto` between two 1fr flanks;
   a second child in there widens the track and shoves SPIN off centre
   by half the new button's width.
   ===================================================================== */
.control-bar .deck-col-right { justify-content: flex-start; }

.control-bar .deck-inventory {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  /* SIZED BY THE WORD, NOT BY A GUESS. This was clamp(104px, 11vw,
     152px), and 152px is not enough: the icon, the gap and the padding
     take 46 of it, and "INVENTORY" at this size needs 130. So the label
     hit its own text-overflow at EVERY width and the button permanently
     read "INVENTOR…" — the same clipping the fixed width was set to
     avoid.

     Letting it size to content is safe here despite the note above about
     shoving SPIN off centre: that only happens to a second child of the
     centre track. This is in the right-hand flank, and the flanks are
     both 1fr, so they stay equal however wide their contents are.
     min-width only keeps the icon-only mobile version from collapsing to
     a sliver. */
  width: auto;
  min-width: clamp(104px, 11vw, 152px);
  min-height: 46px;
  flex: 0 0 auto;
  padding: 8px 10px;
  /* .btn.bag already carries the violet face from mobile-bag-chest.css
     and position:relative + overflow:visible from tutorial.css, which is
     what lets the count badge and the level-up NEW flag sit on the
     corner instead of being clipped. */
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(0.5rem, 0.78vw, 0.68rem);
  font-weight: 700;
  letter-spacing: 0.09em;
}

.control-bar .deck-inventory svg {
  width: 18px;
  height: 18px;
  flex: none;
}

.control-bar .deck-inventory .deck-inventory-label {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The badge is red and rides the corner — .count-badge from tutorial.css
   wins over .bag-count's inline gold pill, which is what we want here. */
.control-bar .deck-inventory .count-badge { top: -7px; right: -7px; }

@media (max-width: 760px) {
  .control-bar .deck-col-right { justify-content: center; }
  .control-bar .deck-inventory {
    width: auto;
    /* Releases the desktop min-width, which is sized to hold the word.
       The word is hidden below, so holding room for it would only make
       an icon button three times wider than its icon. */
    min-width: 0;
    min-height: 38px;
    padding: 6px 12px;
  }
  /* Below the fold the word is the first thing to go: the bag icon and
     the count are enough to identify it, and the deck is fighting for
     every pixel down here. */
  .control-bar .deck-inventory .deck-inventory-label { display: none; }
}

/* Visually hidden but still announced. The deck's wheel button is an
   icon; the words are for anyone not looking at it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* =====================================================================
   THE PRIZE WHEEL BUTTON — an actual wheel
   ---------------------------------------------------------------------
   Round, gold-rimmed, with a ticker at the top and a red count beside
   it. The face is a twelve-wedge conic gradient rather than an image so
   it stays sharp at every size the deck reaches, and so the wedges match
   the real wheel this button opens.

   The radius has to be set HERE and not earlier: arcade.css:4154 is the
   last rule to touch border-radius on .control-bar .btn.wheel, and it
   sets --radius-sm. Anything before this point loses.
   ===================================================================== */
.control-bar .deck-wheel {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(52px, 5.6vw, 74px);
  aspect-ratio: 1;
  min-width: 0;
  flex: 0 0 auto;
  padding: 0;
  border-radius: 50%;
  border-width: 3px;
  border-color: #fff2b8;
  overflow: visible;
  background: radial-gradient(circle at 50% 30%, #ffe9a3, #d99a12 70%, #8c5f04);
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.5),
    inset 0 2px 0 rgba(255, 255, 255, 0.55),
    inset 0 -4px 8px rgba(0, 0, 0, 0.3);
  transition: transform 160ms ease, box-shadow 220ms ease, filter 180ms ease;
}

.control-bar .deck-wheel[hidden] { display: none !important; }

.deck-wheel-disc {
  position: relative;
  display: block;
  width: 74%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 2px solid rgba(60, 30, 0, 0.55);
  background: conic-gradient(
    #3b1470 0deg 30deg, #a8761b 30deg 60deg,
    #3b1470 60deg 90deg, #a8761b 90deg 120deg,
    #3b1470 120deg 150deg, #a8761b 150deg 180deg,
    #3b1470 180deg 210deg, #a8761b 210deg 240deg,
    #3b1470 240deg 270deg, #a8761b 270deg 300deg,
    #3b1470 300deg 330deg, #a8761b 330deg 360deg);
  box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.65);
}

.deck-wheel-hub {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 30%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: #ffe9a3;
  box-shadow: 0 0 0 2px rgba(60, 30, 0, 0.6);
}

/* The ticker, so the button reads as a wheel that stops rather than a
   striped circle. */
.deck-wheel-pointer {
  position: absolute;
  top: 1px;
  left: 50%;
  width: 0;
  height: 0;
  transform: translateX(-50%);
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 9px solid #fff2b8;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7));
}

/* Only turns when there is something to win. A permanently spinning
   button is decoration; one that starts moving when you earn a spin is
   a notification. */
.control-bar .deck-wheel.is-ready {
  box-shadow:
    0 4px 12px rgba(0, 0, 0, 0.5),
    0 0 20px rgba(255, 200, 60, 0.55),
    inset 0 2px 0 rgba(255, 255, 255, 0.55),
    inset 0 -4px 8px rgba(0, 0, 0, 0.3);
}
.control-bar .deck-wheel.is-ready .deck-wheel-disc {
  animation: deckWheelTurn 9s linear infinite;
}
@keyframes deckWheelTurn { to { transform: rotate(360deg); } }

.control-bar .deck-wheel:hover:not(:disabled) {
  transform: translateY(-2px) scale(1.04);
  filter: brightness(1.06);
}
.control-bar .deck-wheel:active { transform: translateY(1px) scale(0.97); }

/* Beside the button, as asked, rather than on the corner: a round face
   has no corner, and top/right -6px on a circle lands the badge in the
   middle of the rim. */
.deck-wheel-count {
  top: 50%;
  right: auto;
  left: calc(100% - 4px);
  transform: translateY(-50%);
  min-width: 20px;
  height: 20px;
  font-size: 0.62rem;
  line-height: 16px;
}

@media (prefers-reduced-motion: reduce) {
  .control-bar .deck-wheel.is-ready .deck-wheel-disc { animation: none; }
}

@media (max-width: 760px) {
  .control-bar .deck-wheel { width: clamp(42px, 12vw, 56px); }
}

/* =====================================================================
   THE SPIN BUTTON — a real dome, and the hold hint under the word
   ---------------------------------------------------------------------
   Two changes, and they depend on each other.

   THE LABEL MOVES INSIDE. "HOLD FOR AUTO SPIN" was pinned at
   `bottom: -12px`, i.e. OUTSIDE the button, floating on the deck below
   it. That is why .control-bar carried 18px of bottom padding it did not
   otherwise need — the padding existed to stop the label falling off the
   deck. Reading it meant looking somewhere the eye had no reason to go,
   and it did not read as belonging to SPIN at all. Inside the dome,
   directly under the word, it is part of the button.

   THE DOME GETS DEEPER. The face was already round and lit; what it was
   missing is the two things that make a physical button read as
   physical — a hard rim the cap sits down inside, and travel when it is
   pressed. Both are here.

   Everything in this block has to come after arcade.css:3282-3391, which
   is where the face is currently built.
   ===================================================================== */

/* The word rides above centre to leave room for the hint under it. */
.control-bar .spin-btn .spin-btn-face {
  padding-bottom: 15%;
  box-shadow:
    0 2px 0 rgba(255, 242, 205, 0.3),
    inset 0 0 0 1px rgba(48, 22, 0, 0.6),
    /* the rim the cap sits inside */
    inset 0 -6px 12px rgba(48, 22, 0, 0.55),
    inset 0 6px 10px rgba(255, 244, 200, 0.35);
}

/* The cap. Lifted off the rim with a real outer shadow so there is a
   visible gap between cap and bezel, which is what sells the depth. */
.control-bar .spin-btn .spin-btn-face::before {
  box-shadow:
    inset 0 4px 0 rgba(255, 255, 255, 0.95),
    inset 0 -18px 26px rgba(110, 52, 0, 0.66),
    inset 0 0 0 2px rgba(255, 236, 176, 0.82),
    0 0 0 1px rgba(66, 32, 0, 0.72),
    0 6px 10px -2px rgba(40, 18, 0, 0.62);
  transition: box-shadow 120ms ease, transform 120ms ease;
}

/* Travel. The cap sinks into the rim and its shadow collapses — the
   whole button does not move, which is what a button actually does. */
.control-bar .spin-btn:active:not(:disabled) .spin-btn-face::before {
  transform: translateY(2px) scale(0.985);
  box-shadow:
    inset 0 2px 0 rgba(255, 255, 255, 0.6),
    inset 0 -8px 16px rgba(110, 52, 0, 0.8),
    inset 0 0 0 2px rgba(255, 236, 176, 0.6),
    0 0 0 1px rgba(66, 32, 0, 0.8),
    0 1px 3px -1px rgba(40, 18, 0, 0.7);
}

/* --- the hint, inside the dome, directly below SPIN ------------------ */
.control-bar .spin-btn-sub {
  top: 61%;
  bottom: auto;
  left: 50%;
  transform: translateX(-50%);
  /* Above .spin-btn-overlay (no z-index of its own) and above the
     travelling shine on .spin-btn::after, so it stays readable during
     free spins and while autoplay is running — which is exactly when it
     has something different to say. */
  z-index: 4;
  max-width: 76%;
  padding: 2px 7px;
  border-radius: var(--radius-pill);
  background: rgba(30, 14, 0, 0.42);
  font-size: clamp(0.34rem, 0.62vw, 0.5rem);
  letter-spacing: 0.06em;
  color: #fff6dc;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.95);
}

/* The reserve under the deck was there to catch the label that used to
   hang below the button. It does not hang any more, so the reels get the
   space back. */
.control-bar { padding-bottom: 8px; }

@media (max-width: 760px) {
  .control-bar .spin-btn-sub {
    top: 60%;
    padding: 1px 5px;
    font-size: clamp(0.3rem, 1.5vw, 0.42rem);
  }
}

/* =====================================================================
   NEXT MACHINE — big enough to be read from where the player is looking
   ---------------------------------------------------------------------
   The card announces the single biggest thing that is about to happen to
   the machine, and it was set at 0.5rem for the label and 0.58rem for
   the name — smaller than the copyright line in the corner. It sat at
   the top of the right rail being ignored.

   Every size here has a matching `.is-soon` rule below it. That variant
   re-declares art, name and time, so raising only the base sizes would
   have made the card SHRINK in the last thirty seconds — exactly when it
   matters most.
   ===================================================================== */
.rail-right .next-theme {
  gap: 4px 11px;
  padding: 11px 13px;
}

.rail-right .next-theme-label {
  font-size: clamp(0.62rem, 0.95vmin, 0.82rem);
  letter-spacing: 0.18em;
  color: var(--violet-bright);
}

.rail-right .next-theme-art,
.rail-right .next-theme.is-soon .next-theme-art {
  width: clamp(46px, 4.4vmin, 82px);
  height: clamp(46px, 4.4vmin, 82px);
}

.rail-right .next-theme-name,
.rail-right .next-theme.is-soon .next-theme-name {
  font-size: clamp(0.82rem, 1.35vmin, 1.4rem);
  letter-spacing: 0.05em;
}

.rail-right .next-theme-time,
.rail-right .next-theme.is-soon .next-theme-time {
  font-size: clamp(1rem, 1.7vmin, 1.9rem);
  letter-spacing: 0.03em;
}

/* The narrow layout drops to one row and hides the label. Keep it
   hidden — there is no room for it there — but let the rest grow. */
@media (max-width: 900px) {
  .rail-right .next-theme { padding: 7px 10px; gap: 2px 9px; }
  .rail-right .next-theme-art,
  .rail-right .next-theme.is-soon .next-theme-art { width: 34px; height: 34px; }
  .rail-right .next-theme-name,
  .rail-right .next-theme.is-soon .next-theme-name { font-size: 0.78rem; }
  .rail-right .next-theme-time,
  .rail-right .next-theme.is-soon .next-theme-time { font-size: 0.95rem; }
}

/* =====================================================================
   PAYLINE NUMBERS, IN THE GUTTERS
   ---------------------------------------------------------------------
   The rails that used to carry these hung at
   `left: clamp(-46px, -3.2vw, -30px)` — OUTSIDE the cabinet. A row marker
   that sits off the machine is not marking anything; you had to look
   away from the reels to read it and then look back to work out which
   row it meant.

   .payline-grid sits inside #reels and mirrors its flex row exactly:
   `padding: inherit` and `gap: inherit` take the reels' own values, so
   five flex:1 cells divide the space the same way the five reel columns
   do, at every breakpoint, with no media queries of its own. Each of
   the first four cells hangs its markers on its right edge, which is a
   gutter; the fifth is empty because five reels have four gutters.
   ===================================================================== */
.payline-rail { display: none; }   /* the old outside rails are gone */

.reels { gap: 6px; }               /* was 2px — room for a marker to sit in */

.payline-grid {
  position: absolute;
  inset: 0;
  padding: inherit;
  gap: inherit;
  display: flex;
  z-index: 6;
  pointer-events: none;
}

.payline-gutter {
  position: relative;
  flex: 1;
  min-width: 0;
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: space-around;
  align-items: flex-end;
}

/* The fifth cell MUST keep its box. `display: none` on it dropped it out
   of the flex row entirely, so the remaining four split the width four
   ways and every marker landed a quarter-column off its gutter. It is
   empty in the markup, which is all the hiding it needs. */

.payline-badge {
  position: relative;
  display: grid;
  place-items: center;
  width: clamp(18px, 1.7vw, 27px);
  height: clamp(20px, 1.9vw, 30px);
  /* Half the badge either side of the column edge, so it straddles the
     gutter. The gutter is only 6px wide, so the marker overlaps a few
     pixels of each neighbouring reel — which is what a real machine's
     line markers do, and why they carry their own dark plate below. */
  transform: translateX(50%);
}

.payline-badge b {
  display: grid;
  place-items: center;
  width: 100%;
  height: 100%;
  clip-path: polygon(50% 0%, 100% 26%, 100% 74%, 50% 100%, 0% 74%, 0% 26%);
  background: linear-gradient(180deg, var(--pl-a), var(--pl-b));
  box-shadow: 0 0 0 6px rgba(0, 0, 0, 0.45);
  font-family: 'Orbitron', sans-serif;
  font-weight: 900;
  font-size: clamp(0.6rem, 0.9vw, 0.9rem);
  color: #fff;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9);
}

/* The connector stubs pointed from the old outside rails toward the
   reels. There is nothing to point at from inside a gutter. */
.payline-badge::after { content: none; }

/* Below 900px the reels are already tight; the markers would be sitting
   on top of the symbols rather than beside them. */
@media (max-width: 900px) {
  .payline-grid { display: none; }
  .reels { gap: 4px; }
}

/* =====================================================================
   THE CLOCK COLUMN
   ---------------------------------------------------------------------
   Castle countdown, Wonderland countdown and the boost clocks, stacked
   under the jackpot banner in the right rail.

   Everything here is undoing the old floating placement. .terror-timer
   was position:fixed at z-index 11850 with a viewport-relative width;
   .alice-timer had auto margins and a 34rem max-width. Both were sized
   for a full-width strip across the top of the screen, and neither would
   sit in a 150-190px rail column without this.
   ===================================================================== */
.rail-clocks {
  display: flex;
  flex-direction: column;
  gap: clamp(5px, 0.7vmin, 10px);
  flex: 0 1 auto;
  min-height: 0;
  min-width: 0;
  width: 100%;
}

/* Collapses completely when every clock is hidden, so an idle rail has
   no gap in it. Mirrors the :has() collapse .spin-cluster used. */
.rail-clocks:not(:has(> :not([hidden]))) { display: none; }

.rail-clocks > .terror-timer,
.rail-clocks > .alice-timer {
  position: static;
  inset: auto;
  transform: none;
  z-index: auto;
  margin: 0;
  width: 100%;
  max-width: none;
  flex: 0 0 auto;
  min-width: 0;
  /* The old rules set this to none because the timers were floating over
     the machine and had to be clicked through. In the rail they are the
     only thing at that position, and they need it for their hover. */
  pointer-events: auto;
}

/* Both become two-row cards in a narrow column: label row, progress row. */
.rail-clocks > .terror-timer {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-areas: "skull text" "bar bar";
  align-items: center;
  gap: 4px 7px;
  padding: 7px 9px 8px;
}

.rail-clocks > .alice-timer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 7px;
  padding: 7px 11px;
  border-radius: var(--radius-md);
}
.rail-clocks > .alice-timer .alice-timer-track { flex: 1 1 100%; }

/* The type was set in absolute rem for a strip that ran most of the way
   across the screen. The rail is a container, so it can size to itself. */
.rail-clocks .terror-timer-text,
.rail-clocks .alice-timer-text {
  font-size: clamp(0.56rem, 4.4cqw, 0.8rem);
  min-width: 0;
}
.rail-clocks .terror-timer-text strong,
.rail-clocks .alice-timer-text strong {
  font-size: clamp(0.72rem, 6cqw, 1.1rem);
}
.rail-clocks .terror-timer-skull,
.rail-clocks .alice-timer-mark {
  font-size: clamp(0.8rem, 5.5cqw, 1.15rem);
}

@media (max-width: 900px) {
  .rail-clocks { flex-direction: row; flex-wrap: wrap; }
  .rail-clocks > .terror-timer,
  .rail-clocks > .alice-timer { width: auto; flex: 1 1 12rem; }
}

/* =====================================================================
   ACTIVE BOOSTS — bigger, and each one says what it is doing
   ---------------------------------------------------------------------
   The chips were set at 0.46rem for the name and 0.52rem for the count —
   seven or eight pixels. A boost you have paid coins for, running on a
   clock, was the smallest text on the screen, and the only explanation
   was a `title` attribute that repeated the chip's own two labels back
   at you ("Lucky — spins left").

   Roughly double the type, a real icon, and a hover card carrying the
   server's own figures. deck-info-boosts.css owns the base sizes, so
   these have to come later.
   ===================================================================== */
.boosts-dock .active-boosts { gap: 7px; padding: 11px; }

.active-boosts .boost-chip {
  gap: 9px;
  padding: 8px 11px;
  border-radius: var(--radius-md);
}

.active-boosts .boost-chip img {
  width: clamp(26px, 2.6vw, 34px);
  height: clamp(26px, 2.6vw, 34px);
}

.active-boosts .boost-chip-label {
  font-size: clamp(0.68rem, 1vw, 0.86rem);
  letter-spacing: 0.04em;
}

.active-boosts .boost-chip-detail {
  font-size: clamp(0.62rem, 0.92vw, 0.78rem);
}

/* --- the hover card ------------------------------------------------- */
.active-boosts .boost-chip.has-tip { position: relative; cursor: help; }

.boost-tip {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 50%;
  transform: translate(-50%, 4px);
  z-index: 90;
  display: flex;
  flex-direction: column;
  gap: 3px;
  width: max-content;
  max-width: 15rem;
  padding: 9px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--cyan-deep, #22e3ff);
  background: linear-gradient(180deg, rgba(10, 26, 32, 0.99), rgba(4, 10, 14, 0.99));
  box-shadow: 0 10px 26px rgba(0, 0, 0, 0.75);
  text-align: left;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity 140ms ease, transform 140ms ease, visibility 140ms;
}

.boost-chip.has-tip:hover .boost-tip,
.boost-chip.has-tip:focus-visible .boost-tip,
.boost-chip.has-tip:focus-within .boost-tip {
  opacity: 1;
  visibility: visible;
  transform: translate(-50%, 0);
}

.boost-tip b {
  font-family: 'Orbitron', sans-serif;
  font-size: 0.74rem;
  letter-spacing: 0.05em;
  color: var(--cyan-bright, #7bf3ff);
}

.boost-tip span {
  font-size: 0.7rem;
  line-height: 1.35;
  color: var(--text, #e8eaed);
}

.boost-tip em {
  font-style: normal;
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--gold-bright, #ffd94d);
}

/* The panel is anchored to the bottom of the rail tile and opens upward;
   a card above the topmost chip would leave the frame. Flip it under. */
.boosts-dock .active-boosts .boost-tip {
  bottom: auto;
  top: calc(100% + 8px);
  transform: translate(-50%, -4px);
}
.boosts-dock .active-boosts .boost-chip.has-tip:hover .boost-tip,
.boosts-dock .active-boosts .boost-chip.has-tip:focus-within .boost-tip {
  transform: translate(-50%, 0);
}

@media (prefers-reduced-motion: reduce) {
  .boost-tip { transition: none; }
}

/* =====================================================================
   THE BOOSTS PANEL STAYS IN ITS OWN RAIL
   ---------------------------------------------------------------------
   It was `width: max-content; max-width: min(46rem, 74vw)` laid out as a
   wrapping ROW, which at a normal desktop size made it 736px wide — it
   started in the left rail and finished most of the way across the
   reels. That was a reasonable trade when the chips were tiny; now that
   they are legible it is most of the machine covered by a popover.

   One chip per row at the rail's own width means it cannot reach the
   reels at all, and the panel scrolls rather than growing if a player
   ever has more running than fits.
   ===================================================================== */
.boosts-dock .active-boosts {
  flex-direction: column;
  flex-wrap: nowrap;
  width: 100%;
  max-width: none;
  max-height: min(44vh, 26rem);
}

.boosts-dock .active-boosts .boost-chip { width: 100%; }

/* In a single column, a card above or below would cover the neighbouring
   chip. Out to the side is the only clear direction — and the reels are
   what is out to the side, which is fine for something that appears on
   hover and leaves with the pointer. */
.boosts-dock .active-boosts .boost-tip {
  top: 50%;
  bottom: auto;
  left: calc(100% + 9px);
  transform: translate(-4px, -50%);
}
.boosts-dock .active-boosts .boost-chip.has-tip:hover .boost-tip,
.boosts-dock .active-boosts .boost-chip.has-tip:focus-within .boost-tip {
  transform: translate(0, -50%);
}

@media (max-width: 900px) {
  /* The rail is a strip down here, not a column; go back to wrapping. */
  .boosts-dock .active-boosts {
    flex-direction: row;
    flex-wrap: wrap;
    width: max-content;
    max-width: min(26rem, 82vw);
  }
  .boosts-dock .active-boosts .boost-chip { width: auto; }
  .boosts-dock .active-boosts .boost-tip {
    top: calc(100% + 8px);
    left: 50%;
    transform: translate(-50%, -4px);
  }
  .boosts-dock .active-boosts .boost-chip.has-tip:hover .boost-tip,
  .boosts-dock .active-boosts .boost-chip.has-tip:focus-within .boost-tip {
    transform: translate(-50%, 0);
  }
}

/* Setting `display` on the clocks above out-specifies the [hidden] rules
   in hud-bet-castle.css and wonderland-castle.css — same specificity,
   later sheet — so a hidden timer came back as a permanently-visible
   "CASTLE OF TERROR — 0:45" on every machine. The guard has to be at
   least as specific as the rules that set display, and it has to sit
   after them. */
.rail-clocks > [hidden] { display: none !important; }

/* =====================================================================
   THE BOOST EXPLANATION EXPANDS THE CHIP; IT DOES NOT FLOAT OVER IT
   ---------------------------------------------------------------------
   A floating card cannot work here. The panel is a scroll container
   (`overflow-y: auto`), and `overflow-y: auto` forces `overflow-x` to
   compute as `auto` too — so an absolutely-positioned card was clipped
   at the panel's edge and never appeared, in any direction. Raising
   z-index does nothing against clipping.

   Growing the chip keeps the whole thing inside the box that is doing
   the clipping, so there is nothing to clip.
   ===================================================================== */
.active-boosts .boost-chip.has-tip { position: relative; }

.active-boosts .boost-tip,
.boosts-dock .active-boosts .boost-tip {
  position: static;
  grid-column: 1 / -1;
  transform: none;
  inset: auto;
  width: auto;
  max-width: none;
  max-height: 0;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  box-shadow: none;
  overflow: hidden;
  opacity: 0;
  visibility: visible;
  transition: max-height 220ms ease, opacity 150ms ease, padding 220ms ease;
}

.active-boosts .boost-chip.has-tip:hover .boost-tip,
.active-boosts .boost-chip.has-tip:focus-within .boost-tip,
.boosts-dock .active-boosts .boost-chip.has-tip:hover .boost-tip,
.boosts-dock .active-boosts .boost-chip.has-tip:focus-within .boost-tip {
  transform: none;
  max-height: 12rem;
  opacity: 1;
  padding: 7px 0 1px;
  border-top: 1px solid rgba(34, 227, 255, 0.28);
  margin-top: 6px;
}

/* The heading is already the chip's own label two lines above it. */
.active-boosts .boost-tip b { display: none; }

.active-boosts .boost-tip span { font-size: 0.68rem; }
.active-boosts .boost-tip em { margin-top: 3px; font-size: 0.6rem; }

@media (prefers-reduced-motion: reduce) {
  .active-boosts .boost-tip { transition: none; }
}

/* =====================================================================
   SETTINGS
   ---------------------------------------------------------------------
   A real modal with a scrim, three tabs and a close that actually
   releases its blockUI. What it replaces was a right-edge drawer holding
   nothing but volume sliders — reached from a SETTINGS tile whose only
   handler forwarded the click to the speaker button next to it.

   Built entirely from the theme tokens (--panel-bg, --panel-bg-2,
   --panel-text, --text-dim, --cyan, --violet), so it re-themes for free
   across all four themes rather than needing a fifth palette.
   ===================================================================== */
.settings-overlay {
  position: fixed;
  inset: 0;
  z-index: 12200;
  display: none;
  align-items: center;
  justify-content: center;
  padding: clamp(10px, 2vh, 28px);
  background: rgba(2, 4, 10, 0.78);
  backdrop-filter: blur(4px);
}
.settings-overlay.open { display: flex; }

.settings-panel {
  display: flex;
  flex-direction: column;
  width: min(34rem, 100%);
  max-height: min(88vh, 46rem);
  overflow: hidden;
  border-radius: var(--radius-lg, 18px);
  border: 2px solid var(--cyan-deep, #22e3ff);
  background: linear-gradient(180deg, var(--panel-bg-2, #10131f), var(--panel-bg, #06070d));
  box-shadow: 0 26px 70px rgba(0, 0, 0, 0.8), 0 0 42px rgba(34, 227, 255, 0.18);
  animation: settingsIn 240ms cubic-bezier(0.2, 1, 0.3, 1) both;
}
@keyframes settingsIn {
  from { opacity: 0; transform: translateY(14px) scale(0.98); }
  to   { opacity: 1; transform: none; }
}

.settings-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 15px 18px 13px;
  border-bottom: 1px solid var(--border-dim, rgba(255, 255, 255, 0.12));
  background: linear-gradient(180deg, rgba(34, 227, 255, 0.10), transparent);
}

.settings-title {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 0;
  font-family: 'Orbitron', sans-serif;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--cyan-bright, #7bf3ff);
}
.settings-title svg { width: 19px; height: 19px; }

.settings-close {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.16));
  background: rgba(0, 0, 0, 0.4);
  color: var(--text-dim);
  font-size: 0.95rem;
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease, transform 150ms ease;
}
.settings-close:hover {
  color: #fff;
  border-color: var(--cyan, #22e3ff);
  transform: rotate(90deg);
}

/* --- tabs ----------------------------------------------------------- */
.settings-tabs {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
}

.settings-tab {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  padding: 9px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius-sm, 8px);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  font-family: 'Orbitron', sans-serif;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color 160ms ease, background 160ms ease, border-color 160ms ease;
}
.settings-tab svg { width: 15px; height: 15px; flex: none; }
.settings-tab:hover { color: #fff; background: rgba(255, 255, 255, 0.08); }
.settings-tab.is-active {
  color: #06212b;
  border-color: var(--cyan-bright, #7bf3ff);
  background: linear-gradient(180deg, var(--cyan-bright, #7bf3ff), var(--cyan, #22e3ff));
}

/* --- body ----------------------------------------------------------- */
.settings-body {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 14px 16px 18px;
  scrollbar-width: thin;
}

.settings-pane { display: none; }
.settings-pane.is-active { display: block; }

/* The sound rows moved in here wholesale. It was a fixed, full-height,
   translateX-ed drawer; in a modal it is simply a pane. */
.settings-overlay .sound-panel {
  position: static;
  transform: none;
  width: auto;
  height: auto;
  max-height: none;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  overflow: visible;
}
.settings-overlay .sound-panel.settings-pane:not(.is-active) { display: none; }
.settings-overlay .sound-panel.settings-pane.is-active { display: block; }

.settings-group + .settings-group { margin-top: 18px; }

.settings-group-title {
  margin: 0 0 4px;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--cyan-bright, #7bf3ff);
}

.settings-group-note {
  margin: 0 0 10px;
  font-size: 0.72rem;
  line-height: 1.4;
  color: var(--text-dim);
}

/* --- theme picker --------------------------------------------------- */
.settings-themes {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 7px;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
}

.settings-themes button {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  width: auto;
  height: auto;
  padding: 11px 4px 9px;
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.14));
  border-radius: var(--radius-sm, 8px);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  cursor: pointer;
  transition: color 160ms ease, border-color 160ms ease, background 160ms ease, transform 160ms ease;
}
.settings-themes button svg { width: 20px; height: 20px; }
.settings-themes .theme-name {
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.06em;
}
.settings-themes button:hover { color: #fff; transform: translateY(-2px); }
.settings-themes button[aria-pressed="true"] {
  color: #fff;
  border-color: var(--violet-bright, #d8b4fe);
  background: linear-gradient(180deg, rgba(168, 85, 247, 0.32), rgba(168, 85, 247, 0.10));
  box-shadow: 0 0 16px rgba(168, 85, 247, 0.35);
}

/* --- switches ------------------------------------------------------- */
.settings-switch {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.035);
  cursor: pointer;
}
.settings-switch + .settings-switch { margin-top: 7px; }

.settings-switch-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.settings-switch-body b {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--panel-text, #eaf6ff);
}
.settings-switch-body small {
  font-size: 0.68rem;
  line-height: 1.35;
  color: var(--text-dim);
}

/* The input is the state; the track is the picture of it. */
.settings-switch input { position: absolute; opacity: 0; pointer-events: none; }

.settings-switch-track {
  position: relative;
  flex: none;
  width: 42px;
  height: 24px;
  border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.18));
  background: rgba(0, 0, 0, 0.5);
  transition: background 180ms ease, border-color 180ms ease;
}
.settings-switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--text-dim, #9fb6cc);
  transition: transform 180ms cubic-bezier(0.2, 1, 0.3, 1), background 180ms ease;
}
.settings-switch input:checked ~ .settings-switch-track {
  border-color: var(--cyan-bright, #7bf3ff);
  background: linear-gradient(90deg, rgba(34, 227, 255, 0.35), rgba(34, 227, 255, 0.6));
}
.settings-switch input:checked ~ .settings-switch-track::after {
  transform: translateX(18px);
  background: #eaffff;
}
.settings-switch input:focus-visible ~ .settings-switch-track {
  outline: 2px solid var(--cyan-bright, #7bf3ff);
  outline-offset: 2px;
}

/* --- action rows ---------------------------------------------------- */
.settings-action {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 11px 12px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.035);
  color: inherit;
  text-align: left;
  font: inherit;
  cursor: pointer;
  transition: border-color 160ms ease, background 160ms ease;
}
.settings-action:hover {
  border-color: var(--cyan, #22e3ff);
  background: rgba(34, 227, 255, 0.08);
}
.settings-action svg { width: 20px; height: 20px; flex: none; color: var(--cyan-bright, #7bf3ff); }
.settings-action-body { display: flex; flex-direction: column; gap: 2px; }
.settings-action-body b { font-size: 0.8rem; color: var(--panel-text, #eaf6ff); }
.settings-action-body small { font-size: 0.68rem; color: var(--text-dim); }

.settings-about { margin: 0; }

@media (max-width: 520px) {
  .settings-panel { width: 100%; max-height: 92vh; }
  .settings-themes { grid-template-columns: repeat(2, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .settings-panel { animation: none; }
  .settings-close:hover { transform: none; }
  .settings-themes button:hover { transform: none; }
  .settings-switch-track::after { transition: none; }
}

/* ---------------------------------------------------------------------
   SETTINGS IN THE LIGHT THEME

   The panel is built from --panel-bg / --panel-bg-2, which the light
   theme turns pale — and the headings were --cyan-bright (#7bf3ff),
   which is close to invisible on pale. Three of the four themes are
   dark, so the dark palette stays the default and light gets the
   corrections rather than the other way round.

   The icon stays cyan: as a shape rather than as text it still reads,
   and it keeps the panel recognisably the same panel in both.
   --------------------------------------------------------------------- */
[data-theme="light"] .settings-overlay {
  background: rgba(20, 24, 40, 0.55);
}

[data-theme="light"] .settings-panel {
  border-color: rgba(20, 30, 60, 0.28);
  box-shadow: 0 26px 70px rgba(20, 30, 60, 0.28);
}

[data-theme="light"] .settings-head {
  background: linear-gradient(180deg, rgba(60, 120, 200, 0.12), transparent);
  border-bottom-color: rgba(20, 30, 60, 0.16);
}

[data-theme="light"] .settings-title,
[data-theme="light"] .settings-group-title {
  color: #17325c;
}

[data-theme="light"] .settings-tabs { border-bottom-color: rgba(20, 30, 60, 0.14); }

[data-theme="light"] .settings-tab {
  background: rgba(20, 30, 60, 0.06);
  color: #4a5b78;
}
[data-theme="light"] .settings-tab:hover {
  background: rgba(20, 30, 60, 0.12);
  color: #17325c;
}
[data-theme="light"] .settings-tab.is-active {
  color: #ffffff;
  border-color: #1d6fb8;
  background: linear-gradient(180deg, #2f92e0, #1d6fb8);
}

[data-theme="light"] .settings-switch,
[data-theme="light"] .settings-action,
[data-theme="light"] .settings-themes button {
  background: rgba(20, 30, 60, 0.05);
  border-color: rgba(20, 30, 60, 0.16);
}
[data-theme="light"] .settings-switch-body b,
[data-theme="light"] .settings-action-body b { color: #17325c; }

[data-theme="light"] .settings-action svg { color: #1d6fb8; }

[data-theme="light"] .settings-themes button { color: #4a5b78; }
[data-theme="light"] .settings-themes button:hover { color: #17325c; }
[data-theme="light"] .settings-themes button[aria-pressed="true"] {
  color: #ffffff;
  border-color: #7a3fd0;
  background: linear-gradient(180deg, #9b5de5, #7a3fd0);
  box-shadow: 0 0 16px rgba(122, 63, 208, 0.3);
}

[data-theme="light"] .settings-switch-track {
  background: rgba(20, 30, 60, 0.16);
  border-color: rgba(20, 30, 60, 0.2);
}
[data-theme="light"] .settings-switch-track::after { background: #ffffff; }
[data-theme="light"] .settings-switch input:checked ~ .settings-switch-track {
  border-color: #1d6fb8;
  background: linear-gradient(90deg, #4aa3e8, #1d6fb8);
}

[data-theme="light"] .settings-close {
  background: rgba(20, 30, 60, 0.06);
  border-color: rgba(20, 30, 60, 0.18);
  color: #4a5b78;
}
[data-theme="light"] .settings-close:hover { color: #17325c; border-color: #1d6fb8; }

/* =====================================================================
   THE DECK BELOW 768px, AND THE RULE THAT BREAKS IT
   ---------------------------------------------------------------------
   base-machine.css has, inside `@media (max-width: 768px)`:

       .control-bar span, .control-bar select { font-size: 1rem; width: 100%; }

   It was written for a deck made of text labels, and it is (0,1,1) —
   which out-specifies any plain class. EVERY <span> inside the control
   bar gets width: 100% on a phone.

   The wheel button is built from spans, so on mobile its 74% disc and
   its 30% hub both became 100%: a 45px button containing a 55px disc
   containing a 49px hub, i.e. a pale blank circle with the wedges
   pushed out of sight. arcade.css already carries a block at :1522
   correcting the font-size half of this leak for the same reason.

   Everything below is (0,3,0) or better so it lands on the right side
   of that rule.
   ===================================================================== */
.control-bar .deck-wheel .deck-wheel-disc {
  width: 74%;
  height: auto;
  aspect-ratio: 1;
}

.control-bar .deck-wheel .deck-wheel-hub {
  width: 30%;
  height: auto;
  aspect-ratio: 1;
}

.control-bar .deck-wheel .deck-wheel-pointer {
  width: 0;
  height: 0;
}

/* Same leak, same fix: a corner badge stretched to the full width of its
   containing block reads as a red bar rather than a count. */
.control-bar .count-badge {
  width: auto;
  min-width: 18px;
}

@media (max-width: 760px) {
  /* Beside the button is out of room down here — the deck is already
     against the screen edge. Back to the corner. */
  .control-bar .deck-wheel .deck-wheel-count {
    top: -6px;
    right: -6px;
    left: auto;
    transform: none;
  }

  /* One height for both, so the pair reads as a pair rather than as two
     controls that happened to land next to each other. */
  .control-bar .deck-col-right {
    gap: 10px;
    align-items: center;
  }
  .control-bar .deck-inventory,
  .control-bar .deck-wheel {
    min-height: 46px;
    height: 46px;
  }
  .control-bar .deck-wheel { width: 46px; }
  .control-bar .deck-inventory { width: 46px; padding: 0; }
}

/* =====================================================================
   DECK TUNING
   ===================================================================== */

/* --- INVENTORY and WHEEL were touching -------------------------------
   .deck-col-left and .deck-col-centre both set a gap; .deck-col-right
   never did, because it used to hold a single readout. With two buttons
   in it the wheel sat flush against the inventory button's right edge
   and crowded its label. */
.control-bar .deck-col-right {
  gap: clamp(10px, 1.1vw, 20px);
}

/* --- ENTER SHOP ------------------------------------------------------
   The one control on the deck that is trying to sell something, and it
   was the smallest thing down there: 138x36, sharing a width clamp with
   a wheel button that has since become a 74px circle somewhere else
   entirely.

   IT MUST SHRINK-WRAP. `width: 100%` here was the whole of the broken
   deck: .deck-shortcuts is a flex sibling of the bet panel and HOLD, so
   claiming the full column left the bet panel at ZERO width, pushed the
   stepper outside its own parent, and slid HOLD underneath the SPIN
   button. The shop button already carries its own width clamp; the
   wrapper only has to get out of the way. */
.control-bar .deck-shortcuts { width: auto; flex: 0 0 auto; }

.control-bar .rail-tile.rail-offer.deck-shortcut {
  width: clamp(122px, 13vw, 208px);
  min-height: clamp(52px, 5.4vh, 74px);
  padding: 8px 12px;
  gap: 9px;
}

.control-bar .rail-offer.deck-shortcut .rail-offer-art {
  width: clamp(30px, 2.6vw, 44px);
  height: clamp(30px, 2.6vw, 44px);
}

.control-bar .rail-offer.deck-shortcut .rail-offer-cta {
  font-size: clamp(0.6rem, 0.95vw, 0.92rem);
  letter-spacing: 0.08em;
  padding: 4px 8px;
}

@media (max-width: 760px) {
  .control-bar .rail-tile.rail-offer.deck-shortcut {
    width: auto;
    min-height: 44px;
    padding: 6px 10px;
  }
  .control-bar .rail-offer.deck-shortcut .rail-offer-art { width: 26px; height: 26px; }
  .control-bar .rail-offer.deck-shortcut .rail-offer-cta { font-size: 0.6rem; }
}

/* --- THE HOLD HINT ---------------------------------------------------
   It has to sit inside the dome, under the word SPIN, and be read at a
   glance. On one line at 18 characters it filled 76% of the button and
   pressed against the bezel on both sides.

   Two lines instead. The same text gets a larger glyph AND clears the
   curve of the dome, because the widest line is now "AUTO SPIN" rather
   than the whole phrase. arcade.css:1072 forces nowrap on this element,
   so the wrap has to be re-enabled here. */
.control-bar .spin-btn-sub {
  white-space: normal;
  max-width: 66%;
  top: 58%;
  padding: 2px 6px;
  font-size: clamp(0.36rem, 0.66vw, 0.55rem);
  line-height: 1.15;
  letter-spacing: 0.04em;
}

@media (max-width: 760px) {
  .control-bar .spin-btn-sub {
    top: 57%;
    max-width: 70%;
    font-size: clamp(0.32rem, 1.7vw, 0.46rem);
  }
}

/* =====================================================================
   THE MYSTERY CHEST SAYS NOTHING
   ---------------------------------------------------------------------
   Every other chest publishes its odds, and that rule is deliberate.
   This one is the single exception, because not knowing is what it
   sells — and it used to be the only mystery in the game whose answer
   you could read before paying.
   ===================================================================== */
.shop-card-loot.is-secret .shop-card-loot-item {
  letter-spacing: 0.32em;
  font-weight: 800;
  color: var(--violet-bright, #d8b4fe);
  text-shadow: 0 0 10px rgba(168, 85, 247, 0.55);
}

.odds-secret {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 18px 8px 6px;
  text-align: center;
}

.odds-secret-mark {
  display: grid;
  place-items: center;
  width: 88px;
  height: 88px;
  border-radius: 50%;
  border: 2px solid var(--violet, #a855f7);
  background: radial-gradient(circle at 50% 32%, rgba(168, 85, 247, 0.34), rgba(10, 4, 22, 0.9) 70%);
  box-shadow: 0 0 28px rgba(168, 85, 247, 0.45), inset 0 0 22px rgba(0, 0, 0, 0.6);
  font-family: 'Orbitron', sans-serif;
  font-size: 2.6rem;
  font-weight: 900;
  color: var(--violet-bright, #d8b4fe);
  text-shadow: 0 0 16px rgba(168, 85, 247, 0.8);
  animation: oddsSecretPulse 3.2s ease-in-out infinite;
}

@keyframes oddsSecretPulse {
  0%, 100% { transform: scale(1);    box-shadow: 0 0 24px rgba(168, 85, 247, 0.4),  inset 0 0 22px rgba(0, 0, 0, 0.6); }
  50%      { transform: scale(1.05); box-shadow: 0 0 40px rgba(168, 85, 247, 0.75), inset 0 0 22px rgba(0, 0, 0, 0.6); }
}

.odds-secret-lead {
  margin: 0;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.86rem;
  letter-spacing: 0.05em;
  color: var(--panel-text, #eaf6ff);
}

.odds-secret .odds-foot { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  .odds-secret-mark { animation: none; }
}

/* =====================================================================
   THE BIG WIN FEED
   ---------------------------------------------------------------------
   A second stream in the chat panel. It reuses the panel's chrome and
   the .chat-list scroll box, and nothing else — the rows are their own
   shape because a win is not a message: there is no avatar, no time, no
   edit, and the number is the point.
   ===================================================================== */
.chat-tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  flex: none;
  padding: 0 10px 8px;
}

.chat-tab {
  position: relative;
  padding: 7px 6px;
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.14));
  border-radius: var(--radius-sm, 8px);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-dim);
  font-family: 'Orbitron', sans-serif;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  cursor: pointer;
  transition: color 150ms ease, background 150ms ease, border-color 150ms ease;
}
.chat-tab:hover { color: #fff; background: rgba(255, 255, 255, 0.09); }
.chat-tab.is-active {
  color: #06212b;
  border-color: var(--cyan-bright, #7bf3ff);
  background: linear-gradient(180deg, var(--cyan-bright, #7bf3ff), var(--cyan, #22e3ff));
}

/* Unread on the feed is a dot, not a count. A number would invite the
   player to read every win; the dot just says the room has been busy. */
.chat-tab-dot {
  position: absolute;
  top: 5px;
  right: 6px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #e2372f;
  box-shadow: 0 0 8px rgba(226, 55, 47, 0.9);
}
.chat-tab-dot[hidden] { display: none; }

.feed-list[hidden] { display: none; }

.feed-row {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.035);
}

.feed-tag {
  align-self: flex-start;
  padding: 2px 7px;
  border-radius: var(--radius-pill, 999px);
  font-family: 'Orbitron', sans-serif;
  font-size: 0.5rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  color: #2a1600;
  background: linear-gradient(180deg, #ffe98a, #ffb800);
}
.feed-row[data-kind="jackpot"] .feed-tag {
  color: #fff;
  background: linear-gradient(180deg, #ff5ad4, #9c1a6e);
}
.feed-row[data-kind="vegas"] .feed-tag {
  color: #fff;
  background: linear-gradient(180deg, #ff2d55, #7e22ce);
  animation: feedVegasPulse 1.8s ease-in-out infinite;
}
@keyframes feedVegasPulse {
  0%, 100% { box-shadow: 0 0 6px rgba(255, 45, 85, 0.5); }
  50%      { box-shadow: 0 0 16px rgba(255, 45, 85, 0.95); }
}

.feed-body {
  font-size: 0.76rem;
  line-height: 1.45;
  color: var(--panel-text, #eaf6ff);
}

.feed-who { color: var(--cyan-bright, #7bf3ff); }
.feed-who.is-anon { color: var(--text-dim); font-style: italic; }

.feed-coins {
  font-family: 'Orbitron', sans-serif;
  color: var(--gold-bright, #ffd94d);
  font-variant-numeric: tabular-nums;
}

.feed-coin {
  width: 13px;
  height: 13px;
  vertical-align: -2px;
  object-fit: contain;
}

.feed-where {
  display: block;
  margin-top: 2px;
  font-size: 0.64rem;
  color: var(--text-dim);
}

@media (prefers-reduced-motion: reduce) {
  .feed-row[data-kind="vegas"] .feed-tag { animation: none; }
}

/* =====================================================================
   AUTO SPIN STATUS
   ---------------------------------------------------------------------
   A run used to be invisible apart from the spin button changing colour,
   so a player who had set "stop after 50 spins" had no way to see how
   far through it was. Sits under the button, in the space the hold hint
   vacated when it moved inside the dome.
   ===================================================================== */
.autoplay-status {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  margin-top: 6px;
  padding: 4px 10px;
  border-radius: var(--radius-pill, 999px);
  border: 1px solid var(--cyan-deep, #22e3ff);
  background: rgba(6, 30, 40, 0.82);
  font-size: clamp(0.44rem, 0.72vw, 0.6rem);
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--cyan-bright, #7bf3ff);
  white-space: nowrap;
}
.autoplay-status[hidden] { display: none; }
.autoplay-status b { color: #fff; font-variant-numeric: tabular-nums; }
.autoplay-status i { opacity: 0.5; font-style: normal; }

.autoplay-status-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--cyan-bright, #7bf3ff);
  animation: autoplayPulse 1.4s ease-in-out infinite;
}
@keyframes autoplayPulse {
  0%, 100% { opacity: 0.35; transform: scale(0.8); }
  50%      { opacity: 1;    transform: scale(1.15); }
}

@media (prefers-reduced-motion: reduce) {
  .autoplay-status-dot { animation: none; opacity: 1; }
}

/* =====================================================================
   SETTINGS: a labelled number field
   ---------------------------------------------------------------------
   The switch rows cover on/off. The auto-spin limits are quantities, and
   0 means "no limit" — so they are number inputs with a placeholder
   rather than a switch plus a field, which would need two controls to
   say one thing.
   ===================================================================== */
.settings-field {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 11px 12px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.035);
  cursor: text;
}
.settings-switch + .settings-field,
.settings-field + .settings-field { margin-top: 7px; }

.settings-field-body {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.settings-field-body b {
  font-size: 0.8rem;
  font-weight: 700;
  color: var(--panel-text, #eaf6ff);
}
.settings-field-body small {
  font-size: 0.68rem;
  line-height: 1.35;
  color: var(--text-dim);
}

.settings-field-input {
  display: flex;
  align-items: baseline;
  gap: 5px;
  flex: none;
}

.settings-field-input input {
  width: 6.2rem;
  padding: 7px 9px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.18));
  background: rgba(0, 0, 0, 0.42);
  color: var(--panel-text, #eaf6ff);
  font-family: 'Orbitron', sans-serif;
  font-size: 0.8rem;
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.settings-field-input input:focus-visible {
  outline: 2px solid var(--cyan-bright, #7bf3ff);
  outline-offset: 1px;
}

.settings-field-unit {
  font-size: 0.66rem;
  color: var(--text-dim);
}

[data-theme="light"] .settings-field {
  background: rgba(20, 30, 60, 0.05);
  border-color: rgba(20, 30, 60, 0.16);
}
[data-theme="light"] .settings-field-body b { color: #17325c; }
[data-theme="light"] .settings-field-input input {
  background: #fff;
  border-color: rgba(20, 30, 60, 0.2);
  color: #17325c;
}

/* =====================================================================
   QUESTS, BATTLE PASS AND ACHIEVEMENTS
   ---------------------------------------------------------------------
   Reuses the settings modal's shell wholesale (.settings-overlay,
   .settings-panel, .settings-tabs, .settings-pane, .settings-group), so
   only the contents need styling. Two modals that behave differently for
   no reason is a cost paid on every visit.
   ===================================================================== */
.quests-panel { width: min(38rem, 100%); }

.quest-timer {
  float: right;
  font-family: inherit;
  font-size: 0.62rem;
  font-weight: 400;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--text-dim);
}

.quest-list { display: flex; flex-direction: column; gap: 8px; }

.quest-empty {
  margin: 0;
  font-size: 0.74rem;
  color: var(--text-dim);
}

.quest-card {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 11px 12px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.035);
  transition: border-color 180ms ease, background 180ms ease;
}

/* Finished-but-unclaimed is the only state that should pull the eye. */
.quest-card.is-done {
  border-color: var(--gold-bright, #ffd94d);
  background: linear-gradient(180deg, rgba(255, 217, 77, 0.12), rgba(255, 217, 77, 0.03));
  box-shadow: 0 0 18px rgba(255, 184, 0, 0.22);
}
.quest-card.is-claimed { opacity: 0.55; }

.quest-card-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 10px;
}

.quest-label {
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--panel-text, #eaf6ff);
}
.quest-label small {
  font-size: 0.68rem;
  font-weight: 400;
  color: var(--text-dim);
}

.quest-tag {
  flex: none;
  padding: 2px 7px;
  border-radius: var(--radius-pill, 999px);
  font-family: 'Orbitron', sans-serif;
  font-size: 0.48rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  color: #06212b;
  background: var(--cyan-bright, #7bf3ff);
}
.quest-tag[data-difficulty="medium"] { background: #c9a8ff; color: #21103d; }
.quest-tag[data-difficulty="hard"]   { background: linear-gradient(180deg, #ffe98a, #ffb800); color: #2a1600; }

.quest-card-bar {
  display: flex;
  align-items: center;
  gap: 9px;
}

/* Shaped like the level bar in the top-left, because that is the bar
   this game already teaches you to read. */
.quest-bar {
  position: relative;
  flex: 1;
  height: 7px;
  border-radius: var(--radius-pill, 999px);
  background: rgba(0, 0, 0, 0.5);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.7);
  overflow: hidden;
}
.quest-bar-fill {
  display: block;
  height: 100%;
  border-radius: inherit;
  background: linear-gradient(90deg, var(--cyan, #22e3ff), var(--cyan-bright, #7bf3ff));
  transition: width 420ms cubic-bezier(0.22, 1, 0.36, 1);
}
.quest-card.is-done .quest-bar-fill {
  background: linear-gradient(90deg, #ffb800, var(--gold-bright, #ffd94d));
}

.quest-figures {
  flex: none;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.62rem;
  font-variant-numeric: tabular-nums;
  color: var(--text-dim);
}

.quest-card-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 26px;
}

.quest-pass {
  font-size: 0.64rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  color: var(--violet-bright, #d8b4fe);
}

.quest-claim {
  margin-left: auto;
  padding: 6px 18px;
  border: none;
  border-radius: var(--radius-pill, 999px);
  cursor: pointer;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.66rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  color: #2a1600;
  background: linear-gradient(180deg, #ffe98a, #ffb800);
  box-shadow: 0 0 16px rgba(255, 184, 0, 0.5);
  transition: transform 140ms ease, box-shadow 140ms ease;
}
.quest-claim:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 0 24px rgba(255, 184, 0, 0.8); }
.quest-claim:active { transform: translateY(1px) scale(0.97); }
.quest-claim:disabled { opacity: 0.5; cursor: default; }

.quest-claimed {
  margin-left: auto;
  font-family: 'Orbitron', sans-serif;
  font-size: 0.6rem;
  letter-spacing: 0.12em;
  color: var(--text-dim);
}

/* --- the battle pass ------------------------------------------------ */
.pass-head { margin-bottom: 12px; }

.pass-tier {
  font-family: 'Orbitron', sans-serif;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  color: var(--text-dim);
}
.pass-tier b { font-size: 1.4rem; color: var(--gold-bright, #ffd94d); }
.pass-tier span { font-size: 0.7rem; }

.pass-bar { display: block; height: 10px; margin: 7px 0 5px; }

.pass-note {
  font-size: 0.7rem;
  color: var(--text-dim);
}

.pass-track { display: flex; flex-direction: column; gap: 6px; }

.pass-row {
  display: grid;
  grid-template-columns: 26px 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.1));
  background: rgba(255, 255, 255, 0.035);
}
.pass-row.is-locked { opacity: 0.45; }
.pass-row.is-claimed { opacity: 0.6; }
.pass-row.is-done {
  border-color: var(--gold-bright, #ffd94d);
  background: linear-gradient(90deg, rgba(255, 217, 77, 0.14), rgba(255, 217, 77, 0.03));
}

/* Tier ten is the reason to finish, so it looks like one. */
.pass-row.is-final {
  border-color: var(--violet-bright, #d8b4fe);
  background: linear-gradient(90deg, rgba(168, 85, 247, 0.2), rgba(168, 85, 247, 0.04));
}
.pass-row.is-final.is-done {
  box-shadow: 0 0 22px rgba(168, 85, 247, 0.45);
}

.pass-row-num {
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid var(--border-dim, rgba(255, 255, 255, 0.2));
  font-family: 'Orbitron', sans-serif;
  font-size: 0.66rem;
  font-weight: 800;
  color: var(--text-dim);
}
.pass-row.is-done .pass-row-num { border-color: var(--gold-bright, #ffd94d); color: var(--gold-bright, #ffd94d); }

.pass-row-art img { width: 34px; height: 34px; object-fit: contain; }

.pass-row-label {
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--panel-text, #eaf6ff);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pass-locked {
  font-family: 'Orbitron', sans-serif;
  font-size: 0.56rem;
  letter-spacing: 0.1em;
  color: var(--text-dim);
}

/* --- the "something is waiting" marks -------------------------------- */
.settings-tab .tab-dot {
  position: absolute;
  top: 5px;
  right: 6px;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: #e2372f;
  box-shadow: 0 0 8px rgba(226, 55, 47, 0.9);
}
.settings-tab .tab-dot[hidden] { display: none; }
.settings-tab { position: relative; }

.rail-menu-item .quests-count { top: 4px; right: 6px; }
.rail-tile-menu.has-quests { border-color: var(--gold-bright, #ffd94d); }

@media (max-width: 520px) {
  .pass-row { grid-template-columns: 22px 28px minmax(0, 1fr) auto; gap: 7px; }
  .pass-row-art img { width: 28px; height: 28px; }
}

/* --- light theme ---------------------------------------------------- */
[data-theme="light"] .quest-card,
[data-theme="light"] .pass-row {
  background: rgba(20, 30, 60, 0.05);
  border-color: rgba(20, 30, 60, 0.16);
}
[data-theme="light"] .quest-label { color: #17325c; }
[data-theme="light"] .pass-row-label { color: #17325c; }
[data-theme="light"] .quest-bar { background: rgba(20, 30, 60, 0.14); }
[data-theme="light"] .quest-pass { color: #7a3fd0; }

/* The composer belongs to the chat tab only. `hidden` alone is not
   enough: .chat-composer sets `display: flex`, and a class rule beats
   the user-agent's `[hidden] { display: none }`. */
.chat-composer[hidden],
.chat-note[hidden] { display: none !important; }

/* =====================================================================
   THE QUESTS TILE, UNDER THE JACKPOT
   ---------------------------------------------------------------------
   In the right rail with the other things that change on their own. It
   is `flex: 0 0 auto` so it keeps its height when a Castle countdown
   appears below it — .rail-clocks is the only child of this rail that
   comes and goes, and it is the one that should absorb the change.
   ===================================================================== */
.rail-quests {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  width: 100%;
  min-width: 0;
  padding: 10px 12px;
  border-radius: var(--radius-md, 12px);
  border: 1px solid var(--violet, #a855f7);
  background: linear-gradient(180deg, rgba(38, 16, 60, 0.88), rgba(16, 8, 26, 0.94));
  box-shadow: 0 0 16px var(--violet-glow, rgba(168, 85, 247, 0.3)),
              inset 0 1px 0 rgba(255, 255, 255, 0.08);
  color: var(--panel-text, #eaf6ff);
  cursor: pointer;
  overflow: visible;
  transition: border-color 180ms ease, box-shadow 200ms ease, transform 160ms ease;
}
.rail-quests:hover { transform: translateY(-2px); border-color: var(--violet-bright, #d8b4fe); }
.rail-quests:active { transform: translateY(1px); }

.rail-quests svg {
  width: clamp(20px, 1.8vmin, 30px);
  height: clamp(20px, 1.8vmin, 30px);
  flex: none;
  color: var(--violet-bright, #d8b4fe);
}

.rail-quests-body {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  text-align: left;
}
.rail-quests-body b {
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(0.6rem, 0.95vmin, 0.85rem);
  font-weight: 800;
  letter-spacing: 0.14em;
  color: var(--violet-bright, #d8b4fe);
}
.rail-quests-body small {
  font-size: clamp(0.52rem, 0.8vmin, 0.72rem);
  color: var(--text-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Something waiting turns it gold, matching the CLAIM buttons inside. */
.rail-quests.has-waiting {
  border-color: var(--gold-bright, #ffd94d);
  box-shadow: 0 0 20px rgba(255, 184, 0, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
}
.rail-quests.has-waiting svg,
.rail-quests.has-waiting .rail-quests-body b { color: var(--gold-bright, #ffd94d); }

.rail-quests .quests-count { top: -7px; right: -7px; }

@media (max-width: 900px) {
  .rail-quests { padding: 7px 9px; gap: 7px; }
  .rail-quests-body small { display: none; }
}

[data-theme="light"] .rail-quests {
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(238, 240, 250, 0.95));
  border-color: rgba(122, 63, 208, 0.55);
}
[data-theme="light"] .rail-quests-body b { color: #5b21a8; }
[data-theme="light"] .rail-quests svg { color: #7a3fd0; }

/* =====================================================================
   THE SERVER ANNOUNCEMENT
   ---------------------------------------------------------------------
   Deliberately NOT dressed as a prize. Every other full-screen panel in
   this game — level up, chest, daily gift, wheel — is a celebration, and
   they all share the gold-and-confetti language. This one is the game
   apologising, so it borrows the layout and none of the party: an amber
   warning tone, no confetti, no rays, one flat button.

   z-index 12500 because "your save was reset" outranks everything a
   player could have open. 1400 was chosen against the celebratory
   overlays alone (chest and level-up, both 1350) — but the full stacking
   table runs far higher than that: chat 11500, wheel 11600, odds and the
   claim panel 11700, the jk1x intro 12000, quests and settings 12200. At
   1400 this would have opened UNDER any of them. It is only safe today
   because it fires on load with nothing else up and takes blockUI, and
   "safe by accident" is not a layer.
   ===================================================================== */
.notice-overlay {
  position: fixed;
  inset: 0;
  z-index: 12500;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(0, 0, 0, 0.92);
  backdrop-filter: blur(4px);
}
.notice-overlay.open { display: flex; }

.notice-modal {
  position: relative;
  width: min(520px, 100%);
  max-height: 90vh;
  overflow-y: auto;
  padding: clamp(22px, 4vw, 34px);
  text-align: center;
  border-radius: var(--radius-lg, 18px);
  border: 1px solid rgba(255, 176, 32, 0.55);
  background: linear-gradient(180deg, rgba(46, 30, 8, 0.96), rgba(16, 10, 4, 0.98));
  box-shadow: 0 24px 70px rgba(0, 0, 0, 0.8),
              0 0 40px rgba(255, 176, 32, 0.18),
              inset 0 1px 0 rgba(255, 255, 255, 0.08);
  animation: noticeIn 420ms cubic-bezier(0.2, 0.9, 0.3, 1) both;
}
@keyframes noticeIn {
  0%   { opacity: 0; transform: translateY(18px) scale(0.96); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}

/* A slow amber breath behind the panel. One moving thing, because the
   panel is asking to be read rather than admired.

   inset: 0, NOT a negative inset. The modal is a scroll container
   (max-height + overflow-y), and in a scroll container a positioned
   child that reaches outside the box becomes scrollable overflow — in
   BOTH axes, because overflow-y: auto makes the x axis auto too. A -40%
   inset here put a horizontal and a vertical scrollbar on a panel whose
   text fitted comfortably. The gradient is widened to compensate, so it
   still reads as a glow rather than a disc. */
.notice-glow {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% 22%,
    rgba(255, 176, 32, 0.18) 0%,
    rgba(255, 176, 32, 0.07) 38%,
    transparent 72%);
  animation: noticeBreathe 5200ms ease-in-out infinite;
}
@keyframes noticeBreathe {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}

.notice-modal > *:not(.notice-glow) { position: relative; z-index: 1; }

.notice-icon {
  width: clamp(44px, 8vw, 58px);
  height: clamp(44px, 8vw, 58px);
  margin: 0 auto 14px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  color: #ffb020;
  border: 1px solid rgba(255, 176, 32, 0.45);
  background: rgba(255, 176, 32, 0.1);
}
.notice-icon svg { width: 58%; height: 58%; }

.notice-title {
  margin: 0 0 12px;
  font-family: 'Orbitron', sans-serif;
  font-size: clamp(1.05rem, 3.2vw, 1.45rem);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: #ffd08a;
  text-shadow: 0 2px 0 rgba(0, 0, 0, 0.5);
}

.notice-body {
  margin: 0 0 14px;
  font-size: clamp(0.84rem, 2.2vw, 0.95rem);
  line-height: 1.65;
  color: var(--panel-text, #eaf6ff);
  /* Left-aligned inside a centred panel: this is a paragraph to read,
     and centred prose of this length is markedly harder to scan. */
  text-align: left;
}

.notice-when {
  margin: 0 0 20px;
  font-size: 0.72rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-dim, #9fb0c0);
}
.notice-when:empty { display: none; }

.notice-dismiss {
  appearance: none;
  cursor: pointer;
  width: 100%;
  min-height: 46px;
  padding: 12px 20px;
  border-radius: var(--radius-sm, 8px);
  border: 1px solid rgba(255, 176, 32, 0.8);
  background: linear-gradient(180deg, #d99418, #8a5c0d);
  color: #fff;
  font-family: 'Orbitron', sans-serif;
  font-weight: 700;
  font-size: 0.86rem;
  letter-spacing: 0.12em;
  transition: transform 140ms ease, background 140ms ease;
}
.notice-dismiss:hover:not(:disabled) {
  transform: translateY(-1px);
  background: linear-gradient(180deg, #f0aa22, #a06d10);
}
.notice-dismiss:active:not(:disabled) { transform: translateY(1px); }
.notice-dismiss:disabled { opacity: 0.55; cursor: default; }
.notice-dismiss:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .notice-modal, .notice-glow { animation: none; }
}

[data-theme="light"] .notice-modal {
  background: linear-gradient(180deg, rgba(255, 250, 240, 0.98), rgba(246, 238, 224, 0.99));
  border-color: rgba(190, 120, 10, 0.5);
}
[data-theme="light"] .notice-title { color: #8a5c0d; }
[data-theme="light"] .notice-body { color: #2a2118; }

/* =====================================================================
   THE LUCKY SPIN'S SPARKLE OPENING
   ---------------------------------------------------------------------
   The jk1x_mode_jingle recording holds for 2.85 seconds before the beat
   drops, and the reels are held still for all of it. This fills that
   window: golden motes streaking across the screen, finishing exactly as
   the reels let go.

   Built and removed by scheduleCueFlourishes; --sparkle-ms is the cue's
   own sparkleUntilMs, so the layer's fade is tied to the recording
   rather than to a number duplicated here.
   ===================================================================== */
.spin-sparkles {
  position: fixed;
  inset: 0;
  z-index: 1200;
  pointer-events: none;
  overflow: hidden;
  animation: sparkleLayerOut var(--sparkle-ms, 2850ms) linear both;
}
@keyframes sparkleLayerOut {
  0%, 82% { opacity: 1; }
  100%    { opacity: 0; }
}

.spin-sparkle {
  position: absolute;
  top: var(--y, 50%);
  left: -6%;
  width: var(--size, 8px);
  height: var(--size, 8px);
  border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #fff 0%, #ffe9a8 35%, #ffb800 70%, transparent 72%);
  box-shadow: 0 0 12px 3px rgba(255, 196, 40, 0.7),
              0 0 30px 8px rgba(255, 160, 0, 0.28);
  opacity: 0;
  animation: sparkleFly var(--dur, 1200ms) cubic-bezier(0.3, 0, 0.5, 1) var(--delay, 0ms) infinite;
}
/* The streak behind it. A pseudo-element rather than a second node, so
   twelve sparkles stay twelve elements. */
.spin-sparkle::after {
  content: '';
  position: absolute;
  top: 50%;
  right: 50%;
  width: 90px;
  height: 2px;
  transform: translateY(-50%);
  background: linear-gradient(270deg, rgba(255, 200, 60, 0.85), transparent);
  border-radius: 2px;
}
@keyframes sparkleFly {
  0%   { opacity: 0; transform: translateX(0) scale(0.5); }
  12%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(112vw) scale(1.05); }
}

@media (prefers-reduced-motion: reduce) {
  .spin-sparkles { display: none; }
}
/* Full-screen compositing on top of a reel animation is the one thing
   the lean path cannot afford. */
.lean-fx .spin-sparkles { display: none; }

/* =====================================================================
   THE BUSY SPINNER
   ---------------------------------------------------------------------
   Worn by any control that has gone to the server — see withBusy. The
   button's own label is faded rather than removed so the control keeps
   its exact width and the layout cannot jump underneath a press.

   ::after rather than a child element: these are applied to buttons all
   over the shop, the bag and the quests board, and none of them should
   have to gain markup to be able to say "working".
   ===================================================================== */
.is-busy {
  position: relative;
  cursor: progress !important;
}
.is-busy > * { opacity: 0.25; }
.is-busy::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.28);
  border-top-color: #fff;
  animation: busySpin 620ms linear infinite;
  pointer-events: none;
  z-index: 4;
}
@keyframes busySpin { to { transform: rotate(360deg); } }

/* Buttons whose label is a bare text node rather than an element — the
   rule above can only fade CHILDREN, so those need the text hidden a
   different way. Colour, not visibility, so the box does not reflow. */
.is-busy { color: transparent !important; text-shadow: none !important; }

@media (prefers-reduced-motion: reduce) {
  /* Still a spinner, still moving — a stationary ring would read as a
     decoration rather than as "wait". Slowed and stepped instead. */
  .is-busy::after { animation: busySpin 1600ms steps(8, end) infinite; }
}

/* THE HIDDEN LIST WAS STILL TAKING HALF THE PANEL.

   .chat-list sets display:flex, and ANY author display rule beats the
   UA stylesheet's [hidden] { display: none }. .feed-list already had an
   explicit [hidden] rule and so vanished properly; #chatList never did,
   so switching to BIG WINS left an empty chat list occupying the top of
   the panel and squeezed the feed into the bottom half. That is the
   "only at the bottom side" complaint, and it was never about height.

   Same failure the chat composer had, fixed the same way. */
.chat-list[hidden] { display: none !important; }

/* The BIG WINS tab now fills the panel (see the height note on
   .chat-panel), which means its empty state has a tall box to sit in
   rather than a collapsed one. Centred, so "no big wins yet" reads as a
   state rather than as a row that failed to load. */
.feed-list:has(.chat-empty),
.chat-list:has(.chat-empty) {
  justify-content: center;
  align-items: center;
  text-align: center;
}
.chat-list .chat-empty { max-width: 22ch; line-height: 1.6; }

/* =====================================================================
   THE SPIN BUTTON ON A TOUCHSCREEN
   ---------------------------------------------------------------------
   Two complaints, one cause and one near it: the screen jumps while
   pressing SPIN, and sometimes the press does not register at all.

   THE PRESS THAT DOES NOT REGISTER. SPIN is a hold-to-autoplay control,
   so it holds the pointer from pointerdown until pointerup — and it was
   set to `touch-action: manipulation`, which still permits PANNING. So
   the moment a finger moved even slightly during that hold, the browser
   claimed the gesture as a scroll, fired pointercancel at the button,
   and swallowed the click that would have followed. A thumb resting on a
   round button is never perfectly still, which is why it was
   intermittent rather than reproducible.

   `none` gives the gesture to the button outright. It is the correct
   value for any control that tracks a press over time, and it is exactly
   what the reel area already uses.

   THE JUMPY SCREEN. body scrolls (overflow-y: auto, min-height 100vh),
   so a drag starting on the deck panned the whole page, and on iOS the
   rubber-band carried on after the finger stopped. overscroll-behavior
   stops the bounce at the ends; the deck no longer initiates a pan at
   all, so the two together hold the layout still under a thumb.
   ===================================================================== */
@media (pointer: coarse) {
  .spin-btn,
  .cabinet-controls .spin-btn { touch-action: none; }

  /* The rest of the deck keeps `manipulation` — those are ordinary taps
     and panning past them is legitimate — but none of it should bounce. */
  .control-bar { overscroll-behavior: contain; }

  body { overscroll-behavior-y: contain; }
}

/* =====================================================================
   THE CLAIM PANEL MUST OUTRANK WHATEVER IT WAS CLAIMED FROM
   ---------------------------------------------------------------------
   .prize-claim is 11700 (boosts-paytable-wheel.css:560), set when only the
   wheel used it — and the wheel closes before the panel appears, so
   nothing was above it.

   It is now also the quests / battle-pass reward panel, and those are
   claimed WITH the board still open at 12200. So the panel opened behind
   the thing that opened it: invisible, and elementFromPoint at its centre
   returned a .quest-card, meaning the CLAIM button could not even be
   pressed. That is the exact failure it was built to fix — the reward
   being announced underneath the panel — reappearing one layer up.

   12600 clears every overlay that can be open at the same time
   (quests/settings 12200, jk1x intro 12000) while staying below the
   machine transition at 20500, which must cover everything.
   ===================================================================== */
.prize-claim { z-index: 12600; }

/* =====================================================================
   [hidden] MEANS HIDDEN. ONE RULE, FOR THE WHOLE APP.
   ---------------------------------------------------------------------
   The UA stylesheet's `[hidden] { display: none }` is the weakest rule
   there is: ANY author rule setting `display` beats it. This codebase
   toggles visibility with the `hidden` property everywhere, and almost
   every one of those elements also carries a class that sets display —
   .chat-list is flex, .quest-list is flex, .shop-grid is grid, and so on.

   That has already been found and patched three separate times, each
   time for one element: the chat composer, the chat list, the Castle
   timer. Auditing the live DOM found 51 MORE elements that would fail to
   hide the same way if anything ever set .hidden on them, plus one that
   already does — #playerAvatarImg, which sits in the HUD as a 48x13 box
   for every player without an avatar.

   Patching them one at a time is how you get to a fourth, fifth and
   sixth. This is the general fix.

   !important is load-bearing here rather than lazy: the rule has to beat
   class-based display rules in 26 stylesheets, including ones that load
   after this file. Nothing in the app wants to render while carrying the
   hidden attribute — that is what the attribute means.
   ===================================================================== */
[hidden] { display: none !important; }

/* =====================================================================
   THE MACHINE PLAQUE HANGS ABOVE ITS STAGE, AND ON A PHONE THE LEFT RAIL
   WAS SITTING IN THAT SPACE
   ---------------------------------------------------------------------
   .machine-plaque is position:absolute with a negative overhang — it is a
   sign hung over the cabinet door, deliberately breaking out of
   .machine-stage's top edge by about 12px. On desktop the rails are down
   the SIDES so nothing is up there.

   Below 900px the rails become a horizontal strip above the cabinet, and
   at 375px the strip ends at y=155 while the plaque starts at y=149. So
   "ACTIVATE YOUR BOOSTS!" and the machine name overlapped by six pixels
   and read as one smudged line.

   Clearance on the rail rather than moving the plaque: the overhang is
   the effect, and the rail has room below it.
   ===================================================================== */
@media (max-width: 900px) {
  .cabinet > .rail-left { margin-bottom: 12px; }
}
