/* Fonts (Oswald, Share Tech Mono) load from a <link> in index.dev.html's
   <head>, not an @import here: an @import is only discovered after this whole
   sheet has downloaded, which chained a second render-blocking round-trip onto
   every cold boot. */

body {
  background: #050505;
  font-family: "Oswald", sans-serif;
  margin: 0;
  /* `clip`, not `hidden`: the CSS overflow spec force-computes the OTHER axis
     (overflow-y, left at its `visible` default) to `auto` whenever one axis is
     `hidden`/`scroll`/`auto` and the other is `visible` — silently turning
     body into a scroll container. That makes body the reference box for every
     `position: sticky`/`fixed` descendant on the page, which breaks their
     "stick to the real viewport" behavior. `clip` is exempt from that
     coercion, so it blocks horizontal bleed without the side effect. */
  overflow-x: clip;
  /* Polish: crisper glyph edges (esp. the condensed Oswald headings) and no
     grey/blue box flashing on every tap on touch devices. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  -webkit-tap-highlight-color: transparent;
}

/* Polish: native controls (checkboxes, number-input spinners, un-themed
   range sliders) pick up the casino gold instead of the OS default blue. */
:root {
  accent-color: #f59e0b;
}

/* Polish: text selection uses the brand gold rather than the browser's default
   blue, so highlighting a username / amount stays on-theme. */
::selection {
  background: rgba(251, 191, 36, 0.32);
  color: #fff;
}
::-moz-selection {
  background: rgba(251, 191, 36, 0.32);
  color: #fff;
}

/* Polish: chrome you tap repeatedly (SPIN, bet +/-, nav) shouldn't let the
   label get text-selected on a fast double-tap. Real text inputs opt back in. */
button,
[role="button"],
.no-select {
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}
input,
textarea,
[contenteditable] {
  -webkit-user-select: text;
  -moz-user-select: text;
  user-select: text;
}

/* Polish: a disabled button should read as un-pressable on hover. */
button:disabled,
button[disabled] {
  cursor: not-allowed;
}

#chromatic-bg {
  position: fixed;
  top: 0;
  left: 0;
  width: 100vw;
  height: 100vh;
  z-index: -1;
  pointer-events: none;
}

.font-mono-game {
  font-family: "Share Tech Mono", monospace;
}

/* Polish: keep an over-scrolled modal list from rubber-banding the whole page
   behind it (mobile), give iOS momentum scrolling, and hand Firefox the same
   slim dark scrollbar WebKit already gets below. */
.custom-scrollbar {
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.custom-scrollbar::-webkit-scrollbar {
  width: 6px;
}

.custom-scrollbar::-webkit-scrollbar-track {
  background: rgba(0, 0, 0, 0.2);
  border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.1);
  border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.2);
}

@keyframes spin-blur {

  0%,
  100% {
    filter: blur(0px) brightness(1);
  }

  50% {
    filter: blur(3px) brightness(1.4);
  }
}

@keyframes reel-roll {
  0% {
    transform: translateY(-50%);
  }

  100% {
    transform: translateY(0);
  }
}

@keyframes tilt-pulse {

  0%,
  100% {
    box-shadow: 0 0 30px 5px rgba(202, 138, 4, 0.2);
  }

  50% {
    box-shadow: 0 0 60px 15px rgba(202, 138, 4, 0.5);
  }
}

@keyframes win-glow {

  0%,
  100% {
    text-shadow: 0 0 10px currentColor;
  }

  50% {
    text-shadow:
      0 0 30px currentColor,
      0 0 60px currentColor;
  }
}

@keyframes float-up {
  0% {
    opacity: 1;
    transform: translateY(0);
  }

  100% {
    opacity: 0;
    transform: translateY(-60px);
  }
}

/* (bounce-stop keyframes live further down, next to rain-down — a second
   px-based copy here used to shadow-fight the %-based one.) */

@keyframes win-pop {
  0% {
    transform: scale(0.3);
    opacity: 0;
  }

  50% {
    transform: scale(1.1);
    opacity: 1;
  }

  100% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes win-pulse-scale {

  0%,
  100% {
    transform: scale(1);
  }

  50% {
    transform: scale(1.05);
  }
}

@keyframes super-win-pop {
  0% {
    transform: scale(0.5);
    opacity: 0;
  }

  50% {
    transform: scale(1.15);
    opacity: 1;
  }

  100% {
    transform: scale(1);
    opacity: 1;
  }
}

@keyframes ultra-win-pop {
  0% {
    transform: scale(0.3) rotate(-15deg);
    opacity: 0;
  }

  30% {
    transform: scale(1.2) rotate(10deg);
    opacity: 1;
  }

  50% {
    transform: scale(0.9) rotate(-5deg);
    opacity: 1;
  }

  70% {
    transform: scale(1.1) rotate(5deg);
    opacity: 1;
  }

  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

.animate-ultra-win {
  animation:
    ultra-win-pop 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards,
    win-pulse-scale 1s ease-in-out 0.8s infinite;
}

.animate-super-win {
  animation:
    super-win-pop 0.7s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards,
    win-pulse-scale 1.2s ease-in-out 0.7s infinite;
}

.animate-win-pop {
  animation:
    win-pop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards,
    win-pulse-scale 1.5s ease-in-out 0.6s infinite;
}

/* MEGA sits between BIG (win-pop) and SUPER (super-win-pop): a punchier pop with
   a little settle dip so it reads as a distinct step up, not a clone of SUPER. */
@keyframes mega-win-pop {
  0%   { transform: scale(0.4);  opacity: 0; }
  45%  { transform: scale(1.13); opacity: 1; }
  72%  { transform: scale(0.97); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}

.animate-mega-win {
  animation:
    mega-win-pop 0.65s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards,
    win-pulse-scale 1.35s ease-in-out 0.65s infinite;
}

/* Big-win headline: slams in from above (rotated + oversized), overshoots
   back to upright for impact, then settles into a continuous pulse/wobble. */
@keyframes big-win-impact {
  0% {
    transform: translateY(-30px) scale(1.5) rotate(-12deg);
    opacity: 0;
  }

  45% {
    transform: translateY(0) scale(0.92) rotate(7deg);
    opacity: 1;
  }

  70% {
    transform: scale(1.05) rotate(-3deg);
    opacity: 1;
  }

  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

@keyframes big-win-pulse {

  0%,
  100% {
    transform: scale(1) rotate(0deg);
  }

  50% {
    transform: scale(1.05) rotate(1.5deg);
  }
}

.animate-big-win-text {
  animation:
    big-win-impact 0.75s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards,
    big-win-pulse 1.4s ease-in-out 0.75s infinite;
}

/* Swirling themed light ring drawn around the reels while a slot feature
   applies mid-spin. Color comes from per-slot --swirl-color / --swirl-rgb. */
@property --swirl-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

@keyframes feature-swirl {
  to {
    --swirl-angle: 360deg;
  }
}

.feature-swirl-border {
  --swirl-color: #ffffff;
  --swirl-rgb: 255, 255, 255;
  position: absolute;
  inset: -4px;
  border-radius: 14px;
  padding: 5px;
  pointer-events: none;
  z-index: 0;
  background: conic-gradient(from var(--swirl-angle),
      transparent 0deg,
      var(--swirl-color) 60deg,
      #ffffff 90deg,
      var(--swirl-color) 120deg,
      transparent 180deg,
      var(--swirl-color) 240deg,
      #ffffff 270deg,
      var(--swirl-color) 300deg,
      transparent 360deg);
  -webkit-mask:
    linear-gradient(#fff 0 0) content-box,
    linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  filter:
    drop-shadow(0 0 4px var(--swirl-color)) drop-shadow(0 0 9px rgba(var(--swirl-rgb), 0.6));
  animation: feature-swirl 2.4s linear infinite;
}

.animate-tilt-pulse {
  animation: tilt-pulse 1.5s ease-in-out infinite;
}

/* Static glow + an opacity-only breath: animating text-shadow repaints the
   text every frame; opacity is composited. */
.animate-win-glow {
  text-shadow: 0 0 14px currentColor;
  animation: win-glow-breathe 1s ease-in-out infinite;
}
@keyframes win-glow-breathe { 0%, 100% { opacity: 0.85; } 50% { opacity: 1; } }

.animate-float-up {
  animation: float-up 2s ease-out forwards;
}

/* Note: no static transform here — the reel-roll animation owns `transform`
   while spinning (any scaleY set alongside it is dead-on-arrival and would
   also break the seamless -50% wrap). will-change promotes the strip to its
   own compositor layer BEFORE the first frame, so spin-up doesn't hitch. */
/* No filter on the moving strip: a filter on a transform-animated layer is
   re-applied by the compositor on every frame of every spin (five strips at
   once). The symbols inside the strip already carry their own static blur
   (SymbolCell, isSpinning), which rasters once with the layer. */
.reel-spinning .reel-strip {
  animation: reel-roll 0.12s linear infinite;
  will-change: transform;
}

.reel-spinning-fast .reel-strip {
  animation: reel-roll 0.05s linear infinite;
  will-change: transform;
}

.symbol-win {
  animation: win-glow 0.8s ease-in-out infinite;
  box-shadow: 0 0 20px currentColor;
}

@keyframes win-float {

  0%,
  100% {
    transform: translateY(0) scale(1.05);
  }

  50% {
    transform: translateY(-8px) scale(1.08);
  }
}

@keyframes win-shimmer {
  0% {
    filter: brightness(1) drop-shadow(0 0 4px rgba(251, 191, 36, 0.3));
  }

  25% {
    filter: brightness(1.4) drop-shadow(0 0 12px rgba(251, 191, 36, 0.8));
  }

  50% {
    filter: brightness(1.1) drop-shadow(0 0 6px rgba(251, 191, 36, 0.5));
  }

  75% {
    filter: brightness(1.5) drop-shadow(0 0 16px rgba(255, 255, 255, 0.6));
  }

  100% {
    filter: brightness(1) drop-shadow(0 0 4px rgba(251, 191, 36, 0.3));
  }
}

@keyframes win-border-pulse {

  0%,
  100% {
    border-color: rgba(251, 191, 36, 0.6);
    box-shadow:
      0 0 15px 2px rgba(251, 191, 36, 0.3),
      inset 0 0 8px rgba(251, 191, 36, 0.1);
  }

  50% {
    border-color: rgba(255, 255, 255, 0.9);
    box-shadow:
      0 0 30px 6px rgba(251, 191, 36, 0.6),
      inset 0 0 15px rgba(251, 191, 36, 0.2);
  }
}

/* A winning tile floats (transform) and its rim pulses on an opacity-only
   layer. The old filter (win-shimmer) and border/box-shadow (win-border-pulse)
   keyframes repainted every winning tile on every frame of the win, and the
   animated box-shadow also overrode the skin's own themed win glow (set
   inline by SymbolCell), which now shows. */
.symbol-winning {
  animation: win-float 1.2s ease-in-out infinite;
  z-index: 10;
  position: relative;
}
.symbol-winning::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.75), inset 0 0 16px rgba(255, 255, 255, 0.16);
  animation: win-rim-pulse 1s ease-in-out infinite;
}
@keyframes win-rim-pulse {
  0%, 100% { opacity: 0.25; }
  50%      { opacity: 1; }
}
.reduced-motion .symbol-winning::after { animation: none; opacity: 0.6; }

@keyframes win-fade-out {
  0% {
    opacity: 1;
  }

  70% {
    opacity: 1;
  }

  100% {
    opacity: 0;
  }
}

.win-popup-auto-hide {
  animation: win-pop 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}

@keyframes cash-lock {
  0% {
    transform: scale(0.3) rotate(-10deg);
    opacity: 0;
  }

  50% {
    transform: scale(1.2) rotate(5deg);
    opacity: 1;
  }

  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

.animate-cash-lock {
  animation: cash-lock 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}

@keyframes heist-pulse {

  0%,
  100% {
    box-shadow: 0 0 8px rgba(251, 191, 36, 0.3);
  }

  50% {
    box-shadow: 0 0 25px rgba(251, 191, 36, 0.7);
  }
}

.heist-cell-locked {
  animation: heist-pulse 1.5s ease-in-out infinite;
}

::-webkit-scrollbar {
  width: 6px;
}

::-webkit-scrollbar-track {
  background: #1c1917;
}

::-webkit-scrollbar-thumb {
  background: #57534e;
  border-radius: 3px;
}

@keyframes spin-background {
  from {
    transform: translate(-50%, -50%) rotate(0deg);
  }

  to {
    transform: translate(-50%, -50%) rotate(360deg);
  }
}

@keyframes shimmer-edge {
  0% {
    transform: translateX(-100%);
  }

  100% {
    transform: translateX(100%);
  }
}

.jackpot-card {
  position: relative;
  overflow: hidden;
}

.jackpot-card::before {
  content: "";
  position: absolute;
  z-index: 0;
  left: -50%;
  top: -50%;
  width: 200%;
  height: 200%;
  background-image: conic-gradient(from 90deg at 50% 50%,
      transparent 0%,
      transparent 35%,
      rgba(255, 255, 255, 0.4) 50%,
      transparent 65%,
      transparent 100%);
  animation: spin-background 6s linear infinite;
  filter: blur(12px);
}

.jackpot-card::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  background: linear-gradient(110deg,
      transparent 0%,
      transparent 30%,
      rgba(255, 255, 255, 0.25) 50%,
      transparent 70%,
      transparent 100%);
  animation: shimmer-edge 4s ease-in-out infinite;
  transform: translateX(-100%);
  pointer-events: none;
}

.jackpot-grand {
  box-shadow:
    0 0 15px 2px rgba(251, 191, 36, 0.4),
    0 0 30px 5px rgba(239, 68, 68, 0.2);
}

.jackpot-grand::before {
  animation-duration: 2s;
  background-image: conic-gradient(from 90deg at 50% 50%,
      transparent 0%,
      transparent 35%,
      rgba(255, 215, 0, 0.6) 50%,
      transparent 65%,
      transparent 100%);
}

.jackpot-grand::after {
  animation-duration: 2.5s;
  background: linear-gradient(110deg,
      transparent 0%,
      transparent 30%,
      rgba(255, 255, 255, 0.6) 50%,
      transparent 70%,
      transparent 100%);
}

@keyframes floaty-hover {

  0%,
  100% {
    transform: translateY(0);
  }

  50% {
    transform: translateY(-20px);
  }
}

@keyframes rhythmic-pulse {

  0%,
  100% {
    transform: scale(1);
    filter: brightness(1);
  }

  50% {
    transform: scale(1.05);
    filter: brightness(1.3);
  }
}

@keyframes particle-drift {
  0% {
    transform: translate(0, 0) scale(1);
    opacity: 0;
  }

  20% {
    opacity: 0.8;
  }

  80% {
    opacity: 0.8;
  }

  100% {
    transform: translate(var(--tx), var(--ty)) scale(0.5);
    opacity: 0;
  }
}

@keyframes overlay-fade-in {
  0% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

@keyframes xp-fill {
  from {
    width: var(--xp-from, 0%);
  }

  to {
    width: var(--xp-to, 100%);
  }
}

@keyframes level-up-pop {
  0% {
    transform: scale(0.3) rotate(-10deg);
    opacity: 0;
  }

  40% {
    transform: scale(1.3) rotate(5deg);
    opacity: 1;
  }

  60% {
    transform: scale(0.9) rotate(-3deg);
    opacity: 1;
  }

  80% {
    transform: scale(1.05) rotate(1deg);
    opacity: 1;
  }

  100% {
    transform: scale(1) rotate(0deg);
    opacity: 1;
  }
}

@keyframes tier-shimmer {
  0% {
    background-position: -200% center;
  }

  100% {
    background-position: 200% center;
  }
}

@keyframes tier-card-entrance {
  0% {
    opacity: 0;
    transform: translateY(30px) scale(0.9);
  }

  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes vip-glow-pulse {

  0%,
  100% {
    box-shadow: 0 0 8px var(--tier-glow, rgba(205, 127, 50, 0.4));
  }

  50% {
    box-shadow: 0 0 20px var(--tier-glow, rgba(205, 127, 50, 0.6));
  }
}

.animate-level-up {
  animation: level-up-pop 0.8s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}

.animate-tier-shimmer {
  background-size: 200% auto;
  animation: tier-shimmer 3s linear infinite;
}

.animate-vip-glow {
  animation: vip-glow-pulse 2s ease-in-out infinite;
}

/* ============================================================
   MULTI-SLOT THEME SYSTEM (CSS Custom Properties)
   ============================================================ */
:root {
  --theme-primary: #fbbf24;
  --theme-bg: #050505;
  --theme-accent: #daa520;
  --theme-glow: rgba(251, 191, 36, 0.4);
  --theme-border: rgba(161, 98, 7, 0.5);
}

/* ============================================================
   ANIMATED NUMBER POP (count-up win feedback)
   ============================================================ */
@keyframes num-pop {
  0%   { transform: scale(1); }
  30%  { transform: scale(1.22); filter: brightness(1.5) drop-shadow(0 0 6px currentColor); }
  100% { transform: scale(1); }
}
.animate-num-pop {
  display: inline-block;
  animation: num-pop 0.5s ease-out;
}

/* ============================================================
   PLAY-NOW BUTTON BORDER LIGHT
   ============================================================ */
@keyframes border-spin {
  from { transform: translate(-50%, -50%) rotate(0deg); }
  to   { transform: translate(-50%, -50%) rotate(360deg); }
}

.play-btn-wrap {
  position: relative;
  border-radius: 0.75rem;
  padding: 3px;
  overflow: hidden;
}

/* Spinning conic-gradient — only visible in the padding ring because
   .play-btn-inner's opaque background covers the center.
   aspect-ratio:1 keeps the gradient a true square so the light doesn't
   stretch/accelerate across the button's long edges. */
.play-btn-wrap::before {
  content: '';
  position: absolute;
  left: 50%; top: 50%;
  width: 250%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%) rotate(0deg);
  background: conic-gradient(
    from 0deg,
    transparent 0deg,
    transparent 165deg,
    var(--btn-color2, #fbbf24) 210deg,
    var(--btn-color, #fbbf24) 260deg,
    #ffffff 305deg,
    var(--btn-color, #fbbf24) 340deg,
    transparent 360deg
  );
  filter: blur(1.5px) saturate(1.5) drop-shadow(0 0 4px var(--btn-color, #fbbf24));
  opacity: 0;
  /* Paused while invisible: it used to spin (with a blur filter) on every
     card at opacity 0. Hover or .play-btn-lit starts it. */
  animation: border-spin 4s linear infinite paused;
  transition: opacity 0.3s ease;
}
.lobby-tile:hover .play-btn-wrap::before,
.group:hover .play-btn-wrap::before,
.play-btn-lit::before { animation-play-state: running; }

.lobby-tile:hover .play-btn-wrap::before {
  opacity: 1;
  filter: blur(1px) saturate(1.8) brightness(1.3) drop-shadow(0 0 7px var(--btn-color, #fbbf24));
}

/* Must be opaque — any alpha here lets the light bleed through the center */
.play-btn-inner {
  position: relative;
  z-index: 1;
  border-radius: calc(0.75rem - 3px);
  overflow: hidden;
}

/* ============================================================
   LOBBY STYLES
   ============================================================ */
@keyframes lobby-float {

  0%,
  100% {
    transform: translateY(0px);
  }

  50% {
    transform: translateY(-12px);
  }
}

@keyframes lobby-glow-pulse {

  0%,
  100% {
    box-shadow: 0 0 20px var(--card-glow, rgba(251, 191, 36, 0.2)), 0 8px 32px rgba(0, 0, 0, 0.5);
  }

  50% {
    box-shadow: 0 0 40px var(--card-glow, rgba(251, 191, 36, 0.4)), 0 8px 32px rgba(0, 0, 0, 0.5);
  }
}

@keyframes lobby-shimmer {
  0% {
    background-position: -200% center;
  }

  100% {
    background-position: 200% center;
  }
}

@keyframes lobby-entrance {
  0% {
    opacity: 0;
    transform: translateY(40px) scale(0.9);
  }

  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

/* Entrance uses fill-mode `backwards` (not `both`): it still hides the card
   during its staggered delay, but releases `transform` once finished. With
   `both`, the filled final keyframe kept overriding the hover tilt (CSS
   animations outrank inline styles), so cards never lifted under the cursor. */
.lobby-card {
  animation: lobby-entrance 0.6s ease-out backwards;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

/* THE HOVER TARGET IS THE WRAPPER (.lobby-tile), NOT THE CARD.
   The lift below moves the card up and out from under the pointer. While the
   card was also the thing being hovered, that fed back on itself — lift,
   un-hover, drop, hover — and the tile juddered while the emblem animation
   flickered on and off. The wrapper does not move when the card lifts, so the
   state is stable; and since the lifted card is still inside the wrapper, the
   hover region is the union of both boxes rather than a smaller one.
   A small cushion (below) absorbs the parallax drift on top of that.

   While hovered, stop the looping box-shadow pulse and lift the card with a
   themed glow (--card-glow is set inline per machine). This is the ONLY
   hover transform — a single source, so the motion always settles cleanly. */
.lobby-tile { position: relative; }
/* Invisible hover cushion. The parallax slides tiles by up to ~34px as the
   pointer moves, so without a little slack a card can drift out from under a
   near-stationary cursor and drop the hover mid-animation. 6px keeps a 12px
   neutral gutter inside the grid's 24px gap, so neighbouring cushions never
   touch and hover is never ambiguous. */
.lobby-tile::before {
  content: '';
  position: absolute;
  inset: -6px;
  border-radius: 1.25rem;
}
.lobby-tile:hover .lobby-card {
  animation-name: lobby-entrance, none;
  transform: translateY(-8px) scale(1.03);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55), 0 0 26px var(--card-glow, rgba(251, 191, 36, 0.25));
  z-index: 10;
}
/* The diagonal sheen sweep only runs WHILE hovered — it used to animate
   perpetually behind an opacity-0 layer, repainting every card off-screen. */
.lobby-tile:hover .lobby-sheen { animation: lobby-shimmer 2s linear infinite; }
.reduced-motion .lobby-sheen { animation: none !important; }

/* ============================================================
   LOGIN SCREEN ANIMATIONS
   ============================================================ */
@keyframes login-particle-float {
  0%, 100% {
    opacity: 0;
    transform: translateY(0) scale(0.8);
  }
  15% {
    opacity: 0.12;
  }
  50% {
    opacity: 0.18;
    transform: translateY(-40px) scale(1.1) rotate(15deg);
  }
  85% {
    opacity: 0.08;
  }
}

@keyframes login-card-enter {
  0% {
    opacity: 0;
    transform: translateY(30px) scale(0.95);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

@keyframes login-form-slide {
  0% {
    opacity: 0;
    transform: translateX(20px);
  }
  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes login-shake {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); }
  40% { transform: translateX(6px); }
  60% { transform: translateX(-4px); }
  80% { transform: translateX(2px); }
}

@keyframes login-glow-pulse {
  0%, 100% { opacity: 0.5; transform: scale(1); }
  50% { opacity: 1; transform: scale(1.15); }
}

/* ============================================================
   LAVA THEME ANIMATIONS
   ============================================================ */
@keyframes lava-bubble {
  0% {
    transform: translateY(0) scale(1);
    opacity: 0.6;
  }

  50% {
    transform: translateY(-20px) scale(1.3);
    opacity: 0.9;
  }

  100% {
    transform: translateY(-40px) scale(0.8);
    opacity: 0;
  }
}

@keyframes lava-heat-shimmer {

  0%,
  100% {
    filter: blur(0px) brightness(1);
    transform: scaleX(1);
  }

  25% {
    filter: blur(0.5px) brightness(1.1);
    transform: scaleX(1.002);
  }

  50% {
    filter: blur(0px) brightness(1.05);
    transform: scaleX(0.998);
  }

  75% {
    filter: blur(0.5px) brightness(1.08);
    transform: scaleX(1.001);
  }
}

@keyframes lava-flow {
  0% {
    background-position: 0% 0%;
  }

  100% {
    background-position: 200% 200%;
  }
}

@keyframes ember-drift {
  0% {
    transform: translate(0, 0) scale(1);
    opacity: 0;
  }

  15% {
    opacity: 0.9;
  }

  85% {
    opacity: 0.6;
  }

  100% {
    transform: translate(var(--ember-tx, 30px), var(--ember-ty, -80px)) scale(0.3);
    opacity: 0;
  }
}

@keyframes magma-meter-fill {
  0% {
    background-position: 0% 100%;
  }

  100% {
    background-position: 0% 0%;
  }
}

@keyframes magma-glow {

  0%,
  100% {
    box-shadow: 0 0 8px rgba(255, 69, 0, 0.3), inset 0 0 8px rgba(255, 69, 0, 0.1);
  }

  50% {
    box-shadow: 0 0 20px rgba(255, 69, 0, 0.6), inset 0 0 15px rgba(255, 69, 0, 0.2);
  }
}

@keyframes eruption-shake {

  0%,
  100% {
    transform: translateX(0);
  }

  10% {
    transform: translateX(-3px) rotate(-0.5deg);
  }

  30% {
    transform: translateX(3px) rotate(0.5deg);
  }

  50% {
    transform: translateX(-2px);
  }

  70% {
    transform: translateX(2px) rotate(-0.3deg);
  }

  90% {
    transform: translateX(-1px);
  }
}

.lava-border {
  border-image: linear-gradient(180deg, #FF4500, #FF6347, #FF8C00, #FF4500) 1;
}

.magma-meter-active {
  animation: magma-glow 1.5s ease-in-out infinite;
}

.eruption-active {
  animation: eruption-shake 0.5s ease-in-out;
}

@keyframes emoji-fountain {
  0% {
    transform: translate(-50%, 50px) scale(0.5) rotate(0deg);
    opacity: 1;
  }
  20% {
    opacity: 1;
  }
  100% {
    transform: translate(calc(-50% + var(--tx)), var(--ty)) scale(1.5) rotate(var(--rot));
    opacity: 0;
  }
}
.animate-emoji-fountain {
  animation: emoji-fountain ease-out forwards;
}

@keyframes shimmer {
  0% { transform: translateX(-100%); }
  100% { transform: translateX(100%); }
}

/* ============================================================
   NEW UI ENHANCEMENTS (Glassmorphism, Gradients, Pulses)
   ============================================================ */

.glass-panel {
  background: rgba(15, 15, 20, 0.4);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.08);
  box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
  border-radius: 12px;
}

/* While the reels are spinning, drop the reel board's backdrop-filter on
   phones/tablets: re-blurring a full panel every frame during the most
   animation-heavy moment is pure GPU tax, and it's hidden behind the reels
   anyway. A slightly more opaque tint keeps the board reading as solid without
   the blur. The frost returns the instant the reels stop; desktop keeps full
   glass throughout. Toggled via `.glass-motion` from App.js. */
@media (pointer: coarse) {
  .glass-panel.glass-motion {
    backdrop-filter: none !important;
    -webkit-backdrop-filter: none !important;
    background: rgba(15, 15, 20, 0.82);
  }
}

/* ============================================================
   LIQUID GLASS  — a subtle translucent "glass" material (inspired by
   Apple's Liquid Glass). Deliberately restrained for a dark casino theme:
   a frosted blur+saturation lets the backdrop bleed through, a bright top
   specular hairline + faint rim read as a curved glass surface, and a soft
   inner base-shadow gives depth — all via box-shadow INSETS so they paint
   BEHIND content and never touch legibility.

   Composable: it sets only backdrop-filter + box-shadow, so callers keep
   their own bg-*/border-* tint (remove any Tailwind shadow-* util from the
   element — box-shadow has a single owner). Degrades to a near-opaque panel
   where backdrop-filter is unsupported.
   ============================================================ */
.liquid-glass {
  /* No `position` here — the base material must not fight consumers that set
     their own position (the Battle Pass lane header is `sticky`, the audio
     mixer popover is `absolute`). Only the -hero variant needs positioning for
     its ::before sheen. */
  backdrop-filter: blur(16px) saturate(155%);
  -webkit-backdrop-filter: blur(16px) saturate(155%);
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.15),      /* specular top edge */
    inset 0 0 0 1px rgba(255, 255, 255, 0.045),     /* faint luminous rim */
    inset 0 -18px 26px -18px rgba(0, 0, 0, 0.55),   /* base darkening → depth */
    0 10px 30px rgba(0, 0, 0, 0.42);                /* lift shadow */
}
/* A stronger, "hero" variant with a swept top sheen for prominent chrome
   (bottom nav, sheets). The sheen sits in a ::before clipped to the radius;
   pointer-events:none keeps it click-through, and it fades out by 45% height
   so it never washes over body content. */
.liquid-glass-hero {
  position: relative;
  backdrop-filter: blur(20px) saturate(170%);
  -webkit-backdrop-filter: blur(20px) saturate(170%);
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.18),
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    inset 0 -22px 30px -22px rgba(0, 0, 0, 0.6),
    0 -6px 24px rgba(0, 0, 0, 0.5);
}
.liquid-glass-hero::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 0;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.09) 0%, rgba(255, 255, 255, 0.02) 22%, transparent 46%);
}
/* Fallback: no backdrop-filter → lean on a near-opaque tint so the material
   never reads as transparent-over-nothing. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .liquid-glass { background-color: rgba(18, 16, 14, 0.92); }
  .liquid-glass-hero { background-color: rgba(14, 12, 11, 0.95); }
}

.text-gradient-gold {
  background: linear-gradient(to right, #fbbf24, #f59e0b, #fbbf24);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  color: transparent;
  background-size: 200% auto;
  animation: tier-shimmer 3s linear infinite;
}

.text-gradient-neon {
  background: linear-gradient(to right, #00f2fe, #4facfe, #00f2fe);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  color: transparent;
  background-size: 200% auto;
  animation: tier-shimmer 3s linear infinite;
}

/* Scale stays on the element (compositor-friendly); the gold glow moved to a
   pseudo-element whose opacity pulses over a static shadow. The old version
   animated box-shadow on the idle SPIN button 100% of the time, repainting the
   busiest control on screen even while nothing else moved. */
@keyframes spin-btn-pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.025); }
}
@keyframes spin-btn-glow {
  0%, 100% { opacity: 0.45; }
  50%      { opacity: 1; }
}

.btn-spin-pulse {
  position: relative;
  animation: spin-btn-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  transition: all 0.2s ease;
}
.btn-spin-pulse::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 12px rgba(251, 191, 36, 0.35);
  animation: spin-btn-glow 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
  will-change: opacity;
}

.btn-spin-pulse:active {
  transform: scale(0.95);
  animation: none;
}
.btn-spin-pulse:active::after { animation: none; opacity: 0.45; }
.reduced-motion .btn-spin-pulse::after { animation: none !important; opacity: 0.45; }

/* (The old .hover-3d-tilt JS pointer-tracking tilt is gone: three transform
   sources — parallax wrapper, CSS hover, per-mousemove inline styles — could
   never settle. Lobby cards now hover via .lobby-card:hover alone.) */

@keyframes stroke-dash {
  to {
    stroke-dashoffset: 0;
  }
}

@keyframes sweep-reflection {
  0% { transform: translateX(-150%) skewX(-45deg); }
  100% { transform: translateX(200%) skewX(-45deg); }
}

.jackpot-shimmer {
  position: relative;
  overflow: hidden;
}

.jackpot-shimmer::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 50%;
  height: 100%;
  background: linear-gradient(to right, rgba(255,255,255,0) 0%, rgba(255,255,255,0.4) 50%, rgba(255,255,255,0) 100%);
  animation: sweep-reflection 4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
  z-index: 20;
  pointer-events: none;
}

@keyframes bounce-stop {
  0% { transform: translateY(-8%); }
  50% { transform: translateY(4%); }
  100% { transform: translateY(0); }
}

@keyframes rain-down {
  0% { transform: translateY(-10vh) rotate(0deg) scale(0.5); opacity: 0; }
  10% { transform: translateY(2vh) rotate(calc(var(--rot) * 0.1)) scale(1); opacity: 1; }
  90% { opacity: 1; }
  100% { transform: translateY(110vh) rotate(var(--rot)) scale(1.5); opacity: 0; }
}

/* ============================================================
   COIN BURST — floats up from balance display on win
   ============================================================ */
@keyframes coin-float {
  0%   { opacity: 0; transform: translateY(0) scale(0.4) rotate(-15deg); }
  18%  { opacity: 1; transform: translateY(-12px) scale(1.15) rotate(8deg); }
  100% { opacity: 0; transform: translateY(-90px) scale(0.7) rotate(25deg); }
}
.animate-coin-float {
  animation: coin-float 1.5s ease-out forwards;
}

/* ============================================================
   REDUCED MOTION — toggled via .reduced-motion on <html>
   Silences ambient loops; keeps win/reel feedback intact.
   ============================================================ */
.reduced-motion .lobby-card { animation: none !important; }
.reduced-motion .btn-spin-pulse { animation: none !important; }
.reduced-motion .jackpot-card::before,
.reduced-motion .jackpot-card::after { animation: none !important; }
.reduced-motion .jackpot-shimmer::after { animation: none !important; }
.reduced-motion .animate-vip-glow { animation: none !important; }
.reduced-motion .play-btn-wrap::before { animation: none !important; }
.reduced-motion .text-gradient-gold,
.reduced-motion .text-gradient-neon { animation: none !important; }
.reduced-motion .animate-tilt-pulse { animation: none !important; }
.reduced-motion .feature-swirl-border { animation: none !important; }
.reduced-motion .animate-coin-float { animation: none !important; }
/* Infinite decorative loops that were ignoring the reduced-motion setting —
   the 'Last Win' HUD glow, winning-symbol pulse, Hold&Win locked cells, and the
   scrolling Battle Pass grid. Freeze the motion but keep a static readable state. */
.reduced-motion .animate-win-glow { animation: none !important; text-shadow: 0 0 10px currentColor; }
.reduced-motion .symbol-winning { animation: none !important; }
.reduced-motion .heist-cell-locked { animation: none !important; }
.reduced-motion .bp-track-bg { animation: none !important; }
.reduced-motion * { transition-duration: 0.05s !important; }

/* ============================================================
   FEATURE: Win Breakdown Strip
   ============================================================ */
@keyframes win-chip-in {
  0%   { opacity: 0; transform: translateY(-10px) scale(0.8); }
  70%  { transform: translateY(2px) scale(1.05); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
.win-chip-enter {
  animation: win-chip-in 0.32s cubic-bezier(0.175, 0.885, 0.32, 1.275) both;
}
.reduced-motion .win-chip-enter { animation: none; opacity: 1; }

/* One-shot themed glow pulse around the reel board when a spin wins. Host sets
   --win-glow-rgb to the slot theme's primary RGB. */
@keyframes reel-win-flash {
  0%   { box-shadow: 0 0 0 0 rgba(var(--win-glow-rgb, 251,191,36), 0); }
  22%  { box-shadow: 0 0 28px 5px rgba(var(--win-glow-rgb, 251,191,36), 0.55); }
  100% { box-shadow: 0 0 0 0 rgba(var(--win-glow-rgb, 251,191,36), 0); }
}
/* The glow is a static box-shadow on a pseudo layer whose OPACITY flashes —
   animating the box-shadow itself repainted the whole reel area per frame.
   Hosts (game/App.js) are position: relative. */
@keyframes reel-win-glow {
  0%   { opacity: 0; }
  22%  { opacity: 1; }
  100% { opacity: 0; }
}
.reel-area-win { border-radius: 1rem; }
.reel-area-win::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 28px 5px rgba(var(--win-glow-rgb, 251,191,36), 0.55);
  opacity: 0;
  animation: reel-win-glow 1.1s ease-out 1;
}
.reduced-motion .reel-area-win::after { animation: none; }

/* Machine screen ambient art (a skin's background.appFx, deck plate, panel
   frame and cabinet backdrop — game/App.js, ui/ReelComponents.js): its loops
   hold still under Performance Mode (the phone default) and Reduced Motion,
   per the house motion contract, and while a full-screen surface (a modal,
   the big-win / jackpot ceremony) covers the machine (.machine-quiet). */
.performance-mode .machine-ambient *,
.performance-mode .machine-ambient *::before,
.performance-mode .machine-ambient *::after,
.reduced-motion .machine-ambient *,
.reduced-motion .machine-ambient *::before,
.reduced-motion .machine-ambient *::after,
.machine-quiet .machine-ambient *,
.machine-quiet .machine-ambient *::before,
.machine-quiet .machine-ambient *::after {
  animation-play-state: paused !important;
}
/* A wheel STAGE is up (ui/WheelEngine.js WheelStageShell puts .wx-stage-up on
   <html>): the opaque stage hides the whole machine, so everything looping in
   it — the win-symbol float/rim loops left over from the triggering spin, the
   emblem parts, meters, the top bar — holds still until the stage closes. The
   stage itself (and anything ever mounted inside the machine column) is
   excluded; the Daily Wheel, the ceremonies and the dev console live outside
   .machine-fit / .phone-machine-col and are untouched. */
.wx-stage-up .machine-fit *:not(.wx-stage, .wx-stage *),
.wx-stage-up .machine-fit *:not(.wx-stage, .wx-stage *)::before,
.wx-stage-up .machine-fit *:not(.wx-stage, .wx-stage *)::after,
.wx-stage-up .phone-machine-col *:not(.wx-stage, .wx-stage *),
.wx-stage-up .phone-machine-col *:not(.wx-stage, .wx-stage *)::before,
.wx-stage-up .phone-machine-col *:not(.wx-stage, .wx-stage *)::after {
  animation-play-state: paused !important;
}
/* The GRAND tile's light sweep is an ambient loop too. */
.performance-mode .jackpot-shimmer::after,
.machine-quiet .jackpot-shimmer::after { animation-play-state: paused !important; }

/* Phone machine SPIN (game/App.js): the house spring (--prm-snap) on the
   press — it lands with weight and the release overshoots back — instead of a
   flat 0.12s ease. The extra class out-ranks styles-mobile.css's rule; that
   sheet's later .reduced-motion rule (same rank) still switches it off. */
.game-screen-safe.machine-phone .phone-spin {
  transition: transform 0.24s var(--prm-snap, ease);
}
/* 320–349px phones: BET's fixed 44 + 34 + 44 and the 104px SPIN left the
   AUTO / TURBO column ~14px wide, so their labels spilled out of the buttons.
   A slightly tighter deck (36px steppers, 88px SPIN, 6px gaps) gives it ~56px,
   where styles-mobile.css's container rules fit both labels. */
@media (max-width: 349px) {
  .machine-phone .phone-deck { padding: 10px !important; column-gap: 6px !important; }
  .machine-phone .phone-deck .phone-step { width: 36px !important; }
  .machine-phone .phone-spin { width: 88px !important; height: 88px !important; }
}

/* ============================================================
   FEATURE: Reel Cabinet themed breathing glow
   ============================================================ */
/* The breathing glow lives on a pseudo-element with a STATIC box-shadow whose
   opacity pulses. Animating box-shadow directly forced the whole cabinet (and
   the reels inside it) to repaint every frame for the entire session — a
   constant background tax that competed with the spin animation itself. */
@keyframes glow-breathe {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
.cabinet-breathe {
  position: relative;
  box-shadow: inset 0 0 8px rgba(0, 0, 0, 0.4);
}
.cabinet-breathe::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 18px var(--cabinet-color, rgba(245, 158, 11, 0.4));
  animation: glow-breathe 3s ease-in-out infinite;
  will-change: opacity;
}
.reduced-motion .cabinet-breathe::after { animation: none; opacity: 0.5; }

/* ============================================================
   PERFORMANCE MODE — toggled via .performance-mode on <html>
   (Settings › Display, and now the default on phones if the owner opts in).
   Until now this class gated the JS particle/canvas load but ZERO CSS, so the
   ~150 always-running decorative loops kept repainting on the very devices
   that could least afford it. This block silences the CONTINUOUS/infinite
   loops that repaint paint-only properties (box-shadow, text-shadow, filter,
   background-position on text gradients) — the steady idle tax. One-shot
   entrances/dismissals are deliberately left alone so the UI still feels
   responsive; this is lighter-touch than reduced-motion by design.
   ============================================================ */
.performance-mode .lobby-card,
.performance-mode .animate-vip-glow,
.performance-mode .animate-tilt-pulse,
.performance-mode .feature-swirl-border,
.performance-mode .bp-track-bg,
.performance-mode .bp-tile-live,
.performance-mode .bp-claim-pill,
.performance-mode .bp-spine-lit,
.performance-mode .bp-current-ring,
.performance-mode .bp-claimall-pulse,
.performance-mode .bp-ray-spin,
.performance-mode .bp-finale-strip,
.performance-mode .bp-finale-icon,
.performance-mode .bp-xp-shine::after,
.performance-mode .vip-pulse-cta,
.performance-mode .vip-badge-pulse,
.performance-mode .cos-frame-legendary::before,
.performance-mode .cos-legendary-ring::before,
.performance-mode .cos-legendary-bg,
.performance-mode .cos-bg-galactic,
.performance-mode .cos-bg-galactic::after,
.performance-mode .cos-legendary-reel,
.performance-mode .cos-legendary-gem,
.performance-mode .reel-glow-cycle,
.performance-mode .reel-sideglow-cycle,
.performance-mode .lucky-pot-fill-hot,
.performance-mode .dcir-dyn-cage,
.performance-mode .dcir-dyn-ring,
.performance-mode .dcir-dyn-pulse,
.performance-mode .dcir-dyn-arcs,
.performance-mode .respin-low-pulse,
.performance-mode .heist-siren,
.performance-mode .heist-safe-shine,
.performance-mode .heist-grand-alarm,
.performance-mode .bonus-glyph-pulse {
  animation: none !important;
}
/* These need a sensible static resting state after their loop is cut. */
.performance-mode .animate-win-glow { animation: none !important; text-shadow: 0 0 10px currentColor; }
.performance-mode .text-gradient-gold,
.performance-mode .text-gradient-neon { animation: none !important; background-position: 0 0; }
.performance-mode .cabinet-breathe::after { animation: none !important; opacity: 0.5; }
.performance-mode .lobby-sheen { animation: none !important; }

/* ============================================================
   BATTLE PASS — custom reward icons + premium finish
   Bespoke SVG reward art (window.BP_ART) fills its sized wrapper. Claimable
   reward icons get a gentle premium bob so the track feels alive; disabled
   under reduced-motion / performance-mode (the mobile default).
   ============================================================ */
.bp-reward-icon { display: block; line-height: 0; }

/* Avatar frame ring — bespoke SVG frame (window.FRAME_ART) overlaid just outside
   the avatar circle; its transparent centre lets the face show through. */
.frame-ring { position: absolute; inset: -14%; pointer-events: none; z-index: 3; }

/* ------------------------------------------------------------------
   RAW-SVG ART WRAPPERS — every one of these must size its child <svg>.
   All our art registries (UI_ICON_SVG, CHEST_ART, OFFER_ART, BP_ART,
   FRAME_ART) ship markup that opens `<svg viewBox="...">` with NO width
   or height attribute, injected via dangerouslySetInnerHTML. Blink then
   stretches it to fill the box, but WebKit/iOS Safari resolves an
   unsized replaced element inside a flex container to 0x0 — the icon
   silently vanishes while its wrapper keeps its size (so the layout,
   and any notification dot pinned to it, still look right).
   That is exactly how the phone bottom nav lost every icon.
   Keep every raw-SVG wrapper in this list.
   ------------------------------------------------------------------ */
.bp-reward-icon > svg,
.frame-ring > svg,
.ui-icon > svg,
.chest-visual > svg,
.art-svg > svg { width: 100%; height: 100%; display: block; }
/* .ui-icon carries its box as an inline style (UIIcon sets width/height/flex);
   .chest-visual and .art-svg are sized by their caller's classes. */
.chest-visual, .art-svg { line-height: 0; }
@keyframes bp-icon-bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
.bp-icon-live { animation: bp-icon-bob 2.8s ease-in-out infinite; }
.reduced-motion .bp-icon-live,
.performance-mode .bp-icon-live { animation: none !important; }

/* ============================================================
   FEATURE: Anticipation — scatter reel glow during tease
   ============================================================ */
@keyframes anticipation-scatter-pulse {
  0%, 100% { box-shadow: 0 0 10px rgba(250,204,21,0.4), inset 0 0 6px rgba(250,204,21,0.08); border-color: rgba(250,204,21,0.55); }
  50%       { box-shadow: 0 0 26px rgba(250,204,21,0.85), inset 0 0 14px rgba(250,204,21,0.18); border-color: rgba(250,204,21,1); }
}
.reel-scatter-anticipation {
  border: 2px solid rgba(250,204,21,0.6);
  border-radius: 0.5rem;
  animation: anticipation-scatter-pulse 0.65s ease-in-out infinite;
  z-index: 25;
}
.reduced-motion .reel-scatter-anticipation {
  animation: none;
  box-shadow: 0 0 18px rgba(250,204,21,0.7);
  border: 2px solid rgb(250,204,21);
}

/* ============================================================
   FEATURE: Session Stats Rail
   ============================================================ */
@keyframes stats-rail-slide-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: translateY(0); }
}
.stats-rail-enter {
  animation: stats-rail-slide-in 0.22s ease-out both;
}
.reduced-motion .stats-rail-enter { animation: none; }


/* (The old BonusWheel's .wheel-* rules lived here. The slot bonus wheel is now
   the WHEEL STAGE — ui/WheelEngine.js + the [wx-*] block at the end of this
   file. .wheel-frame svg below is kept for the Daily Wheel.) */

/* ============================================================
   CASH RESPINS (VAULT CRACK) — delight upgrades
   Floating cash, vault-fill meter, near-grand alarm,
   respin reset flash, low-respin urgency, safe shine.
   ============================================================ */
@keyframes value-float {
  0%   { opacity: 0; transform: translate(-50%, 6px) scale(0.6); }
  20%  { opacity: 1; transform: translate(-50%, -6px) scale(1.1); }
  100% { opacity: 0; transform: translate(-50%, -42px) scale(1); }
}
.heist-value-float {
  position: absolute;
  left: 50%;
  top: 4px;
  transform: translateX(-50%);
  font-family: "Share Tech Mono", monospace;
  font-weight: 700;
  font-size: 0.85rem;
  color: #4ade80;
  text-shadow: 0 0 8px rgba(74, 222, 128, 0.9), 0 1px 2px rgba(0, 0, 0, 0.9);
  pointer-events: none;
  z-index: 40;
  animation: value-float 1.1s ease-out forwards;
}

@keyframes safe-shine {
  0%   { transform: translateX(-160%) skewX(-20deg); }
  100% { transform: translateX(260%) skewX(-20deg); }
}
.heist-safe-shine {
  position: absolute;
  top: 0;
  left: 0;
  width: 45%;
  height: 100%;
  background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.5), transparent);
  animation: safe-shine 2.6s ease-in-out infinite;
  pointer-events: none;
}

@keyframes grand-alarm {
  0%, 100% {
    border-color: rgba(220, 38, 38, 0.55);
    box-shadow: 0 0 30px rgba(220, 38, 38, 0.35), inset 0 0 14px rgba(220, 38, 38, 0.12);
  }
  50% {
    border-color: rgba(248, 113, 113, 1);
    box-shadow: 0 0 60px rgba(220, 38, 38, 0.75), inset 0 0 22px rgba(220, 38, 38, 0.25);
  }
}
.heist-grand-alarm { animation: grand-alarm 0.7s ease-in-out infinite; }

@keyframes respin-reset-flash {
  0%   { opacity: 0; transform: translateY(6px) scale(0.7); }
  30%  { opacity: 1; transform: translateY(0) scale(1.1); }
  80%  { opacity: 1; transform: translateY(0) scale(1); }
  100% { opacity: 0; transform: translateY(-8px) scale(0.95); }
}
.respin-reset-flash { animation: respin-reset-flash 1.1s ease-out forwards; }

@keyframes respin-low-pulse {
  0%, 100% { color: #ef4444; text-shadow: 0 0 6px rgba(239, 68, 68, 0.6); transform: scale(1); }
  50%      { color: #fca5a5; text-shadow: 0 0 16px rgba(239, 68, 68, 1); transform: scale(1.12); }
}
.respin-low-pulse { animation: respin-low-pulse 0.7s ease-in-out infinite; }

/* ============================================================
   CASH RESPINS (Hold & Win) — refreshed vault board
   ============================================================ */
.heist-board {
  background:
    radial-gradient(130% 80% at 50% -10%, rgba(251,191,36,0.08), transparent 60%),
    repeating-linear-gradient(0deg, rgba(0,0,0,0.22) 0 2px, transparent 2px 5px),
    linear-gradient(160deg, #1c1917, #0a0908);
  border: 1px solid rgba(146,104,12,0.45);
  box-shadow: inset 0 0 34px rgba(0,0,0,0.75), 0 0 0 1px rgba(251,191,36,0.05);
}
.heist-reel-col {
  background: linear-gradient(180deg, rgba(12,11,10,0.92), rgba(5,5,4,0.96));
  box-shadow: inset 0 2px 10px rgba(0,0,0,0.8), inset 0 0 0 1px rgba(255,255,255,0.025);
}
.heist-cell-empty {
  background:
    radial-gradient(circle at 50% 45%, rgba(251,191,36,0.05), transparent 60%),
    linear-gradient(180deg, rgba(20,18,16,0.6), rgba(8,8,7,0.6));
  box-shadow: inset 0 0 0 1px rgba(68,63,57,0.35);
}

/* Respin pip-lights — the round's "lives", refilled on every new lock */
.heist-pip {
  width: 13px; height: 13px; border-radius: 9999px; flex-shrink: 0;
  background: #1c1917; border: 1px solid #44403c;
  box-shadow: inset 0 0 4px rgba(0,0,0,0.85);
  transition: background 0.25s ease, box-shadow 0.25s ease, border-color 0.25s ease;
}
.heist-pip-lit {
  background: radial-gradient(circle at 34% 30%, #fef3c7, #f59e0b 72%);
  border-color: #fcd34d;
  box-shadow: 0 0 10px rgba(251,191,36,0.85), inset 0 0 3px rgba(255,255,255,0.7);
}
.heist-pip-spent { background: #14110f; border-color: #292524; opacity: 0.55; }
.heist-pip-danger {
  background: radial-gradient(circle at 34% 30%, #fee2e2, #ef4444 72%);
  border-color: #fca5a5;
  animation: heist-pip-danger 0.7s ease-in-out infinite;
}
@keyframes heist-pip-danger {
  0%,100% { box-shadow: 0 0 8px rgba(239,68,68,0.7); transform: scale(1); }
  50%     { box-shadow: 0 0 18px rgba(239,68,68,1); transform: scale(1.2); }
}
.heist-pip-reset { animation: heist-pip-reset 0.55s cubic-bezier(0.175,0.885,0.32,1.275); }
@keyframes heist-pip-reset { 0%{transform:scale(0.3);} 60%{transform:scale(1.35);} 100%{transform:scale(1);} }

/* Segmented vault-tumbler meter toward the GRAND */
.heist-seg {
  height: 12px; border-radius: 3px;
  background: #14110f; border: 1px solid rgba(68,63,57,0.55);
  box-shadow: inset 0 1px 3px rgba(0,0,0,0.7);
  transition: all 0.35s ease;
}
.heist-seg-on {
  background: linear-gradient(180deg, #fde047, #a16207);
  border-color: #fcd34d;
  box-shadow: 0 0 9px rgba(251,191,36,0.65), inset 0 1px 2px rgba(255,255,255,0.4);
}
.heist-seg-hot {
  background: linear-gradient(180deg, #fde047, #b91c1c);
  border-color: #fca5a5;
  box-shadow: 0 0 12px rgba(239,68,68,0.75);
}

/* Header caution stripes + vault dial + corner sirens */
.heist-header-stripes {
  position: absolute; inset: 0; opacity: 0.07; pointer-events: none;
  background: repeating-linear-gradient(45deg, #fbbf24 0 9px, transparent 9px 20px);
}
.heist-vault-dial {
  font-size: 1.85rem; line-height: 1;
  filter: drop-shadow(0 0 9px rgba(251,191,36,0.55));
  animation: heist-dial-bob 3s ease-in-out infinite;
}
@keyframes heist-dial-bob {
  0%,100% { transform: translateY(0) rotate(-5deg); }
  50%     { transform: translateY(-2px) rotate(5deg); }
}
.heist-siren {
  position: absolute; width: 9px; height: 9px; border-radius: 9999px;
  background: #ef4444; box-shadow: 0 0 16px 4px rgba(239,68,68,0.85);
  animation: heist-siren-blink 0.6s ease-in-out infinite; z-index: 30; pointer-events: none;
}
.heist-siren-tl { top: 11px; left: 11px; }
.heist-siren-tr { top: 11px; right: 11px; animation-delay: 0.3s; }
.heist-siren-bl { bottom: 11px; left: 11px; animation-delay: 0.3s; }
.heist-siren-br { bottom: 11px; right: 11px; }
@keyframes heist-siren-blink {
  0%,100% { opacity: 0.25; transform: scale(0.75); }
  50%     { opacity: 1; transform: scale(1.25); }
}

.reduced-motion .heist-vault-dial,
.reduced-motion .heist-pip-danger,
.reduced-motion .heist-pip-reset,
.reduced-motion .heist-siren { animation: none !important; }

.reduced-motion .heist-safe-shine,
.reduced-motion .heist-grand-alarm,
.reduced-motion .respin-low-pulse { animation: none !important; }

/* ============================================================
   PHASE 1 — Big-Win ceremony, Heat meter, Toasts, Reel juice
   ============================================================ */

/* --- #4 Cinematic Big-Win roll-up ceremony --- */
@keyframes ceremony-shake {
  0%, 100% { transform: translate(0, 0) rotate(0deg); }
  15% { transform: translate(-6px, 3px) rotate(-1deg); }
  30% { transform: translate(7px, -4px) rotate(1.2deg); }
  45% { transform: translate(-5px, -3px) rotate(-0.8deg); }
  60% { transform: translate(5px, 4px) rotate(0.8deg); }
  80% { transform: translate(-3px, 2px) rotate(-0.4deg); }
}
.ceremony-shake { animation: ceremony-shake 0.5s cubic-bezier(0.36, 0.07, 0.19, 0.97); }

@keyframes ceremony-flash {
  0% { opacity: 0; }
  20% { opacity: 0.85; }
  100% { opacity: 0; }
}
.ceremony-flash {
  position: fixed;
  inset: 0;
  background: radial-gradient(circle, #ffffff 0%, rgba(255, 255, 255, 0.3) 45%, transparent 75%);
  pointer-events: none;
  z-index: 120;
  animation: ceremony-flash 0.4s ease-out forwards;
}

@keyframes tier-upgrade-pop {
  0% { transform: scale(0.4) rotate(-8deg); opacity: 0; filter: brightness(2.2); }
  55% { transform: scale(1.22) rotate(4deg); opacity: 1; filter: brightness(1.6); }
  100% { transform: scale(1) rotate(0deg); opacity: 1; filter: brightness(1); }
}
.tier-upgrade-pop { animation: tier-upgrade-pop 0.55s cubic-bezier(0.175, 0.885, 0.32, 1.275); }

/* --- #2 Win-Streak "Heat" meter --- */
@keyframes heat-flame-bob {
  0%, 100% { transform: translateY(0) scale(1) rotate(-3deg); }
  50% { transform: translateY(-2px) scale(1.12) rotate(3deg); }
}
.heat-flame { display: inline-block; animation: heat-flame-bob 0.5s ease-in-out infinite; }

@keyframes heat-bar-shimmer {
  0% { background-position: -200% 0; }
  100% { background-position: 200% 0; }
}
.heat-bar-shimmering {
  background-size: 200% 100% !important;
  animation: heat-bar-shimmer 1.2s linear infinite;
}

@keyframes heat-glow-pulse {
  0%, 100% { box-shadow: 0 0 6px var(--heat-color, #f97316); }
  50% { box-shadow: 0 0 18px var(--heat-color, #f97316), 0 0 30px var(--heat-color, #f97316); }
}
.heat-glow { animation: heat-glow-pulse 0.7s ease-in-out infinite; }

@keyframes heat-callout {
  0% { transform: translate(-50%, 8px) scale(0.5); opacity: 0; }
  25% { transform: translate(-50%, 0) scale(1.15); opacity: 1; }
  75% { transform: translate(-50%, 0) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -22px) scale(0.95); opacity: 0; }
}
.heat-callout { animation: heat-callout 1.3s ease-out forwards; }

/* --- #6 Toast / milestone callouts --- */
@keyframes toast-in {
  0% { opacity: 0; transform: translateX(40px) scale(0.9); }
  100% { opacity: 1; transform: translateX(0) scale(1); }
}
@keyframes toast-out {
  0% { opacity: 1; transform: translateX(0) scale(1); max-height: 80px; margin-bottom: 0.5rem; }
  100% { opacity: 0; transform: translateX(40px) scale(0.9); max-height: 0; margin-bottom: 0; }
}
.toast-enter { animation: toast-in 0.32s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; }
.toast-exit { animation: toast-out 0.35s ease-in forwards; overflow: hidden; }

/* --- #9 Reel & symbol juice --- */
@keyframes symbol-land {
  0% { transform: translateY(-12%) scaleY(1.18) scaleX(0.9); }
  45% { transform: translateY(3%) scaleY(0.86) scaleX(1.12); }
  70% { transform: translateY(-1%) scaleY(1.05) scaleX(0.97); }
  100% { transform: translateY(0) scale(1); }
}
.symbol-land { animation: symbol-land 0.34s cubic-bezier(0.2, 0.9, 0.3, 1.3); }

@keyframes impact-dust {
  0% { opacity: 0.5; transform: translateX(var(--dust-x, 0)) scale(0.4); }
  100% { opacity: 0; transform: translateX(var(--dust-x, 0)) translateY(6px) scale(1.4); }
}
.impact-dust {
  position: absolute;
  bottom: 4px;
  left: 50%;
  width: 5px;
  height: 5px;
  border-radius: 9999px;
  background: rgba(255, 255, 255, 0.55);
  opacity: 0;
  pointer-events: none;
  animation: impact-dust 0.4s ease-out forwards;
}

/* --- Cascading reels (Mad Lab): winning tiles fizzle out, the tiles ABOVE
   fall down into the gaps, and fresh tiles drop in from the top. --- */
@keyframes cascade-clear {
  0%   { transform: scale(1);    opacity: 1; filter: brightness(1); }
  35%  { transform: scale(1.14); opacity: 1; filter: brightness(1.9); }
  100% { transform: scale(0.1) rotate(10deg); opacity: 0; filter: brightness(2.4); }
}
.cascade-clearing { animation: cascade-clear 0.24s ease-in forwards; transform-origin: center; }
@keyframes cascade-fall {
  0%   { transform: translateY(calc(var(--fall-rows, 1) * -112%)); opacity: 0.55; }
  60%  { transform: translateY(0); opacity: 1; }
  76%  { transform: translateY(7%) scaleY(0.94); }
  88%  { transform: translateY(-2%) scaleY(1.03); }
  100% { transform: translateY(0) scaleY(1); }
}
.cascade-falling { animation: cascade-fall 0.4s cubic-bezier(0.3, 0.75, 0.35, 1.25); }

/* --- Bubbling reaction flask (cascade multiplier meter) --- */
@keyframes flask-liquid-wobble {
  0%, 100% { transform: translateX(-1px) skewX(-1.4deg); }
  50%      { transform: translateX(1px)  skewX(1.4deg); }
}
@keyframes flask-bubble-rise {
  0%   { transform: translateY(0) scale(0.5);  opacity: 0; }
  18%  { opacity: 0.95; }
  80%  { opacity: 0.6; }
  100% { transform: translateY(-40px) scale(1.15); opacity: 0; }
}
@keyframes flask-pop {
  0%   { transform: scale(1); }
  40%  { transform: scale(1.28); }
  100% { transform: scale(1); }
}
.flask-liquid { animation: flask-liquid-wobble 1.8s ease-in-out infinite; transform-origin: center bottom; }
.flask-bubble { animation: flask-bubble-rise 1.5s ease-in infinite; }
.flask-pop    { animation: flask-pop 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.5); }

.reduced-motion .cascade-clearing,
.reduced-motion .cascade-falling,
.reduced-motion .flask-liquid,
.reduced-motion .flask-bubble,
.reduced-motion .flask-pop { animation: none !important; }

/* --- #5 Attract mode idle hint --- */
@keyframes attract-bob {
  0%, 100% { transform: translate(-50%, 0); }
  50%      { transform: translate(-50%, -7px); }
}
.attract-hint { animation: attract-bob 1.4s ease-in-out infinite; }

/* --- #3 Coin shower (coins fly from reels up to the balance) --- */
@keyframes coin-shower-fly {
  0%   { opacity: 0; transform: translate(0, 0) scale(0.5) rotate(0deg); }
  15%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--cx, 0), -260px) scale(1.1) rotate(220deg); }
}
.coin-shower-coin {
  opacity: 0;
  animation-name: coin-shower-fly;
  animation-timing-function: ease-out;
  animation-fill-mode: forwards;
  will-change: transform, opacity;
}

.reduced-motion .ceremony-shake,
.reduced-motion .ceremony-flash,
.reduced-motion .heat-flame,
.reduced-motion .heat-glow,
.reduced-motion .heat-bar-shimmering,
.reduced-motion .heat-callout,
.reduced-motion .symbol-land,
.reduced-motion .impact-dust,
.reduced-motion .coin-shower-coin { animation: none !important; }

/* ============================================================
   LEGENDARY COSMETIC EFFECTS (#7 premium vanity)
   Self-contained classes toggled when a cosmetic has tier:'legendary'.
   Each one carries its own unique animated treatment.
   ============================================================ */

/* Animated flowing prismatic name text (clip-text rainbow sweep). */
@keyframes cos-name-flow {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.cos-legendary-name {
  background-image: linear-gradient(90deg, #f87171, #fbbf24, #34d399, #22d3ee, #a78bfa, #f472b6, #f87171);
  background-size: 200% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
  animation: cos-name-flow 4s linear infinite;
  filter: drop-shadow(0 0 6px rgba(168,139,250,0.55));
}

/* Rotating conic halo behind a legendary avatar frame. Host must be position:relative. */
@keyframes cos-ring-spin { to { transform: rotate(360deg); } }
.cos-legendary-ring { position: relative; z-index: 0; }
.cos-legendary-ring::before {
  content: "";
  position: absolute;
  inset: -4px;
  border-radius: 9999px;
  background: conic-gradient(from 0deg, #22d3ee, #a78bfa, #f472b6, #fbbf24, #34d399, #22d3ee);
  animation: cos-ring-spin 3.5s linear infinite;
  z-index: -1;
  filter: blur(1px) saturate(1.3);
}
.cos-legendary-ring::after {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 9999px;
  background: #0c0a09;
  z-index: -1;
}

/* Rectangular rainbow border for the in-slot VIP badge when a LEGENDARY avatar
   frame is equipped — a rounded-pill rainbow rim with a dark inner so only the
   border reads as rainbow. The host sets its own border + background transparent
   so these pseudos show. NOTE: unlike the round cos-legendary-ring we must NOT
   rotate the element (a spinning rectangle sweeps out into a huge diagonal band);
   the rim stays put and the colors cycle via hue-rotate instead. */
.cos-frame-legendary { position: relative; z-index: 0; }
.cos-frame-legendary::before {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 1rem; /* matches rounded-2xl */
  background: linear-gradient(120deg, #22d3ee, #a78bfa, #f472b6, #fbbf24, #34d399, #22d3ee);
  animation: cos-frame-hue 4s linear infinite;
  z-index: -1;
  filter: saturate(1.2);
}
.cos-frame-legendary::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: calc(1rem - 2px);
  background: rgba(12, 10, 9, 0.82);
  z-index: -1;
}
@keyframes cos-frame-hue {
  from { filter: saturate(1.2) hue-rotate(0deg); }
  to   { filter: saturate(1.2) hue-rotate(360deg); }
}
.reduced-motion .cos-frame-legendary::before { animation: none !important; }

/* Holographic shimmer sweep across a legendary spin button (overlay div, inset-0). */
@keyframes cos-holo-sweep {
  0%   { background-position: -150% 0; }
  100% { background-position: 250% 0; }
}
.cos-legendary-btn-fx {
  background: linear-gradient(110deg, transparent 25%, rgba(255,255,255,0.55) 45%, rgba(186,230,253,0.6) 50%, rgba(255,255,255,0.55) 55%, transparent 75%);
  background-size: 220% 100%;
  mix-blend-mode: overlay;
  animation: cos-holo-sweep 2.6s linear infinite;
  pointer-events: none;
}

/* Drifting nebula for a legendary background overlay (replaces the static vignette). */
@keyframes cos-bg-drift {
  0%   { transform: translate3d(0,0,0) scale(1); }
  50%  { transform: translate3d(2%, -2%, 0) scale(1.08); }
  100% { transform: translate3d(0,0,0) scale(1); }
}
.cos-legendary-bg {
  background:
    radial-gradient(45% 55% at 22% 28%, rgba(168,85,247,0.30), transparent 70%),
    radial-gradient(45% 55% at 78% 30%, rgba(34,211,238,0.26), transparent 70%),
    radial-gradient(60% 60% at 50% 92%, rgba(244,114,182,0.24), transparent 72%),
    radial-gradient(40% 40% at 70% 70%, rgba(52,211,153,0.20), transparent 70%),
    /* Dark base so the nebula reads as a full background (not a faint overlay
       washed out by the animated chromatic canvas behind it). */
    #07060c;
  background-color: #07060c;
  animation: cos-bg-drift 14s ease-in-out infinite;
  will-change: transform;
}

/* --- Per-item legendary background effects (premium + distinct) --------- */
/* Galactic Drift — a deep-space nebula that drifts, with twinkling stars. */
.cos-bg-galactic {
  overflow: hidden;
  background:
    radial-gradient(45% 55% at 22% 28%, rgba(168,85,247,0.34), transparent 70%),
    radial-gradient(45% 55% at 78% 30%, rgba(34,211,238,0.30), transparent 70%),
    radial-gradient(60% 60% at 50% 92%, rgba(244,114,182,0.26), transparent 72%),
    radial-gradient(40% 40% at 70% 70%, rgba(52,211,153,0.20), transparent 70%),
    #05040b;
  background-color: #05040b;
  animation: cos-bg-drift 16s ease-in-out infinite;
  will-change: transform;
}
.cos-bg-galactic::after {
  content: '';
  position: absolute;
  inset: -10%;
  pointer-events: none;
  background-image:
    radial-gradient(1.5px 1.5px at 15% 25%, #fff, transparent 60%),
    radial-gradient(1px 1px at 45% 60%, #cffafe, transparent 60%),
    radial-gradient(2px 2px at 78% 18%, #fff, transparent 60%),
    radial-gradient(1px 1px at 32% 82%, #e9d5ff, transparent 60%),
    radial-gradient(1.5px 1.5px at 88% 66%, #fff, transparent 60%),
    radial-gradient(1px 1px at 62% 40%, #fff, transparent 60%),
    radial-gradient(1px 1px at 8% 70%, #a5f3fc, transparent 60%);
  animation: cos-bg-twinkle 4.5s ease-in-out infinite;
}
@keyframes cos-bg-twinkle { 0%, 100% { opacity: 0.3; } 50% { opacity: 0.85; } }

/* Heist Master — a dark vault drenched in gold with a slow sweeping spotlight. */
.cos-bg-heistmaster {
  overflow: hidden;
  background:
    radial-gradient(55% 45% at 50% 6%, rgba(251,191,36,0.30), transparent 70%),
    radial-gradient(75% 55% at 50% 100%, rgba(180,120,20,0.34), transparent 72%),
    radial-gradient(38% 50% at 12% 50%, rgba(245,158,11,0.16), transparent 70%),
    radial-gradient(38% 50% at 88% 50%, rgba(245,158,11,0.16), transparent 70%),
    #0a0805;
  background-color: #0a0805;
}
.cos-bg-heistmaster::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(115deg, transparent 38%, rgba(251,191,36,0.14) 47%, rgba(255,248,220,0.22) 50%, rgba(251,191,36,0.14) 53%, transparent 62%);
  background-size: 280% 100%;
  animation: cos-bg-gold-sweep 7s linear infinite;
}
@keyframes cos-bg-gold-sweep { 0% { background-position: 170% 0; } 100% { background-position: -170% 0; } }

/* Color-cycling glow for a legendary reel cabinet + its corner gems. */
@keyframes cos-reel-rainbow {
  0%   { box-shadow: 0 0 26px rgba(34,211,238,0.45), inset 0 0 14px rgba(34,211,238,0.18); border-color: #22d3ee; }
  25%  { box-shadow: 0 0 26px rgba(168,139,250,0.45), inset 0 0 14px rgba(168,139,250,0.18); border-color: #a78bfa; }
  50%  { box-shadow: 0 0 26px rgba(244,114,182,0.45), inset 0 0 14px rgba(244,114,182,0.18); border-color: #f472b6; }
  75%  { box-shadow: 0 0 26px rgba(52,211,153,0.45), inset 0 0 14px rgba(52,211,153,0.18); border-color: #34d399; }
  100% { box-shadow: 0 0 26px rgba(34,211,238,0.45), inset 0 0 14px rgba(34,211,238,0.18); border-color: #22d3ee; }
}
.cos-legendary-reel { animation: cos-reel-rainbow 6s linear infinite; }
@keyframes cos-gem-rainbow {
  0%   { background: #22d3ee; box-shadow: 0 0 10px #22d3ee; }
  25%  { background: #a78bfa; box-shadow: 0 0 10px #a78bfa; }
  50%  { background: #f472b6; box-shadow: 0 0 10px #f472b6; }
  75%  { background: #34d399; box-shadow: 0 0 10px #34d399; }
  100% { background: #22d3ee; box-shadow: 0 0 10px #22d3ee; }
}
.cos-legendary-gem { animation: cos-gem-rainbow 6s linear infinite; }

/* Shimmering LEGENDARY rarity badge used in the shop. */
@keyframes cos-badge-shine {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.cos-legendary-badge {
  background-image: linear-gradient(90deg, #fbbf24, #f472b6, #a78bfa, #22d3ee, #fbbf24);
  background-size: 200% auto;
  animation: cos-badge-shine 3s linear infinite;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
.cos-legendary-card {
  border-color: rgba(251,191,36,0.6) !important;
  box-shadow: 0 0 22px rgba(251,191,36,0.18), inset 0 0 14px rgba(168,85,247,0.08);
}

/* EPIC rarity (one tier below legendary) — the second-priciest item per
   category. A purple shimmering badge + soft violet card glow, parallel to the
   legendary treatment so the rarity ladder reads consistently in the shop. */
@keyframes cos-epic-shine {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.cos-epic-badge {
  background-image: linear-gradient(90deg, #c084fc, #a855f7, #818cf8, #a855f7, #c084fc);
  background-size: 200% auto;
  animation: cos-epic-shine 3s linear infinite;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  -webkit-text-fill-color: transparent;
}
.cos-epic-card {
  border-color: rgba(168,85,247,0.6) !important;
  box-shadow: 0 0 18px rgba(168,85,247,0.16), inset 0 0 12px rgba(129,140,248,0.07);
}

.reduced-motion .cos-legendary-name,
.reduced-motion .cos-legendary-ring::before,
.reduced-motion .cos-legendary-btn-fx,
.reduced-motion .cos-legendary-bg,
.reduced-motion .cos-bg-galactic,
.reduced-motion .cos-bg-galactic::after,
.reduced-motion .cos-bg-heistmaster::after,
.reduced-motion .cos-legendary-reel,
.reduced-motion .cos-legendary-gem,
.reduced-motion .cos-legendary-badge,
.reduced-motion .cos-epic-badge { animation: none !important; }

/* ============================================================
   DELIGHT PASS — shop & preview micro-interactions
   Tactile ripples, card tilt/sheen, preview entrance, buy burst.
   All effects self-disable under .reduced-motion (bottom of block).
   ============================================================ */

/* --- Tactile click ripple (host button needs position:relative; overflow:hidden) --- */
/* Each ink plays inside a clip layer in the host's own shape. The mask is the
   WebKit fix: without it iOS Safari clips an animating child to the host's
   square box, not its rounded corners (a grey square flashed on round SPIN). */
.ripple-clip {
  position: absolute; inset: 0; border-radius: inherit; overflow: hidden; pointer-events: none;
  isolation: isolate;
  -webkit-mask-image: -webkit-radial-gradient(white, black);
  mask-image: radial-gradient(white, black);
}
@keyframes ripple-expand { to { transform: scale(2.6); opacity: 0; } }
.ripple-ink {
  position: absolute;
  border-radius: 9999px;
  background: rgba(255, 255, 255, 0.45);
  transform: scale(0);
  pointer-events: none;
  mix-blend-mode: screen;
  animation: ripple-expand 0.6s ease-out forwards;
}

/* --- 3D pointer tilt + moving gloss sheen for cards --- */
.tilt-card {
  transform: perspective(700px) rotateX(var(--ty, 0deg)) rotateY(var(--tx, 0deg));
  transition: transform 0.16s cubic-bezier(0.22, 1, 0.36, 1);
  transform-style: preserve-3d;
  will-change: transform;
}
.tilt-sheen {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease;
  background: radial-gradient(160px circle at var(--mx, 50%) var(--my, 0%), rgba(255, 255, 255, 0.22), transparent 60%);
}
.tilt-card:hover .tilt-sheen { opacity: 1; }

/* --- Purchase preview entrance --- */
@keyframes preview-pop-in {
  0%   { opacity: 0; transform: translateY(16px) scale(0.92); }
  100% { opacity: 1; transform: translateY(0) scale(1); }
}
.preview-pop { animation: preview-pop-in 0.34s cubic-bezier(0.175, 0.885, 0.32, 1.275) both; }

/* --- One-shot light sweep across the showcased cosmetic --- */
@keyframes showcase-sweep {
  0%   { opacity: 1; transform: translateX(-130%) skewX(-18deg); }
  85%  { opacity: 1; }
  100% { opacity: 0; transform: translateX(240%) skewX(-18deg); }
}
.showcase-sweep { position: relative; overflow: hidden; }
.showcase-sweep::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 42%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.18), transparent);
  opacity: 0;
  animation: showcase-sweep 0.9s ease-out 0.18s 1 forwards;
  pointer-events: none;
}

/* --- Confirm-purchase radial sparkle burst --- */
@keyframes buy-spark-fly {
  0%   { opacity: 1; transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translateY(0) scale(1); }
  100% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translateY(-72px) scale(0.3); }
}
.buy-spark {
  position: absolute;
  top: 50%; left: 50%;
  width: 9px; height: 9px;
  border-radius: 9999px;
  pointer-events: none;
  box-shadow: 0 0 8px currentColor;
  animation: buy-spark-fly 0.7s ease-out forwards;
}

.reduced-motion .ripple-ink,
.reduced-motion .showcase-sweep::after,
.reduced-motion .buy-spark,
.reduced-motion .preview-pop { animation: none !important; }
.reduced-motion .tilt-card { transform: none !important; transition: none !important; }
.reduced-motion .tilt-sheen { display: none; }

/* --- Equipped reel-skin glow, bleeding from each symbol cell into the gaps
   around it. Adjacent cells' halos overlap in the gap, reading as one
   continuous soft flow rather than a separate per-gap effect. --- */
@keyframes reel-cell-glow-cycle {
  0%   { box-shadow: 0 0 14px 6px rgba(34,211,238,0.5); }
  25%  { box-shadow: 0 0 14px 6px rgba(167,139,250,0.5); }
  50%  { box-shadow: 0 0 14px 6px rgba(244,114,182,0.5); }
  75%  { box-shadow: 0 0 14px 6px rgba(52,211,153,0.5); }
  100% { box-shadow: 0 0 14px 6px rgba(34,211,238,0.5); }
}
.reel-cell-glow-legendary { animation: reel-cell-glow-cycle 6s linear infinite; }
.reduced-motion .reel-cell-glow-legendary { animation: none !important; box-shadow: 0 0 14px 6px rgba(167,139,250,0.5) !important; }

/* Spinning legendary reel: cycle the static gap-glow overlay's hue through the spectrum. */
@keyframes reel-glow-hue {
  from { filter: blur(2.5px) hue-rotate(0deg); }
  to   { filter: blur(2.5px) hue-rotate(360deg); }
}
.reel-glow-cycle { animation: reel-glow-hue 6s linear infinite; }
.reduced-motion .reel-glow-cycle { animation: none !important; }

/* Same color cycle for the spinning legendary reel's side glow (no blur — it's a box-shadow). */
@keyframes reel-sideglow-hue {
  from { filter: hue-rotate(0deg); }
  to   { filter: hue-rotate(360deg); }
}
.reel-sideglow-cycle { animation: reel-sideglow-hue 6s linear infinite; }
.reduced-motion .reel-sideglow-cycle { animation: none !important; }

/* ============================================================
   GLOBAL POLISH — accessibility & cross-browser consistency
   Cuts across every screen (lobby, shop, in-game) without touching
   component markup.
   ============================================================ */

/* Consistent keyboard focus ring. :focus-visible only triggers for keyboard
   navigation, so pointer/touch users see no change — this adds accessibility
   without altering the click feel. The themed gold matches the UI accent and
   the offset keeps it clear of each control's own glow/border. */
:focus-visible {
  outline: 2px solid rgba(251, 191, 36, 0.9);
  outline-offset: 2px;
}
/* Suppress the ring for mouse clicks in browsers that support :focus-visible. */
:focus:not(:focus-visible) {
  outline: none;
}

/* Firefox scrollbar parity with the WebKit styling defined at the top of this
   file (WebKit-only ::-webkit-scrollbar rules don't apply in Firefox). */
html {
  scrollbar-width: thin;
  scrollbar-color: #57534e #1c1917;
}
.custom-scrollbar {
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) rgba(0, 0, 0, 0.2);
}

/* ============================================================
   UI POLISH KIT — fast tooltips, invalid-action feedback,
   resource-low pulse, hover edge-glow.
   Backs ui/UIKit.js (Tooltip / IconButton). Every effect degrades
   gracefully under .reduced-motion (guards at the bottom).
   ============================================================ */

/* --- Crisp, fast hover/focus tooltip (replaces slow native title=) --- */
.ui-tip-wrap {
  position: relative;
  display: inline-flex;
}

.ui-tooltip {
  position: absolute;
  z-index: 200;
  pointer-events: none;
  white-space: nowrap;
  padding: 5px 9px;
  border-radius: 8px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  line-height: 1;
  color: #f5f5f4;
  background: rgba(12, 10, 9, 0.97);
  border: 1px solid var(--tip-color, rgba(251, 191, 36, 0.55));
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.6), 0 0 14px var(--tip-glow, rgba(251, 191, 36, 0.2));
  opacity: 0;
  /* Snappy: ~110ms in, instant feel. Per-placement transforms below. */
  transition: opacity 0.11s ease, transform 0.11s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Small diamond arrow, re-colored to match the (themeable) border. */
.ui-tooltip::after {
  content: "";
  position: absolute;
  width: 7px;
  height: 7px;
  background: rgba(12, 10, 9, 0.97);
  border: 1px solid var(--tip-color, rgba(251, 191, 36, 0.55));
}

/* Placements — hidden + shown transforms specified together so the
   centering translate isn't clobbered by the show animation. */
.ui-tooltip.tip-top    { bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%) translateY(4px) scale(0.96); }
.ui-tooltip.tip-bottom { top: calc(100% + 9px);    left: 50%; transform: translateX(-50%) translateY(-4px) scale(0.96); }
.ui-tooltip.tip-left   { right: calc(100% + 9px);  top: 50%;  transform: translateY(-50%) translateX(4px) scale(0.96); }
.ui-tooltip.tip-right  { left: calc(100% + 9px);   top: 50%;  transform: translateY(-50%) translateX(-4px) scale(0.96); }

.ui-tooltip.tip-top[data-show="true"]    { transform: translateX(-50%) translateY(0) scale(1); }
.ui-tooltip.tip-bottom[data-show="true"] { transform: translateX(-50%) translateY(0) scale(1); }
.ui-tooltip.tip-left[data-show="true"]   { transform: translateY(-50%) translateX(0) scale(1); }
.ui-tooltip.tip-right[data-show="true"]  { transform: translateY(-50%) translateX(0) scale(1); }
.ui-tooltip[data-show="true"] { opacity: 1; }

.ui-tooltip.tip-top::after    { bottom: -4px; left: 50%; margin-left: -4px; border-top: none;  border-left: none; }
.ui-tooltip.tip-bottom::after { top: -4px;    left: 50%; margin-left: -4px; border-bottom: none; border-right: none; }
.ui-tooltip.tip-left::after   { right: -4px;  top: 50%;  margin-top: -4px;  border-left: none;  border-bottom: none; }
.ui-tooltip.tip-right::after  { left: -4px;   top: 50%;  margin-top: -4px;  border-right: none; border-top: none; }

/* --- Hover edge-glow for juicy buttons (themeable via --glow-color) --- */
.ui-glow-hover {
  transition: box-shadow 0.18s ease, transform 0.15s ease, filter 0.18s ease;
}
.ui-glow-hover:hover:not(:disabled) {
  box-shadow: 0 0 0 1px var(--glow-color, rgba(251, 191, 36, 0.45)),
              0 0 16px var(--glow-color, rgba(251, 191, 36, 0.35));
  filter: brightness(1.08);
}

/* --- Invalid-action shake (e.g. SPIN with insufficient balance) --- */
@keyframes ui-error-shake {
  0%, 100% { transform: translateX(0); }
  15% { transform: translateX(-6px); }
  30% { transform: translateX(5px); }
  45% { transform: translateX(-4px); }
  60% { transform: translateX(3px); }
  75% { transform: translateX(-2px); }
}
.ui-error-shake { animation: ui-error-shake 0.42s cubic-bezier(0.36, 0.07, 0.19, 0.97); }

/* --- Resource-low pulse: richer "topped out" warning than a flat opacity blink.
   Cycles a red border + outward glow so a near-empty wallet draws the eye. --- */
@keyframes ui-resource-low {
  0%, 100% { box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); border-color: rgba(239, 68, 68, 0.5); }
  50%      { box-shadow: 0 0 18px 2px rgba(239, 68, 68, 0.5); border-color: rgba(248, 113, 113, 0.95); }
}
.ui-resource-low { animation: ui-resource-low 1.1s ease-in-out infinite; }

/* --- reduced-motion guards (mirror the rest of this file) --- */
.reduced-motion .ui-tooltip { transition: opacity 0.05s linear; }
.reduced-motion .ui-error-shake { animation: none !important; }
.reduced-motion .ui-glow-hover { transition-duration: 0.05s; }
.reduced-motion .ui-resource-low {
  animation: none !important;
  box-shadow: 0 0 12px 2px rgba(239, 68, 68, 0.45);
  border-color: rgba(248, 113, 113, 0.9);
}

/* ============================================================
   META LAYER  (#1 Daily Hub · #4 Lucky Pot)
   ============================================================ */

/* #4 — Lucky Pot meter: a sweeping shimmer once the pot is "hot". */
@keyframes lucky-pot-sweep {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.lucky-pot-fill-hot {
  background-size: 200% 100% !important;
  animation: lucky-pot-sweep 1.1s linear infinite;
}
.reduced-motion .lucky-pot-fill-hot { animation: none !important; }

/* ============================================================
   #1 — THE DAILY CIRCUIT (ui/MetaFeatures.js · DailyHubModal, QuestRow,
   QuestTicker; the dynamo + charge cells live in ui/QuestArt.js)
   The daily reward is a dynamo, the lightning seam is the bus bar carrying its
   charge, and the missions are the circuits it powers. Phones stack the two
   halves with a horizontal bus rail between them; ≥900px splits them across a
   diagonal seam with the bolt running down it.

   Electric palette: white-hot #ffffff/#ecfeff · cyan #67e8f9/#22d3ee · blue
   #38bdf8/#0ea5e9/#3b82f6 · violet depth #818cf8/#6366f1 · navy-black
   #050912→#0a1020 · circuit traces rgba(34,211,238,.10–.18).

   PERF: every animation below moves transform or opacity on an HTML box (a
   div / span / pseudo), never SVG internals. The big ones (the seam's strikes,
   the edge spark, the bus packet) run a few times as the hub opens and then
   rest lit; the only endless loops are the small live dynamo's (a rotation and
   two opacity flickers). Reduced motion silences all of it, performance mode
   the loops and the one-shots (guards at the end of this block).
   ============================================================ */
.daily-hub-card {
  border-color: rgba(34, 211, 238, 0.38);
  background:
    radial-gradient(90% 55% at 50% -12%, rgba(34, 211, 238, 0.16), transparent 62%),
    radial-gradient(60% 50% at 100% 100%, rgba(99, 102, 241, 0.10), transparent 70%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'%3E%3Cg fill='none' stroke='%2322d3ee' stroke-opacity='0.13' stroke-width='1'%3E%3Cpath d='M0 40H60L80 60H140L160 40H240'/%3E%3Cpath d='M60 0V40'/%3E%3Cpath d='M180 0V40'/%3E%3Cpath d='M140 60L160 80H240'/%3E%3Cpath d='M0 80H30'/%3E%3Cpath d='M0 150H40L60 170V240'/%3E%3Cpath d='M60 170H90'/%3E%3Cpath d='M100 60V110L120 130H200L220 150H240'/%3E%3Cpath d='M0 200H20L40 220H100L120 200H240'/%3E%3Cpath d='M180 200V240'/%3E%3C/g%3E%3Cg fill='%2367e8f9' fill-opacity='0.22'%3E%3Ccircle cx='60' cy='40' r='2'/%3E%3Ccircle cx='180' cy='40' r='2'/%3E%3Ccircle cx='100' cy='60' r='2'/%3E%3Ccircle cx='140' cy='60' r='2'/%3E%3Ccircle cx='200' cy='130' r='2'/%3E%3Ccircle cx='60' cy='170' r='2'/%3E%3Ccircle cx='180' cy='200' r='2'/%3E%3Ccircle cx='100' cy='220' r='2'/%3E%3C/g%3E%3Cg fill='none' stroke='%2367e8f9' stroke-opacity='0.16' stroke-width='1'%3E%3Ccircle cx='30' cy='80' r='3.5'/%3E%3Ccircle cx='90' cy='170' r='3.5'/%3E%3Ccircle cx='100' cy='110' r='3.5'/%3E%3C/g%3E%3C/svg%3E") 0 0 / 240px 240px repeat,
    linear-gradient(180deg, #0a1020 0%, #070c18 55%, #050912 100%);
  box-shadow:
    0 34px 100px rgba(0, 0, 0, 0.88),
    0 0 46px rgba(34, 211, 238, 0.16),
    inset 0 0 0 1px rgba(103, 232, 249, 0.05),
    inset 0 1px 0 rgba(236, 254, 255, 0.10);
}
/* the card's own hairline, lit like a live conductor */
.daily-hub-card::before {
  content: ""; position: absolute; inset-inline: 0; top: 0; height: 1px; z-index: 40;
  background: linear-gradient(90deg, transparent, #67e8f9 30%, #ecfeff 50%, #67e8f9 70%, transparent);
  opacity: 0.85; pointer-events: none;
}
/* ...and a spark that runs it as the hub opens (three passes, then rests) */
.dcir-edge-spark {
  position: absolute; top: -5px; left: 0; width: 100%; height: 11px; z-index: 42;
  pointer-events: none; opacity: 0; transform: translateX(-100%);
  background:
    radial-gradient(34px 5px at calc(100% - 24px) 50%, #ffffff 0%, rgba(165, 243, 252, 0.95) 30%, rgba(34, 211, 238, 0.35) 62%, transparent 78%),
    linear-gradient(90deg, transparent 55%, rgba(103, 232, 249, 0.8) 100%) center / 100% 2px no-repeat;
  animation: dcir-edge-run 3.6s cubic-bezier(0.45, 0, 0.25, 1) 0.35s 3;
}
@keyframes dcir-edge-run {
  0%        { transform: translateX(-100%); opacity: 0; }
  5%        { opacity: 1; }
  52%       { transform: translateX(0); opacity: 1; }
  58%, 100% { transform: translateX(0); opacity: 0; }
}
/* HUD corner brackets (static) */
.dcir-hud {
  position: absolute; inset: 10px; z-index: 41; pointer-events: none;
  --dcir-hud: rgba(103, 232, 249, 0.7);
  background:
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) left top / 18px 2px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) left top / 2px 18px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) right top / 18px 2px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) right top / 2px 18px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) left bottom / 18px 2px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) left bottom / 2px 18px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) right bottom / 18px 2px,
    linear-gradient(var(--dcir-hud), var(--dcir-hud)) right bottom / 2px 18px;
  background-repeat: no-repeat;
}

/* ---- header rail ---- */
.daily-hub-head {
  background: linear-gradient(180deg, rgba(34, 211, 238, 0.08), rgba(34, 211, 238, 0.015));
  border-bottom: 1px solid rgba(34, 211, 238, 0.16);
}
.daily-hub-head::after {
  content: ""; position: absolute; left: 8%; right: 8%; bottom: -1px; height: 1px; pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(103, 232, 249, 0.75), transparent);
}
.daily-hub-title {
  background: linear-gradient(100deg, #ffffff 0%, #ecfeff 30%, #67e8f9 58%, #22d3ee 78%, #a5f3fc 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 0 10px rgba(34, 211, 238, 0.55));
}
.daily-hub-badge {
  width: 42px; height: 42px; border-radius: 14px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid rgba(103, 232, 249, 0.6);
  background: radial-gradient(80% 80% at 50% 25%, rgba(34, 211, 238, 0.35), rgba(5, 9, 18, 0.92));
  box-shadow: 0 0 22px rgba(34, 211, 238, 0.35), inset 0 0 14px rgba(34, 211, 238, 0.25);
}
.dcir-badge-bolt { filter: drop-shadow(0 0 6px rgba(103, 232, 249, 0.95)); }
/* the badge crackles as the hub opens */
.dcir-badge-crackle {
  position: absolute; inset: 0; pointer-events: none; opacity: 0;
  animation: dcir-crackle 2.9s linear 0.2s 6;
}
@keyframes dcir-crackle {
  0%, 60%   { opacity: 0; }
  61%       { opacity: 1; }
  63%       { opacity: 0.2; }
  64.5%     { opacity: 0.95; }
  68%       { opacity: 0.3; }
  74%, 100% { opacity: 0; }
}
.dcir-count {
  color: #04121f;
  background: linear-gradient(180deg, #ecfeff, #67e8f9 55%, #22d3ee);
  box-shadow: 0 0 14px rgba(34, 211, 238, 0.65), inset 0 -1px 0 rgba(8, 145, 178, 0.6);
}
.dcir-chip { background: rgba(5, 9, 18, 0.72); }
.dcir-close { color: #7dd3fc; background: rgba(8, 15, 30, 0.85); border-color: rgba(56, 189, 248, 0.3); }
.dcir-close:hover { color: #ffffff; border-color: rgba(103, 232, 249, 0.7); box-shadow: 0 0 14px rgba(34, 211, 238, 0.35); }

/* ---- the two panes ---- */
.daily-hub-left {
  background:
    radial-gradient(120% 80% at 26% 6%, rgba(34, 211, 238, 0.10), transparent 62%),
    radial-gradient(70% 45% at 45% 58%, rgba(59, 130, 246, 0.09), transparent 70%);
}
.daily-hub-right {
  background:
    radial-gradient(120% 80% at 74% 6%, rgba(56, 189, 248, 0.10), transparent 62%),
    radial-gradient(60% 60% at 92% 100%, rgba(99, 102, 241, 0.09), transparent 70%);
}
.dcir-heading {
  background: linear-gradient(100deg, #ffffff, #cffafe 35%, #67e8f9 70%, #38bdf8);
  -webkit-background-clip: text; background-clip: text; color: transparent;
  filter: drop-shadow(0 0 8px rgba(34, 211, 238, 0.45));
}
.dcir-hot { text-shadow: 0 0 8px rgba(103, 232, 249, 0.9); }

/* overflow-x hidden: on phones this is a vertical scroller, and the sideways
   lightning rail's art is wider than the card — without it the whole hub
   could be dragged sideways. */
.daily-hub-grid { display: flex; flex-direction: column; min-height: 480px; overflow-y: auto; overflow-x: hidden; }
.daily-hub-bolt { display: none; }

/* --- phone: the same discharge, laid on its side between the halves --- */
.daily-hub-rail {
  position: relative; height: 34px; flex: none; overflow: visible; z-index: 25;
  background: linear-gradient(180deg, transparent, rgba(34, 211, 238, 0.08), transparent);
}

@media (min-width: 900px) {
  .daily-hub-card .daily-hub-grid {
    display: grid;
    /* minmax(0, …): a long tier name or mission title truncates instead of
       widening its column and shoving the split off the bolt */
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    min-height: 640px;
    overflow: hidden;
  }
  .daily-hub-card .daily-hub-left {
    clip-path: polygon(0 0, 100% 0, calc(100% - 82px) 100%, 0 100%);
    padding-right: 5.25rem;
  }
  .daily-hub-card .daily-hub-right {
    clip-path: polygon(82px 0, 100% 0, 100% 100%, 0 100%);
    padding-left: 5.25rem;
    overflow-y: auto;
  }
  .daily-hub-card .daily-hub-bolt {
    display: block;
    position: absolute;
    top: -3%; bottom: -3%; left: 50%;
    width: 132px;
    transform: translateX(-50%);
    z-index: 25;
  }
  .daily-hub-card .daily-hub-rail { display: none; }
}

/* ---- the discharge -------------------------------------------------------
   Layers, outermost first: violet-blue bloom (SVG Gaussian), cyan corona,
   white-hot core — all static, painted once. Over them, HTML layers that only
   flash opacity: strike A (the filament, forks and arc points —
   .circuit-strike-layer, in the META SCREENS block below), the off-beat
   strike B, a spark burst at each arc node and the light the strike throws
   across both panes. A, the node sparks and the pane flash share ONE 3.8s
   cycle so each strike lands as a single event; B runs on its own 4.6s beat
   in the gaps. The forks exist only during a strike — that absence is what
   makes it read as a crack rather than a squiggle. */
.daily-hub-bolt .circuit-svg,
.daily-hub-rail .circuit-svg { width: 100%; height: 100%; display: block; overflow: visible; }

/* the light the channel throws into both panes (static — a breathing layer
   this size cost a full-height blend every frame) */
.daily-hub-bolt::before,
.daily-hub-rail::before {
  content: ""; position: absolute; inset: -30% -140%;
  background: radial-gradient(50% 42% at 50% 50%, rgba(56, 189, 248, 0.18), transparent 70%);
  opacity: 0.7;
  pointer-events: none;
}
/* ...and the flash a strike throws (opacity only, on the strike-A beat) */
.dcir-flash {
  position: absolute; inset: 0 -320%; pointer-events: none; opacity: 0;
  background: radial-gradient(36% 46% at 50% 50%, rgba(165, 243, 252, 0.24), rgba(56, 189, 248, 0.10) 45%, transparent 72%);
  animation: dcir-flash 3.8s linear -1.6s 6;
}
.daily-hub-rail .dcir-flash { inset: -500% 0; }
@keyframes dcir-flash {
  0%, 53.5% { opacity: 0; }
  54%       { opacity: 1; }
  56%       { opacity: 0.3; }
  57.5%     { opacity: 0.9; }
  62%       { opacity: 0.25; }
  72%, 100% { opacity: 0; }
}
/* THE STRIKE — near-instant attack, a ragged double-hit, exponential decay */
@keyframes circuit-strike {
  0%, 53.5% { opacity: 0; }
  54%       { opacity: 1; }
  56%       { opacity: 0.18; }
  57.5%     { opacity: 1; }
  60%       { opacity: 0.45; }
  61.5%     { opacity: 0.92; }
  66%       { opacity: 0.3; }
  74%       { opacity: 0; }
  100%      { opacity: 0; }
}
/* the off-beat strike: a sister filament and the other forks */
.dcir-strike-b {
  position: absolute; inset: 0; pointer-events: none; opacity: 0;
  animation: dcir-strike-b 4.6s linear 0s 5;
}
@keyframes dcir-strike-b {
  0%, 17%   { opacity: 0; }
  17.6%     { opacity: 1; }
  19.5%     { opacity: 0.15; }
  21%       { opacity: 0.95; }
  24%       { opacity: 0.35; }
  25.5%     { opacity: 0.8; }
  30%       { opacity: 0.2; }
  37%, 100% { opacity: 0; }
}
/* a spark burst where the arc jumps to each pane (strike-A beat) */
.dcir-node-spark {
  position: absolute; width: 34px; height: 34px; margin: -17px 0 0 -17px;
  pointer-events: none; opacity: 0;
  background:
    radial-gradient(circle, #ffffff 0 9%, rgba(207, 250, 254, 0.9) 16%, rgba(34, 211, 238, 0.4) 34%, transparent 62%),
    linear-gradient(90deg, transparent, rgba(236, 254, 255, 0.95), transparent) center / 100% 2px no-repeat,
    linear-gradient(0deg, transparent, rgba(236, 254, 255, 0.8), transparent) center / 2px 70% no-repeat;
  animation: dcir-node-flash 3.8s linear -1.6s 6;
}
@keyframes dcir-node-flash {
  0%, 53.5% { opacity: 0; transform: scale(0.4) rotate(0deg); }
  54%       { opacity: 1; transform: scale(1.25) rotate(8deg); }
  56%       { opacity: 0.35; transform: scale(0.9) rotate(8deg); }
  57.5%     { opacity: 1; transform: scale(1.1) rotate(-6deg); }
  63%       { opacity: 0.4; transform: scale(0.8) rotate(-6deg); }
  72%, 100% { opacity: 0; transform: scale(0.5) rotate(0deg); }
}

/* ---- left pane: the plasma dynamo (ui/QuestArt.js CircuitDynamo) ---- */
.dcir-dyn { position: relative; }
.dcir-dyn > * { position: absolute; inset: 0; }
.dcir-dyn-halo {
  inset: -22%; border-radius: 50%; pointer-events: none;
  background: radial-gradient(closest-side, rgba(34, 211, 238, 0.34), rgba(59, 130, 246, 0.14) 55%, transparent 100%);
}
.dcir-dyn.is-dead .dcir-dyn-halo { background: radial-gradient(closest-side, rgba(30, 41, 59, 0.35), transparent 100%); }
.dcir-dyn-pulse {
  inset: 25%; border-radius: 50%; pointer-events: none; opacity: 0.55;
  background: radial-gradient(closest-side, rgba(236, 254, 255, 0.5), rgba(103, 232, 249, 0.28) 45%, transparent 100%);
  animation: dcir-core-pulse 2.4s ease-in-out infinite;
}
@keyframes dcir-core-pulse {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 0.85; }
}
@keyframes dcir-spin { to { transform: rotate(360deg); } }
/* the stator spins up only when the reward is actually claimable */
.circuit-dynamo-live .dcir-dyn-cage { animation: dcir-spin 28s linear infinite; }
.circuit-dynamo-live .dcir-dyn-ring { animation: dcir-spin 18s linear infinite reverse; }
/* arcs crackle from the core to the cage, on two uneven beats */
.dcir-dyn-arcs { pointer-events: none; opacity: 0; }
.dcir-dyn-arcs-a { animation: dcir-arc-a 2.7s linear 0.3s infinite; }
.dcir-dyn-arcs-b { animation: dcir-arc-b 3.5s linear 1.2s infinite; }
@keyframes dcir-arc-a {
  0%, 8%    { opacity: 0; }
  9%        { opacity: 1; }
  11%       { opacity: 0.2; }
  12.5%     { opacity: 0.95; }
  16%       { opacity: 0.3; }
  22%, 100% { opacity: 0; }
}
@keyframes dcir-arc-b {
  0%, 40%   { opacity: 0; }
  41%       { opacity: 0.95; }
  43%       { opacity: 0.15; }
  44.5%     { opacity: 1; }
  49%       { opacity: 0.25; }
  55%, 100% { opacity: 0; }
}
.dcir-status {
  align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px;
  border: 1px solid rgba(56, 189, 248, 0.25); background: rgba(5, 9, 18, 0.7);
  color: #64748b; font-size: 10px; font-weight: 900; letter-spacing: 0.22em; text-transform: uppercase; line-height: 1;
}
.dcir-status.is-live { color: #a5f3fc; border-color: rgba(103, 232, 249, 0.55); box-shadow: 0 0 12px rgba(34, 211, 238, 0.25); }
.dcir-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #334155; }
.dcir-status.is-live .dcir-status-dot { background: #67e8f9; box-shadow: 0 0 8px #22d3ee; }

/* the login-streak ladder: a bank of charge cells */
.dcir-cell { border-color: #16213a; background: rgba(3, 7, 16, 0.55); }
.dcir-cell.is-done { border-color: rgba(56, 189, 248, 0.45); background: linear-gradient(180deg, rgba(14, 165, 233, 0.14), rgba(3, 7, 16, 0.5)); }
.dcir-cell.is-current {
  border-color: #67e8f9;
  background: linear-gradient(180deg, rgba(34, 211, 238, 0.24), rgba(8, 47, 73, 0.35));
  box-shadow: 0 0 16px rgba(34, 211, 238, 0.45), inset 0 0 10px rgba(103, 232, 249, 0.25);
}
/* today's cell pulses a few times as the hub opens, then rests charged */
.dcir-cell.is-current::after {
  content: ""; position: absolute; inset: -3px; border-radius: inherit; pointer-events: none;
  box-shadow: 0 0 22px rgba(103, 232, 249, 0.75); opacity: 0.35;
  animation: meta-breathe 1.6s ease-in-out 6;
}
.dcir-cell-label { color: #94a3b8; }
.dcir-cell-day { color: #7b8ba3; }
.dcir-cell.is-done .dcir-cell-day { color: #38bdf8; }
.dcir-cell.is-current .dcir-cell-day { color: #a5f3fc; }
.dcir-cell.is-done .dcir-cell-label { color: #7dd3fc; }
.dcir-cell.is-current .dcir-cell-label { color: #ffffff; text-shadow: 0 0 8px rgba(103, 232, 249, 0.9); }

/* the recharging readout */
.dcir-readout {
  position: relative; border-radius: 16px;
  border: 1px solid rgba(56, 189, 248, 0.28);
  background: linear-gradient(180deg, rgba(14, 30, 56, 0.72), rgba(5, 9, 18, 0.88));
  box-shadow: inset 0 1px 0 rgba(165, 243, 252, 0.08), 0 0 24px rgba(14, 165, 233, 0.12);
}
.dcir-readout-num { color: #e0f2fe; text-shadow: 0 0 12px rgba(56, 189, 248, 0.7); letter-spacing: 0.04em; }

/* THE ELECTRIC CTA (Spin, Claim) — dark navy on a white-hot → cyan bar */
.dcir-cta {
  color: #04121f;
  background: linear-gradient(180deg, #ecfeff 0%, #a5f3fc 26%, #67e8f9 46%, #22d3ee 62%, #0891b2 100%);
  border: 1px solid rgba(236, 254, 255, 0.75);
  box-shadow: 0 4px 0 #0e4f66, 0 0 22px rgba(34, 211, 238, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.95), inset 0 -3px 8px rgba(8, 145, 178, 0.45);
  text-shadow: 0 1px 0 rgba(236, 254, 255, 0.55);
}
.dcir-cta:active:not(:disabled) {
  box-shadow: 0 1px 0 #0e4f66, 0 0 16px rgba(34, 211, 238, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.9), inset 0 -3px 8px rgba(8, 145, 178, 0.45);
}
.dcir-cta.dcir-cta-pro {
  color: #1e1147;
  background: linear-gradient(180deg, #f5f3ff 0%, #ddd6fe 26%, #c4b5fd 46%, #a78bfa 62%, #7c3aed 100%);
  border-color: rgba(245, 243, 255, 0.75);
  box-shadow: 0 4px 0 #3b1d7a, 0 0 22px rgba(167, 139, 250, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.95), inset 0 -3px 8px rgba(124, 58, 237, 0.45);
  text-shadow: 0 1px 0 rgba(245, 243, 255, 0.55);
}
.dcir-cta.dcir-cta-pro:active:not(:disabled) {
  box-shadow: 0 1px 0 #3b1d7a, 0 0 16px rgba(167, 139, 250, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.9), inset 0 -3px 8px rgba(124, 58, 237, 0.45);
}
/* the Tower keeps its amber, set in the same navy socket vocabulary */
.dcir-cta-amber {
  color: #1c0f02;
  background: linear-gradient(180deg, #fef3c7, #fcd34d 40%, #f59e0b 70%, #b45309);
  box-shadow: 0 3px 0 #78350f, 0 0 14px rgba(251, 191, 36, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.dcir-tower { border-color: #16213a; background: linear-gradient(90deg, rgba(15, 23, 42, 0.6), rgba(5, 9, 18, 0.75)); }
.dcir-tower.is-live {
  border-color: rgba(251, 191, 36, 0.5);
  background: linear-gradient(90deg, rgba(251, 191, 36, 0.13), rgba(6, 11, 22, 0.82) 55%, rgba(34, 211, 238, 0.06));
  box-shadow: inset 0 1px 0 rgba(253, 230, 138, 0.08), 0 0 20px rgba(251, 191, 36, 0.08);
}
.dcir-tower.is-live::before {
  content: ""; position: absolute; left: 18px; right: 18px; top: 0; height: 1px; pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(253, 230, 138, 0.7), rgba(103, 232, 249, 0.4), transparent);
}
.dcir-tower-socket {
  display: inline-flex; padding: 3px; border-radius: 12px;
  border: 1px solid rgba(251, 191, 36, 0.35);
  background: radial-gradient(circle at 50% 30%, rgba(251, 191, 36, 0.2), rgba(5, 9, 18, 0.9));
}
.dcir-tower:not(.is-live) .dcir-tower-socket { border-color: #1e293b; background: rgba(5, 9, 18, 0.8); }

/* ---- right pane: the missions bus and its circuit modules ---- */
.dcir-bus { position: relative; padding-left: 18px; }
/* the bus line the modules are wired to */
.dcir-bus::before {
  content: ""; position: absolute; left: 6px; top: 38px; bottom: 21px; width: 2px; border-radius: 2px; pointer-events: none;
  background: linear-gradient(180deg, rgba(103, 232, 249, 0.85), rgba(56, 189, 248, 0.55) 60%, rgba(99, 102, 241, 0.35));
  box-shadow: 0 0 8px rgba(34, 211, 238, 0.45);
}
.dcir-bus.is-pro::before {
  top: 18px; bottom: auto; height: 40px;
  background: linear-gradient(180deg, rgba(196, 181, 253, 0.85), rgba(139, 92, 246, 0.4));
  box-shadow: 0 0 8px rgba(167, 139, 250, 0.45);
}
/* a charge packet runs down the bus as the hub opens (clipped to its lane) */
.dcir-bus-lane {
  position: absolute; left: 3px; top: 38px; bottom: 21px; width: 8px;
  margin: 0 !important; overflow: hidden; pointer-events: none;
}
.dcir-bus-lane::after {
  content: ""; position: absolute; left: 0; top: 0; width: 8px; height: 100%; opacity: 0;
  background:
    radial-gradient(4px 12px at 50% calc(100% - 8px), #ffffff, rgba(165, 243, 252, 0.9) 40%, transparent 100%),
    linear-gradient(180deg, transparent 55%, rgba(103, 232, 249, 0.7)) center / 2px 100% no-repeat;
  transform: translateY(-100%);
  animation: dcir-bus-run 3.2s cubic-bezier(0.5, 0, 0.3, 1) 0.8s 3;
}
@keyframes dcir-bus-run {
  0%        { transform: translateY(-100%); opacity: 0; }
  6%        { opacity: 1; }
  55%       { transform: translateY(0); opacity: 1; }
  62%, 100% { transform: translateY(0); opacity: 0; }
}
/* the wire from the bus into a module, a solder joint at each end:
   dark when idle, charged cyan once the mission draws power, white-hot when
   it is full, dead once discharged */
.dcir-mod-wrap { --dcir-wire: #1a2538; --dcir-node: #1e293b; --dcir-glow: transparent; }
.dcir-mod-wrap.is-powered { --dcir-wire: rgba(56, 189, 248, 0.8); --dcir-node: #67e8f9; --dcir-glow: rgba(34, 211, 238, 0.6); }
.dcir-mod-wrap.is-live { --dcir-wire: #a5f3fc; --dcir-node: #ffffff; --dcir-glow: rgba(103, 232, 249, 0.9); }
.dcir-mod-wrap.is-pro.is-powered,
.dcir-mod-wrap.is-pro.is-live { --dcir-wire: #c4b5fd; --dcir-node: #f5f3ff; --dcir-glow: rgba(167, 139, 250, 0.85); }
.dcir-mod-wrap.is-claimed { --dcir-wire: #141d2e; --dcir-node: #1a2538; }
.dcir-wire {
  position: absolute; right: 100%; top: 37px; width: 12px; height: 2px; z-index: 2; pointer-events: none;
  background: var(--dcir-wire); box-shadow: 0 0 6px var(--dcir-glow);
}
.dcir-wire.is-short { top: 50%; margin-top: -1px; }
.dcir-wire::before {
  content: ""; position: absolute; left: -3px; top: 50%; width: 8px; height: 8px; margin-top: -4px; border-radius: 50%;
  background: var(--dcir-node); box-shadow: 0 0 8px var(--dcir-glow), 0 0 0 2px #060b16;
}
.dcir-wire::after {
  content: ""; position: absolute; right: -2px; top: 50%; width: 5px; height: 5px; margin-top: -2.5px; border-radius: 50%;
  background: var(--dcir-node); box-shadow: 0 0 6px var(--dcir-glow);
}
/* a spark rides the wire into a full module a few times */
.dcir-wire-spark { position: absolute; right: 100%; top: 33px; width: 12px; height: 10px; z-index: 3; pointer-events: none; overflow: hidden; }
.dcir-wire-spark::after {
  content: ""; position: absolute; top: 0; left: 0; width: 10px; height: 10px; border-radius: 50%; opacity: 0;
  background: radial-gradient(circle, #ffffff 0 25%, rgba(165, 243, 252, 0.8) 45%, transparent 70%);
  transform: translateX(-8px);
  animation: dcir-wire-run 1.6s ease-in 0.4s 5;
}
@keyframes dcir-wire-run {
  0%        { transform: translateX(-8px); opacity: 0; }
  15%       { opacity: 1; }
  70%       { transform: translateX(10px); opacity: 1; }
  80%, 100% { transform: translateX(10px); opacity: 0; }
}
.dcir-mod { box-shadow: inset 0 1px 0 rgba(165, 243, 252, 0.05); }
.dcir-mod.is-live { box-shadow: 0 0 22px rgba(34, 211, 238, 0.22), inset 0 0 18px rgba(34, 211, 238, 0.10); }
.dcir-mod.is-claimed { opacity: 0.62; }
/* HUD ticks on a full module */
.dcir-mod-hud {
  position: absolute; inset: 6px; pointer-events: none; opacity: 0.85;
  background:
    linear-gradient(#a5f3fc, #a5f3fc) right top / 10px 2px,
    linear-gradient(#a5f3fc, #a5f3fc) right top / 2px 10px,
    linear-gradient(#a5f3fc, #a5f3fc) left bottom / 10px 2px,
    linear-gradient(#a5f3fc, #a5f3fc) left bottom / 2px 10px;
  background-repeat: no-repeat;
}
/* the emblem's powered socket: a recessed ring and four solder pins */
.dcir-socket { width: 52px; height: 52px; box-shadow: inset 0 0 0 3px #060b16, inset 0 0 0 4px rgba(103, 232, 249, 0.14); }
/* phones: a slimmer bus and socket, so mission names keep their room */
@media (max-width: 639px) {
  .dcir-bus { padding-left: 15px; }
  .dcir-bus::before { left: 4px; }
  .dcir-bus-lane { left: 1px; }
  .dcir-wire { width: 10px; top: 33px; }
  .dcir-wire-spark { width: 10px; top: 29px; }
  .dcir-socket { width: 44px; height: 44px; }
}
.dcir-socket::before {
  content: ""; position: absolute; inset: 3px; border-radius: 9px; pointer-events: none;
  --dcir-pin: rgba(165, 243, 252, 0.8);
  background:
    radial-gradient(circle at 4px 4px, var(--dcir-pin) 1.3px, transparent 2px),
    radial-gradient(circle at calc(100% - 4px) 4px, var(--dcir-pin) 1.3px, transparent 2px),
    radial-gradient(circle at 4px calc(100% - 4px), var(--dcir-pin) 1.3px, transparent 2px),
    radial-gradient(circle at calc(100% - 4px) calc(100% - 4px), var(--dcir-pin) 1.3px, transparent 2px);
}
.dcir-mod.is-idle .dcir-socket::before { --dcir-pin: rgba(100, 116, 139, 0.6); }
.dcir-mod.is-claimed .dcir-socket::before { --dcir-pin: rgba(51, 65, 85, 0.6); }
.dcir-socket-dead { filter: grayscale(1) brightness(0.55); }
.dcir-check {
  position: absolute; bottom: -6px; right: -6px; width: 22px; height: 22px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: 12px; font-weight: 900; line-height: 1; color: #38bdf8;
  background: #0a1222; border: 1px solid #1e3a5f;
}
/* the segmented charge bar (fill = scaleX glide under a static cell mask) */
.dcir-charge {
  position: relative; border-radius: 3px; overflow: hidden;
  background: #0c172b; border: 1px solid rgba(56, 189, 248, 0.2);
}
.dcir-charge-fill {
  position: absolute; inset: 0; transform-origin: left center;
  transition: transform 0.7s var(--prm-glide);
  background: linear-gradient(90deg, #1d4ed8, #0ea5e9 55%, #38bdf8);
}
.dcir-charge-cells {
  position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(90deg,
    transparent 0, transparent calc(100% / var(--cells) - 2px),
    #050a14 calc(100% / var(--cells) - 2px), #050a14 calc(100% / var(--cells)));
}
.dcir-charge.is-full { border-color: rgba(165, 243, 252, 0.7); box-shadow: 0 0 10px rgba(34, 211, 238, 0.6); }
.dcir-charge.is-full .dcir-charge-fill { background: linear-gradient(90deg, #22d3ee, #a5f3fc 50%, #ffffff); }
.dcir-charge.is-pro .dcir-charge-fill { background: linear-gradient(90deg, #4338ca, #6366f1 50%, #a78bfa); }
.dcir-charge.is-pro-full { border-color: rgba(221, 214, 254, 0.7); box-shadow: 0 0 10px rgba(167, 139, 250, 0.6); }
.dcir-charge.is-pro-full .dcir-charge-fill { background: linear-gradient(90deg, #8b5cf6, #c4b5fd 50%, #f5f3ff); }
.dcir-charge.is-dead { border-color: #111a2b; background: #080e1a; }
.dcir-charge.is-dead .dcir-charge-fill { background: #141d2e; }
/* a full bar shimmers a few times */
.dcir-charge.is-full::after,
.dcir-charge.is-pro-full::after {
  content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 30%; pointer-events: none;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.85), transparent);
  transform: translateX(-120%);
  animation: dcir-charge-sheen 2.2s ease-in-out 0.6s 4;
}
@keyframes dcir-charge-sheen {
  0%, 20%  { transform: translateX(-120%); }
  100%     { transform: translateX(360%); }
}
/* the "All 3" capacitor at the end of the bus */
.dcir-cap {
  border-color: rgba(56, 189, 248, 0.25);
  background: linear-gradient(90deg, rgba(14, 165, 233, 0.10), rgba(5, 9, 18, 0.72) 60%);
}
.dcir-pip { width: 8px; height: 8px; border-radius: 2px; background: #0f1829; border: 1px solid #1e293b; }
.dcir-pip.is-on { background: #67e8f9; border-color: #cffafe; box-shadow: 0 0 8px rgba(34, 211, 238, 0.8); }
.dcir-reroll { background: rgba(5, 9, 18, 0.62); }
.dcir-prolock { background: linear-gradient(90deg, rgba(76, 29, 149, 0.2), rgba(5, 9, 18, 0.62)); }

/* ---- the in-slot quest ticker ---- */
.dcir-ticker {
  border-color: rgba(56, 189, 248, 0.25);
  background: linear-gradient(180deg, #0b1324, #060a14);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55), 0 0 18px rgba(14, 165, 233, 0.12);
}
.dcir-ticker.is-live {
  border-color: rgba(103, 232, 249, 0.75);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.55), 0 0 22px rgba(34, 211, 238, 0.4);
}
.dcir-ticker-body { transition: background-color 0.15s ease; }
.dcir-ticker-body:hover { background-color: rgba(34, 211, 238, 0.07); }
.dcir-tick-badge { border-color: rgba(56, 189, 248, 0.35); background: #0a1222; }
.dcir-tick-badge.is-live { border-color: #67e8f9; background: radial-gradient(circle at 50% 35%, rgba(34, 211, 238, 0.35), #06101f); }

/* ---- guards ---- reduced motion stills everything above; performance mode
   stills it too (the strike layers there are handled in the META SCREENS
   block). Resting states: arcs A stay lit on a live dynamo, the rest dark. */
.reduced-motion .dcir-edge-spark,
.reduced-motion .dcir-badge-crackle,
.reduced-motion .dcir-flash,
.reduced-motion .dcir-strike-b,
.reduced-motion .dcir-node-spark,
.reduced-motion .dcir-dyn-cage,
.reduced-motion .dcir-dyn-ring,
.reduced-motion .dcir-dyn-pulse,
.reduced-motion .dcir-dyn-arcs,
.reduced-motion .dcir-cell.is-current::after,
.reduced-motion .dcir-bus-lane::after,
.reduced-motion .dcir-wire-spark::after,
.reduced-motion .dcir-charge.is-full::after,
.reduced-motion .dcir-charge.is-pro-full::after,
.performance-mode .dcir-edge-spark,
.performance-mode .dcir-badge-crackle,
.performance-mode .dcir-flash,
.performance-mode .dcir-strike-b,
.performance-mode .dcir-node-spark,
.performance-mode .dcir-cell.is-current::after,
.performance-mode .dcir-bus-lane::after,
.performance-mode .dcir-wire-spark::after,
.performance-mode .dcir-charge.is-full::after,
.performance-mode .dcir-charge.is-pro-full::after { animation: none !important; }
.reduced-motion .dcir-dyn-arcs-a,
.performance-mode .dcir-dyn-arcs-a { opacity: 0.55; }

/* ============================================================
   BATTLE PASS STYLES
   ============================================================ */
.bp-track-bg {
  background-image: 
    linear-gradient(rgba(139, 92, 246, 0.15) 1px, transparent 1px),
    linear-gradient(90deg, rgba(139, 92, 246, 0.15) 1px, transparent 1px);
  background-size: 40px 40px;
  animation: bp-grid-scroll 20s linear infinite;
}

@keyframes bp-grid-scroll {
  0% { background-position: 0 0; }
  100% { background-position: -40px -40px; }
}

.bp-node {
  flex-shrink: 0;
  transform: perspective(600px) rotateX(2deg);
}

.bp-glow-effect::before {
  content: '';
  position: absolute;
  top: -12px;
  left: -12px;
  right: -12px;
  bottom: -12px;
  border-radius: 2.5rem;
  background: radial-gradient(circle, rgba(255,255,255,0.9) 0%, transparent 65%);
  z-index: -1;
  animation: bp-glow-pulse 1.5s infinite ease-in-out;
  opacity: 0.8;
}

@keyframes bp-glow-pulse {
  0% { transform: scale(0.95); opacity: 0.5; }
  50% { transform: scale(1.1); opacity: 1; filter: drop-shadow(0 0 10px white); }
  100% { transform: scale(0.95); opacity: 0.5; }
}

@keyframes animate-bounce-slight {
  0%, 100% { transform: translateY(0) perspective(600px) rotateX(2deg); }
  50% { transform: translateY(-4px) perspective(600px) rotateX(2deg); }
}

.animate-bounce-slight {
  animation: animate-bounce-slight 2s infinite;
}

/* ---- Brawl-Pass redesign (vertical ladder) ---- */

/* Claimable reward tiles gently breathe so the eye is drawn to them. */
@keyframes bp-tile-live {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-5px) scale(1.02); }
}
.bp-tile-live { animation: bp-tile-live 1.8s ease-in-out infinite; }

/* The little "Claim ▸" pill nudges left↔right like a button beckoning a tap. */
@keyframes bp-claim-pill {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(3px); }
}
.bp-claim-pill { animation: bp-claim-pill 1s ease-in-out infinite; }

/* Energy flowing along the travelled portion of the progression spine. */
@keyframes bp-spine-flow {
  0%   { background-position: 0 0; }
  100% { background-position: 24px 0; }
}
.bp-spine-lit {
  background-size: 24px 100% !important;
  animation: bp-spine-flow 0.8s linear infinite;
}

/* Pulsing halo around the player's current tier badge ("you are here"). */
@keyframes bp-current-ring {
  0%   { transform: scale(1);   box-shadow: 0 0 0 0 rgba(251,191,36,0.7); }
  70%  { transform: scale(1.5); box-shadow: 0 0 0 14px rgba(251,191,36,0); }
  100% { transform: scale(1.5); box-shadow: 0 0 0 0 rgba(251,191,36,0); }
}
.bp-current-ring {
  border-radius: 9999px;
  animation: bp-current-ring 1.6s ease-out infinite;
}

/* Sheen sweeping across the XP bar fill. */
@keyframes bp-xp-shine {
  0%   { background-position: -120% 0; }
  100% { background-position: 220% 0; }
}
.bp-xp-shine {
  position: relative;
  overflow: hidden;
}
.bp-xp-shine::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 35%, rgba(255,255,255,0.85) 50%, transparent 65%);
  background-size: 220% 100%;
  animation: bp-xp-shine 2.2s linear infinite;
}

/* Claim All button breathes a green glow while rewards are waiting. */
@keyframes bp-claimall-pulse {
  0%, 100% { box-shadow: 0 6px 0 #14532d, 0 0 18px rgba(34,197,94,0.45); }
  50%      { box-shadow: 0 6px 0 #14532d, 0 0 30px rgba(34,197,94,0.85); }
}
.bp-claimall-pulse { animation: bp-claimall-pulse 1.4s ease-in-out infinite; }

/* Finale hero strip: slow shifting gold→purple aurora behind the legendary. */
@keyframes bp-finale-strip {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.bp-finale-strip {
  background: linear-gradient(100deg, rgba(120,53,15,0.55), rgba(76,29,149,0.55), rgba(120,53,15,0.55));
  background-size: 200% 100%;
  animation: bp-finale-strip 8s linear infinite;
}
@keyframes bp-finale-icon {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50%      { transform: translateY(-6px) rotate(4deg); }
}
.bp-finale-icon { animation: bp-finale-icon 3s ease-in-out infinite; }

/* The legendary tier-30 row gets a permanent gold aura. */
.bp-finale-row {
  background: radial-gradient(ellipse at center, rgba(251,191,36,0.12), transparent 70%);
  box-shadow: inset 0 0 40px rgba(251,191,36,0.12);
}

/* Big premium-unlock banner: a slow gold↔purple aurora with a sweeping sheen. */
@keyframes bp-unlock-banner {
  0%   { background-position: 0% 50%; }
  100% { background-position: 200% 50%; }
}
.bp-unlock-banner {
  background: linear-gradient(100deg, rgba(76,29,149,0.85), rgba(120,53,15,0.85), rgba(168,85,247,0.7), rgba(76,29,149,0.85));
  background-size: 200% 100%;
  animation: bp-unlock-banner 6s linear infinite;
}
.bp-unlock-banner::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(110deg, transparent 40%, rgba(255,255,255,0.18) 50%, transparent 60%);
  background-size: 220% 100%;
  animation: bp-xp-shine 3s linear infinite;
  pointer-events: none;
}

/* Reward-haul screen: cards pop in with a little overshoot. */
@keyframes bp-haul-pop {
  0%   { transform: scale(0.4) translateY(20px); opacity: 0; }
  70%  { transform: scale(1.08) translateY(0); opacity: 1; }
  100% { transform: scale(1) translateY(0); opacity: 1; }
}
.bp-haul-pop {
  animation: bp-haul-pop 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) both;
}

/* Slow rotating ray backdrop behind the haul celebration. */
@keyframes bp-ray-spin { to { transform: translate(-50%, -50%) rotate(360deg); } }
.bp-ray-spin { animation: bp-ray-spin 18s linear infinite; }

/* Confetti raining down the reward-haul screen. */
@keyframes bp-confetti-fall {
  0%   { transform: translateY(-12vh) rotate(0deg);   opacity: 0; }
  12%  { opacity: 1; }
  100% { transform: translateY(108vh) rotate(620deg); opacity: 0.9; }
}
.bp-confetti {
  position: absolute;
  top: 0;
  width: 10px;
  height: 14px;
  border-radius: 2px;
  opacity: 0;
  animation-name: bp-confetti-fall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/* Glossy sheen sweeping across claimable reward tiles. */
.bp-tile-live::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: linear-gradient(110deg, transparent 42%, rgba(255,255,255,0.32) 50%, transparent 58%);
  background-size: 260% 100%;
  animation: bp-xp-shine 2.6s linear infinite;
  pointer-events: none;
}

/* Epic treatment for a tile showing the exclusive bundle gem: the TILE itself
   becomes a flowing rainbow with a pulsing glow. The rainbow gradient is set
   inline on the tile background, so the icon sits cleanly on top — nothing
   overlays or bleeds onto it. This just animates the flow + glow. */
.bp-bundle-epic {
  background-size: 100% 100%;
  background-position: center;
  animation: bp-bundle-glow 2.2s ease-in-out infinite;
}
@keyframes bp-bundle-glow {
  0%, 100% { box-shadow: 0 0 26px rgba(168,85,247,0.6), 0 10px 0 rgba(0,0,0,0.35); }
  50%      { box-shadow: 0 0 46px rgba(244,114,182,0.85), 0 10px 0 rgba(0,0,0,0.35); }
}

/* Animated gradient "BUNDLE" ribbon. */
@keyframes bp-bundle-ribbon { 0% { background-position: 0% 50%; } 100% { background-position: 300% 50%; } }
.bp-bundle-ribbon {
  background: linear-gradient(90deg, #22d3ee, #a855f7, #f472b6, #fbbf24, #22d3ee);
  background-size: 300% 100%;
  animation: bp-bundle-ribbon 3s linear infinite;
  box-shadow: 0 0 12px rgba(168,85,247,0.8);
}

/* Quick fade when an alternating bundle tile swaps faces. */
@keyframes bp-face-in { 0% { opacity: 0; transform: scale(0.85); } 100% { opacity: 1; transform: scale(1); } }
.bp-face-in { animation: bp-face-in 0.4s ease-out both; }

/* Circular tier-progress ring (header): a gold conic gradient shows % to the next
   tier; the inner dark disc (a child) masks the centre, leaving a glowing ring. */
.bp-tier-ring::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 9999px;
  background: conic-gradient(from -90deg, #fde047, #f59e0b var(--pct, 0%), rgba(41,37,36,0.85) var(--pct, 0%) 100%);
  box-shadow: 0 0 16px rgba(251,191,36,0.45);
}

/* Pulsing rarity halo marking a milestone (cosmetic-bearing) tier on the spine. */
@keyframes bp-spine-milestone {
  0%, 100% { box-shadow: 0 0 8px var(--ms); opacity: 0.85; }
  50%      { box-shadow: 0 0 18px var(--ms); opacity: 1; }
}
.bp-spine-milestone {
  border: 2px solid var(--ms);
  animation: bp-spine-milestone 2.2s ease-in-out infinite;
}

/* Shimmering rarity banner on legendary/epic haul drops. */
@keyframes bp-rarity-banner {
  0%, 100% { transform: scale(1); filter: brightness(1); }
  50%      { transform: scale(1.05); filter: brightness(1.18); }
}
.bp-rarity-banner { animation: bp-rarity-banner 1.4s ease-in-out infinite; }

/* Respect reduced-motion: kill the looping flourishes, keep the layout. */
.reduced-motion .bp-tile-live,
.reduced-motion .bp-claim-pill,
.reduced-motion .bp-spine-lit,
.reduced-motion .bp-current-ring,
.reduced-motion .bp-xp-shine::after,
.reduced-motion .bp-claimall-pulse,
.reduced-motion .bp-finale-strip,
.reduced-motion .bp-finale-icon,
.reduced-motion .bp-unlock-banner,
.reduced-motion .bp-unlock-banner::after,
.reduced-motion .bp-ray-spin,
.reduced-motion .bp-tile-live::after,
.reduced-motion .bp-bundle-epic,
.reduced-motion .bp-bundle-epic::before,
.reduced-motion .bp-bundle-ribbon,
.reduced-motion .bp-spine-milestone,
.reduced-motion .bp-rarity-banner {
  animation: none !important;
}
/* Haul cards still need to be visible (not stuck at opacity:0) without motion. */
.reduced-motion .bp-haul-pop { animation: none !important; opacity: 1; }
/* Face swaps and confetti are purely decorative under reduced-motion. */
.reduced-motion .bp-face-in { animation: none !important; opacity: 1; }
.reduced-motion .bp-confetti { display: none !important; }

/* ============================================================
   META SCREENS — static-first finish (build 58 perf pass)
   Battle Pass, Daily Circuit, Daily Wheel, VIP, Settings (ui/BattlePassUI.js,
   MetaFeatures.js, DailyWheel.js, VIPComponents.js, SettingsModal.js).
   Everything here paints ONCE. The few loops left are one focal element per
   screen, on transform/opacity only, and every one is silenced at the end of
   this block under .reduced-motion AND .performance-mode.
   ============================================================ */
/* Battle Pass track backdrop: the grid + two soft colour pools, static (it
   used to scroll its background-position forever and carry two 150px-blur
   blend-mode orbs). */
.bp-grid-static {
  background-image:
    radial-gradient(38% 42% at 24% 30%, rgba(147, 51, 234, 0.13), transparent 70%),
    radial-gradient(34% 40% at 76% 72%, rgba(217, 119, 6, 0.11), transparent 70%),
    linear-gradient(rgba(139, 92, 246, 0.12) 1px, transparent 1px),
    linear-gradient(90deg, rgba(139, 92, 246, 0.12) 1px, transparent 1px);
  background-size: 100% 100%, 100% 100%, 40px 40px, 40px 40px;
}
/* A claimable tile: lifts on hover, crunches on press (no idle loop). */
.bp-tile-ready { transition: transform 0.22s var(--prm-snap); }
@media (hover: hover) {
  .bp-tile-ready:hover { transform: translateY(-4px) scale(1.03); }
}
.bp-tile-ready:active { transform: scale(0.96); }
/* "You are here": a ring that ripples out of the current tier badge. */
@keyframes bp-here-ring {
  0%   { transform: scale(1);    opacity: 0.9; }
  100% { transform: scale(1.55); opacity: 0; }
}
.bp-here-ring {
  border-radius: 9999px;
  border: 2px solid rgba(251, 191, 36, 0.95);
  box-shadow: 0 0 12px rgba(251, 191, 36, 0.6);
  opacity: 0.6;
  animation: bp-here-ring 1.9s var(--prm-glide) infinite;
}
/* A claim landing: the check pops in and settles (one shot). */
@keyframes bp-claim-settle {
  0%   { transform: scale(0.4);  opacity: 0; }
  60%  { transform: scale(1.14); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}
.bp-claim-settle { display: inline-block; animation: bp-claim-settle 0.38s var(--prm-pop) backwards; }
.bp-bundle-ribbon-static {
  background: linear-gradient(90deg, #22d3ee, #a855f7, #f472b6, #fbbf24);
  box-shadow: 0 0 12px rgba(168, 85, 247, 0.8);
}
.bp-banner-static {
  background: linear-gradient(100deg, rgba(76, 29, 149, 0.9), rgba(120, 53, 15, 0.85) 55%, rgba(88, 28, 135, 0.85));
}
.bp-finale-static {
  background: linear-gradient(100deg, rgba(120, 53, 15, 0.55), rgba(76, 29, 149, 0.5), rgba(120, 53, 15, 0.4));
}
/* Breathing glow for a screen's single call to action: a STATIC box-shadow on
   a pseudo layer whose opacity breathes. Colour via --glow. It breathes a few
   times when the button appears, then rests lit — attention on arrival, zero
   idle cost after (a loop left running costs every frame for as long as the
   screen stays open). */
@keyframes meta-breathe {
  0%, 100% { opacity: 0.35; }
  50%      { opacity: 1; }
}
.meta-glow { position: relative; }
.meta-glow::after {
  content: "";
  position: absolute;
  inset: -3px;
  border-radius: inherit;
  box-shadow: 0 0 26px var(--glow, rgba(251, 191, 36, 0.7));
  opacity: 0.35;
  pointer-events: none;
  animation: meta-breathe 1.8s ease-in-out 4;
}
/* Sheen sweep for a primary button: a skewed highlight that TRANSLATES
   across (compositor-only), resting off-button most of the cycle — three
   passes after the button appears, then it rests off-button. */
@keyframes meta-sheen {
  0%, 58% { transform: translateX(-180%) skewX(-18deg); }
  100%    { transform: translateX(340%) skewX(-18deg); }
}
.meta-sheen { position: relative; overflow: hidden; isolation: isolate; }
.meta-sheen::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 40%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.5), transparent);
  transform: translateX(-180%) skewX(-18deg);
  pointer-events: none;
  animation: meta-sheen 3.4s var(--prm-glide) 3;
}
/* Reward haul: rays swing in once and hold; confetti falls once. */
@keyframes bph-rays-in {
  from { opacity: 0; transform: translate(-50%, -50%) rotate(-28deg) scale(0.75); }
  to   { opacity: 1; transform: translate(-50%, -50%) rotate(0deg) scale(1); }
}
.bph-rays { transform: translate(-50%, -50%); animation: bph-rays-in 1.2s var(--prm-glide) backwards; }
.bph-confetti {
  position: absolute;
  top: 0;
  width: 10px;
  height: 14px;
  border-radius: 2px;
  opacity: 0;
  animation-name: bp-confetti-fall;
  animation-timing-function: cubic-bezier(0.3, 0.6, 0.6, 1);
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
}
@keyframes bph-pop {
  0%   { transform: scale(0.6);  opacity: 0; }
  60%  { transform: scale(1.08); opacity: 1; }
  100% { transform: scale(1);    opacity: 1; }
}
.bph-pop { animation: bph-pop 0.42s var(--prm-pop) backwards; }
/* Solid panel material for the big meta modals. They used .liquid-glass, whose
   16px backdrop blur sat over the live lobby and was recomputed every frame
   the lobby animated; this keeps the glass edge (insets) on an opaque fill. */
.meta-panel {
  background: linear-gradient(180deg, #1a1714 0%, #110f0d 100%);
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.12),
    inset 0 0 0 1px rgba(255, 255, 255, 0.04),
    inset 0 -18px 26px -18px rgba(0, 0, 0, 0.55),
    0 24px 60px rgba(0, 0, 0, 0.6);
}
/* Daily Circuit bolt: the strike flashes as ONE composited layer (a wrapper
   div's opacity) instead of per-path SVG opacity loops that repainted the
   blurred bolt every frame; the bloom path is static. It strikes a few times
   as the hub opens, then the channel rests lit (no idle cost). */
.circuit-strike-layer {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  will-change: opacity;
  animation: circuit-strike 3.8s linear -1.6s 6; /* first strike ~0.45s in; the
     node sparks + pane flash (#1 block) share this beat */
}
.circuit-glow-static { opacity: 0.8; }

/* Guards — reduced-motion kills every loop and one-shot above; performance
   mode kills the loops (one-shots stay: they are brief and signal a claim). */
.reduced-motion .bp-here-ring,
.reduced-motion .bp-claim-settle,
.reduced-motion .meta-glow::after,
.reduced-motion .meta-sheen::after,
.reduced-motion .bph-rays,
.reduced-motion .bph-pop,
.performance-mode .bp-here-ring,
.performance-mode .meta-glow::after,
.performance-mode .meta-sheen::after { animation: none !important; }
.reduced-motion .bph-confetti { display: none !important; }
.reduced-motion .circuit-strike-layer,
.performance-mode .circuit-strike-layer { animation: none !important; opacity: 0.7; }
/* Resting lit, the bolt keeps its filament and arc points but not the forks —
   frozen forks read as brackets hanging off the channel. */
.reduced-motion .circuit-strike-forks,
.performance-mode .circuit-strike-forks { display: none; }

/* ============================================================
   VIP DASHBOARD — tabbed shell mount + tab-switch transitions
   ============================================================ */
@keyframes vip-shell-in {
  0%   { opacity: 0; transform: scale(0.95) translateY(24px); }
  100% { opacity: 1; transform: scale(1) translateY(0); }
}
.vip-shell-in { animation: vip-shell-in 0.4s cubic-bezier(0.16, 1, 0.3, 1) both; }

@keyframes vip-tab-in {
  0%   { opacity: 0; transform: translateY(8px); }
  100% { opacity: 1; transform: translateY(0); }
}
.vip-tab-in { animation: vip-tab-in 0.28s ease-out both; }

/* Claimable daily-bonus CTA — breathes a tier-coloured glow to pull the eye. */
@keyframes vip-pulse-cta {
  0%, 100% { box-shadow: 0 0 16px var(--cta-glow, rgba(255,255,255,0.4)); }
  50%      { box-shadow: 0 0 34px var(--cta-glow, rgba(255,255,255,0.4)); }
}
.vip-pulse-cta { animation: vip-pulse-cta 1.6s ease-in-out infinite; }

/* Tab notification dot — pulses so "something's waiting" reads at a glance. */
@keyframes vip-badge-pulse {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.35); opacity: 0.65; }
}
.vip-badge-pulse { animation: vip-badge-pulse 1.2s ease-in-out infinite; }

.reduced-motion .vip-shell-in,
.reduced-motion .vip-tab-in,
.reduced-motion .vip-pulse-cta,
.reduced-motion .vip-badge-pulse { animation: none !important; }

/* ============================================================
   WELCOME TOUR — overlay entrance (the spotlight, ring and card live
   in the .wt-* section of styles-auth.css)
   ============================================================ */
@keyframes tour-fade-in { from { opacity: 0; } to { opacity: 1; } }
.tour-fade-in { animation: tour-fade-in 0.3s ease-out both; }
.reduced-motion .tour-fade-in { animation: none !important; }

/* ============================================================
   SCREEN TRANSITIONS — cross-fade veil between top-level views
   (login → lobby → slot) so screen swaps never hard-cut.
   ============================================================ */
/* Full-viewport veil rendered only while a transition runs. Sits above
   everything (modals top out at z-[350]). */
.screen-veil {
  position: fixed;
  inset: 0;
  z-index: 500;
  background: #0c0a09;
  pointer-events: none;
}
/* Covering phase — also swallows clicks so nothing can be pressed
   mid-swap (double-taps on a lobby card can't fire twice). */
.screen-veil-out {
  animation: screen-veil-out 0.22s ease-in forwards;
  pointer-events: auto;
}
/* Reveal phase — slightly longer so the new screen eases in. */
.screen-veil-in {
  animation: screen-veil-in 0.45s ease-out forwards;
}
@keyframes screen-veil-out { from { opacity: 0; } to { opacity: 1; } }
@keyframes screen-veil-in  { from { opacity: 1; } to { opacity: 0; } }

/* Gentle rise+fade for a freshly mounted screen root, layered under the
   veil reveal so the new view settles instead of popping. */
@keyframes screen-enter {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}
/* No fill-mode: once the animation ends the element must fall back to its
   plain (untransformed) style. Keeping the `to` transform applied forever
   (via `forwards`/`both`) leaves a non-`none` computed transform on this
   screen root — which makes it the containing block for every
   `position: fixed` descendant (toasts, the idle attract-hint, etc.), so
   they anchor to this tall content box instead of the real viewport. */
.screen-enter { animation: screen-enter 0.45s ease-out; }

/* ============================================================
   MODAL EXIT — applied by <ModalFade> to a closing modal's root
   so overlays fade away instead of vanishing on unmount.
   ============================================================ */
@keyframes modal-exit {
  from { opacity: 1; }
  to   { opacity: 0; }
}
.modal-closing {
  animation: modal-exit 0.18s ease-in forwards !important;
  pointer-events: none !important;
}

.reduced-motion .screen-veil,
.reduced-motion .screen-enter { animation-duration: 0.01s !important; }
.reduced-motion .modal-closing { animation-duration: 0.01s !important; }

/* ============================================================
   WIN-ANIMATION SMOOTHING — graceful exits for win overlays
   ============================================================ */
/* Celebratory win popup dismiss: ease the amount + dark backdrop away
   (fade + gentle sink/shrink) instead of unmounting in a single frame. */
@keyframes win-dismiss {
  from { opacity: 1; transform: scale(1); filter: blur(0); }
  to   { opacity: 0; transform: scale(0.88) translateY(8px); filter: blur(2px); }
}
.win-dismiss {
  animation: win-dismiss 0.34s cubic-bezier(0.4, 0, 0.6, 1) forwards !important;
  pointer-events: none !important;
}

/* Per-line win chip strip dismiss: quick collapse+fade so the row of
   payout pills recedes rather than blinking out. */
@keyframes win-strip-dismiss {
  from { opacity: 1; transform: translateY(0); }
  to   { opacity: 0; transform: translateY(6px); }
}
.win-strip-dismiss {
  animation: win-strip-dismiss 0.28s ease-in forwards !important;
  pointer-events: none !important;
}

/* ============================================================
   TAB CROSS-FADE — content panels that swap on a tab change
   ============================================================ */
@keyframes tab-fade-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: translateY(0); }
}
.tab-fade-in { animation: tab-fade-in 0.28s ease-out both; }

.reduced-motion .win-dismiss,
.reduced-motion .win-strip-dismiss,
.reduced-motion .tab-fade-in { animation-duration: 0.01s !important; }

/* Big-win ceremony (BIG/MEGA/SUPER/ULTRA WIN) dismiss — the celebration
   used to blink out the instant you tapped or it timed out. Ease it away
   with a fade + slight zoom so the moment lands softly. */
@keyframes ceremony-dismiss {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(1.06); }
}
.ceremony-dismiss {
  animation: ceremony-dismiss 0.32s cubic-bezier(0.4, 0, 0.6, 1) forwards !important;
  pointer-events: none !important;
}
.reduced-motion .ceremony-dismiss { animation-duration: 0.01s !important; }

/* ============================================================
   CELEBRATIONS — big win / jackpot / respin (ui/Overlays.js)
   Compositor-only motion: transform + opacity, finite where it can be.
   ============================================================ */
/* Big win light ring: a static glowing frame; the moving highlight is a conic
   gradient ROTATED by transform inside a fixed ring mask, so the compositor
   spins it and nothing repaints. Sized by container units so the rotating
   square always covers the frame's diagonal. */
.bw-ring {
  position: absolute; inset: 0; border-radius: 24px; pointer-events: none; z-index: 0;
  border: 1.5px solid rgba(var(--bw-rgb, 255, 255, 255), 0.5);
  box-shadow: 0 0 22px rgba(var(--bw-rgb, 255, 255, 255), 0.35), inset 0 0 26px rgba(var(--bw-rgb, 255, 255, 255), 0.14);
}
.bw-ring-spin {
  position: absolute; inset: -1.5px; border-radius: inherit; padding: 3px; overflow: hidden;
  container-type: size;
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.bw-ring-spin::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: calc(100cqw + 100cqh); height: calc(100cqw + 100cqh);
  margin: calc((100cqw + 100cqh) / -2) 0 0 calc((100cqw + 100cqh) / -2);
  background: conic-gradient(from 0deg, transparent 0deg, var(--bw-c, #fff) 55deg, #fff 85deg, var(--bw-c, #fff) 115deg,
    transparent 175deg, var(--bw-c, #fff) 235deg, #fff 265deg, var(--bw-c, #fff) 295deg, transparent 355deg);
  animation: bw-ring-spin 2.4s linear infinite;
  will-change: transform;
}
@keyframes bw-ring-spin { to { transform: rotate(360deg); } }
/* Jackpot ambient light: breathes four times while the capsule lands, then
   rests — the screen may wait on Collect indefinitely. */
.jpc-ambient { opacity: 0.55; animation: jpc-ambient 3s ease-in-out 4; }
@keyframes jpc-ambient {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 0.85; }
}
/* A jackpot screen still waiting on Collect after its shower has run out
   rests: every loop inside it (skin motes, the payload's breathing parts)
   holds its pose instead of animating on. */
.jpc-rest *, .jpc-rest *::before, .jpc-rest *::after { animation-play-state: paused !important; }
/* Quiet one-shot fade for "tap to skip / continue" hints. */
.ov-hint-in { animation: ov-hint-in 0.5s ease-out 0.9s backwards; }
@keyframes ov-hint-in { from { opacity: 0; } to { opacity: 1; } }
.reduced-motion .animate-big-win-text,
.reduced-motion .bw-ring-spin::before,
.performance-mode .bw-ring-spin::before,
.reduced-motion .jpc-ambient,
.performance-mode .jpc-ambient,
.reduced-motion .ov-hint-in { animation: none !important; }

/* Dragon Dynasty · Lantern Festival (slots/dragon-dynasty.ui.js): lanterns
   rest still; only the one under the pointer sways (they used to bob
   forever, a dozen filtered layers at once). */
@media (hover: hover) {
  .ddb-lantern-btn:hover:not(:disabled) .ddb-lantern { animation: ddb-sway 2.4s ease-in-out infinite; }
}
@keyframes ddb-sway {
  0%, 100% { transform: translateY(0) rotate(-3deg); }
  50%      { transform: translateY(-4px) rotate(3deg); }
}
.reduced-motion .ddb-lantern { animation: none !important; }

/* ============================================================
   CARD PACKS & CARD COLLECTION (ui/CardsUI.js, ui/CardArt.js)
   The pack-opening ceremony: float → shake → burst → deal →
   4-click reveal (clicks 1–3 can upgrade rarity, click 4 flips).
   ============================================================ */

/* Foil sheen that sweeps across epic/legendary/wild card faces.
   no-repeat is load-bearing: with the default repeat, the oversized gradient
   TILES and the sweep band gets sliced at tile seams — hard-edged bright
   rectangles ("boxes") drifting across the card. */
/* The band is a skewed ::before that TRANSLATES across the card (compositor
   only) — it used to scroll background-position, which repaints the whole
   face every frame. The card's own overflow:hidden clips it. */
.card-foil {
  position: absolute; inset: 0; z-index: 5; overflow: hidden;
}
.card-foil::before {
  content: ''; position: absolute; top: -20%; bottom: -20%; left: 0; width: 55%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.28) 45%, rgba(255,255,255,0.10) 60%, transparent);
  transform: translateX(-120%) skewX(-22deg);
  animation: card-foil-sweep 3s ease-in-out infinite;
}
@keyframes card-foil-sweep {
  0%   { transform: translateX(-120%) skewX(-22deg); }
  55%  { transform: translateX(240%) skewX(-22deg); }
  100% { transform: translateX(240%) skewX(-22deg); }
}
/* THE ALBUM GRID HOLDS STILL (ui/CardsUI.js .cards-tile). Up to 360 cards,
   dozens of them epic/legendary: every foil, ray wheel, floating emblem and
   plate shine used to loop at once. A tile comes alive only while it is
   hovered or keyboard-focused; the inspector shows the full treatment. */
.cards-tile:not(:hover):not(:focus-visible) .card-foil { display: none; }
.cards-tile:not(:hover):not(:focus-visible) .lg-ray-spin,
.cards-tile:not(:hover):not(:focus-visible) .lg-art,
.cards-tile:not(:hover):not(:focus-visible) .lg-rarity { animation: none; }
/* Twenty decks of SVG cards: a deck scrolled out of view skips style, layout
   and paint entirely. The intrinsic size is one deck's typical height, and
   `auto` remembers the real one once it has been seen, so the scrollbar and
   the "Show me" jump stay steady. */
.cards-album-deck { content-visibility: auto; contain-intrinsic-size: auto 290px; }

/* ── THE RARITY PLATE — set into the card's border, not laid on top of it ────
   The rarity flag used to be a plain rectangle floating a few pixels inside the
   top-left corner with only its BOTTOM corners rounded. So its two straight
   edges ran across the card's own rounded corner — a hard right angle inside a
   soft one, with a sliver of card showing through the gap — and on a Legendary
   it landed square on the frame's mitred corner-piece.

   It is now a corner PLATE. Flush to the border on both of those sides, its
   outer corner cut to the SAME radius the card is cut to (rounded-xl 12px less
   the 2px border = 10px), chamfered back into the card on the inner corner, lit
   along its top edge and shadowed underneath so it reads as a machined part of
   the border rather than a sticker sitting on it. A hairline in the rarity's own
   colour runs on from the plate's tail along the top edge toward the deck mark,
   which ties the whole top edge into one piece.

   Every rarity, every size: CardFace, WildCardFace (ui/CardArt.js) and the
   Tower of Fortune's card (ui/TowerFortune.js) all wear it.               */
.card-rarity-tab {
  position: relative;
  border-radius: 10px 0 7px 0;
  background-image: linear-gradient(158deg, rgba(255,255,255,0.5), rgba(255,255,255,0) 55%, rgba(0,0,0,0.2));
  box-shadow:
    inset 0 1px 0 rgba(255,255,255,0.6),  /* the lit top edge */
    0 1px 0 rgba(0,0,0,0.5),              /* the plate's own shadow on the card */
    1px 0 0 rgba(0,0,0,0.32);
}
/* the plate's rule, carrying on along the top border to the deck mark */
.card-rarity-tab::after {
  content: '';
  position: absolute; left: 100%; top: 0; width: 360%; height: 1.5px;
  background: linear-gradient(90deg, var(--tab, #fbbf24), rgba(0,0,0,0) 88%);
  opacity: 0.55;
}
/* the grid thumbnail's plate is ~10px tall — the full 10px radius would swallow
   it whole, so it steps down a little */
.card-rarity-tab-sm { border-radius: 8px 0 6px 0; }
.card-rarity-pip { width: 22px; height: 7px; }

/* the deck mark answers the plate in the opposite corner, cut the same way */
.card-deck-mark {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 0 9px 0 7px;
  background-image: linear-gradient(200deg, rgba(0,0,0,0.6), rgba(0,0,0,0.22));
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.15), inset -1px 0 0 rgba(255,255,255,0.08);
}

/* Rainbow hue-cycling (Wild pack pouch + its overlay rays) */
.pack-rainbow { animation: pack-hue-cycle 5s linear infinite; }
@keyframes pack-hue-cycle { to { filter: hue-rotate(360deg); } }

/* Rainbow border ring (Wild pack / Exotic chest inventory tiles). STATIC: the
   old scrolling gradient repainted the whole tile every frame, forever. */
.rainbow-ring {
  border: 2.5px solid transparent !important;
  background:
    linear-gradient(#151210, #151210) padding-box,
    linear-gradient(120deg, #ef4444, #f97316, #facc15, #4ade80, #38bdf8, #a855f7, #ef4444) border-box !important;
  background-size: 100% 100%, 100% 100% !important;
}

/* Wild Card face: the rainbow border-box layer is painted inline by
   WildCardFace (tier tint, dark base, rainbow). Static ring + a soft pink
   glow — it used to scroll the gradient and cycle the glow (background-
   position + box-shadow, both full repaints) on every Wild on screen. */
.wild-rainbow-border {
  box-shadow: 0 0 12px rgba(244,114,182,0.45), inset 0 1px 0 rgba(255,255,255,0.08);
}

/* Shared notification dot — marks any menu entry with something claimable
   RIGHT NOW (never decorative, so it stays trustworthy). */
.notif-dot {
  display: inline-block;
  width: 8px; height: 8px; border-radius: 9999px;
  background: #fbbf24;
  box-shadow: 0 0 8px rgba(251,191,36,0.9);
  /* A few pulses to draw the eye, then a steady lit dot (no idle loop). */
  animation: notif-dot-pulse 1.6s ease-in-out 4;
}
@keyframes notif-dot-pulse {
  0%, 100% { transform: scale(1);    opacity: 1; }
  50%       { transform: scale(0.72); opacity: 0.65; }
}

/* Upgrade-chance pip igniting (reveal clicks 1–3) */
.pip-burn { animation: pip-burn 0.35s cubic-bezier(0.3, 1.6, 0.5, 1); }
@keyframes pip-burn {
  0%   { transform: scale(1); }
  45%  { transform: scale(2); }
  100% { transform: scale(1); }
}

/* Soft pulse around a face-down card in the NEXT rarity's color — "an
   upgrade is still possible". --aura-rgb is set inline per card. */
/* The glow is painted once at full strength and the loop breathes its
   OPACITY (compositor-only) — it used to animate box-shadow on every
   face-down card, a repaint per card per frame. */
.back-next-aura {
  border-radius: 1rem;
  box-shadow: 0 0 22px 5px rgba(var(--aura-rgb), 0.42);
  animation: back-next-aura 1.7s ease-in-out infinite;
}
@keyframes back-next-aura {
  0%, 100% { opacity: 0.3; }
  50%       { opacity: 1; }
}

/* Shop purchase sparkles (spawnSparkles) — a ✦ burst from the click point */
.buy-sparkle {
  position: absolute; pointer-events: none; z-index: 30;
  color: #fde68a; text-shadow: 0 0 6px rgba(251, 191, 36, 0.9);
  animation: buy-sparkle 0.7s cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
}
@keyframes buy-sparkle {
  0%   { transform: translate(-50%, -50%) scale(0.4) rotate(0deg); opacity: 1; }
  100% { transform: translate(calc(-50% + var(--sx, 0px)), calc(-50% + var(--sy, 0px))) scale(1.15) rotate(120deg); opacity: 0; }
}
.reduced-motion .buy-sparkle { display: none; }

/* Card Inspector: the big card + info panel settle in with a soft zoom */
.card-inspect-in { animation: card-inspect-in 0.28s cubic-bezier(0.2, 1.1, 0.4, 1) both; }
@keyframes card-inspect-in {
  from { transform: scale(0.82); opacity: 0; }
  to   { transform: scale(1);    opacity: 1; }
}

/* Pack rip: an intensifying wobble … */
.pack-shaking { animation: pack-shake 0.55s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
@keyframes pack-shake {
  0%   { transform: rotate(0deg) scale(1); }
  15%  { transform: rotate(-4deg) scale(1.02) translateX(-2px); }
  30%  { transform: rotate(4deg)  scale(1.04) translateX(2px); }
  45%  { transform: rotate(-6deg) scale(1.06) translateX(-4px); }
  60%  { transform: rotate(6deg)  scale(1.08) translateX(4px); }
  75%  { transform: rotate(-8deg) scale(1.12) translateX(-5px); }
  90%  { transform: rotate(8deg)  scale(1.16) translateX(5px); }
  100% { transform: rotate(0deg)  scale(1.2); }
}
/* … then it pops */
.pack-bursting { animation: pack-burst 0.38s cubic-bezier(0.5, 0, 0.9, 0.4) forwards; }
/* transform + opacity only (it used to ramp filter: brightness, re-filtering
   the pouch every frame of the burst — the white drop-flash sells the heat) */
@keyframes pack-burst {
  0%   { transform: scale(1.2); opacity: 1; }
  40%  { transform: scale(1.45); opacity: 1; }
  100% { transform: scale(2.1); opacity: 0; }
}
.pack-burst-ring {
  position: absolute; left: 50%; top: 50%;
  width: 60px; height: 60px; margin: -30px 0 0 -30px;
  border: 4px solid; border-radius: 9999px; pointer-events: none;
  animation: pack-burst-ring 0.55s ease-out forwards;
}
@keyframes pack-burst-ring {
  from { transform: scale(0.4); opacity: 1; }
  to   { transform: scale(9); opacity: 0; }
}

/* Cards dealt from the burst — overshoot fly-in */
.card-deal-in { animation: card-deal-in 0.55s cubic-bezier(0.18, 1.4, 0.4, 1) both; }
@keyframes card-deal-in {
  from { opacity: 0; transform: translateY(90px) rotate(10deg) scale(0.3); }
  60%  { opacity: 1; }
  to   { opacity: 1; transform: translateY(0) rotate(0deg) scale(1); }
}

/* 3D flip shell */
.flip-card { position: relative; aspect-ratio: 3 / 4; perspective: 900px; }
.flip-inner {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform 0.65s cubic-bezier(0.3, 1.4, 0.45, 1);
}
.flip-inner.flipped { transform: rotateY(180deg); }
.flip-front, .flip-back {
  position: absolute; inset: 0;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
.flip-back { transform: rotateY(180deg); }

/* Upgrade feedback: card pulse + expanding ring + label pop */
.card-upgrade-pop { animation: card-upgrade-pop 0.45s cubic-bezier(0.2, 1.6, 0.4, 1); }
@keyframes card-upgrade-pop {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.16) rotate(-2deg); }
  70%  { transform: scale(0.97); }
  100% { transform: scale(1); }
}
.upgrade-burst {
  position: absolute; inset: -4px; z-index: 20;
  border: 3px solid; border-radius: 0.85rem; pointer-events: none;
  animation: upgrade-burst 0.55s ease-out forwards;
}
@keyframes upgrade-burst {
  from { transform: scale(1); opacity: 1; }
  to   { transform: scale(1.8); opacity: 0; }
}

/* NEW / dupe badge pop after a flip */
.reveal-badge-pop { animation: reveal-badge-pop 0.4s cubic-bezier(0.2, 1.6, 0.4, 1) both; animation-delay: 0.3s; }
@keyframes reveal-badge-pop {
  from { opacity: 0; transform: scale(0.3) translateY(6px); }
  to   { opacity: 1; transform: scale(1) translateY(0); }
}

/* Whole-screen gold flash when a LEGENDARY flips */
/* The gold bloom that washes the room on a legendary reveal. Full-bleed, but it
   blooms FROM the revealed card (--leg-x/--leg-y, measured in ui/CardsUI.js) —
   hard-coded at 50% 45% it read as a gold circle pulsing out of the middle of
   the screen no matter which card had just turned over. */
.legendary-flash {
  position: absolute; inset: 0;
  background: radial-gradient(60% 60% at var(--leg-x, 50%) var(--leg-y, 45%), rgba(253,230,138,0.85), rgba(251,191,36,0.35) 55%, transparent 80%);
  animation: legendary-flash 0.9s ease-out forwards;
}
@keyframes legendary-flash {
  0%   { opacity: 0; }
  15%  { opacity: 1; }
  100% { opacity: 0; }
}

/* Reduced motion: freeze the ceremony down to its essentials */
.reduced-motion .card-foil,
.reduced-motion .card-foil::before,
.reduced-motion .pack-rainbow,
.reduced-motion .rainbow-ring,
.reduced-motion .wild-rainbow-border,
.reduced-motion .pack-shaking,
.reduced-motion .upgrade-burst,
.reduced-motion .notif-dot,
.reduced-motion .back-next-aura,
.reduced-motion .pack-burst-ring { animation: none !important; }
.reduced-motion .card-deal-in,
.reduced-motion .card-upgrade-pop,
.reduced-motion .reveal-badge-pop,
.reduced-motion .legendary-flash,
.reduced-motion .pip-burn,
.reduced-motion .card-inspect-in,
.reduced-motion .pack-bursting { animation-duration: 0.01s !important; }
.reduced-motion .flip-inner { transition-duration: 0.1s; }

/* --- Pack rip 2.0: rapid clicks tear the crimped top clean off --------- */
/* A proper rattle per click (re-keyed by the overlay so it replays): quick
   side-to-side + rotation, scaled by --jolt-amt (1 → ~2 as the rip meter
   fills) so the shake gets more frantic the closer the pack is to tearing. */
.pack-click-jolt { animation: pack-click-jolt 0.18s cubic-bezier(0.3, 1.6, 0.5, 1); }
@keyframes pack-click-jolt {
  0%   { transform: translateX(0)                                scale(1)                                  rotate(0deg); }
  15%  { transform: translateX(calc(-5px * var(--jolt-amt, 1)))   scale(calc(1 + 0.05 * var(--jolt-amt, 1))) rotate(calc(-3deg * var(--jolt-amt, 1))); }
  35%  { transform: translateX(calc(4px * var(--jolt-amt, 1)))    scale(calc(1 + 0.08 * var(--jolt-amt, 1))) rotate(calc(2.5deg * var(--jolt-amt, 1))); }
  55%  { transform: translateX(calc(-3px * var(--jolt-amt, 1)))   scale(calc(1 + 0.03 * var(--jolt-amt, 1))) rotate(calc(-1.5deg * var(--jolt-amt, 1))); }
  75%  { transform: translateX(calc(2px * var(--jolt-amt, 1)))    scale(0.98)                                rotate(calc(1deg * var(--jolt-amt, 1))); }
  100% { transform: translateX(0)                                scale(1)                                  rotate(0deg); }
}
/* The torn top strip flies up and away, spinning */
.pack-top-tear { animation: pack-top-tear 0.7s cubic-bezier(0.2, 0.6, 0.4, 1) forwards; }
@keyframes pack-top-tear {
  0%   { transform: rotate(9deg) translate(6px, -5px); opacity: 1; }
  100% { transform: rotate(140deg) translate(150px, -190px); opacity: 0; }
}
/* The body holds for a beat while the top tears, THEN bursts */
.pack-bursting-delayed { animation: pack-burst 0.4s cubic-bezier(0.5, 0, 0.9, 0.4) 0.28s forwards; }
/* Urgent "keep clicking!" label while the rip meter is live */
.rip-urge { animation: rip-urge 0.35s ease-in-out infinite; }
@keyframes rip-urge {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.08); }
}
/* Pack Wheel pointer nods while the wheel spins */
.wheel-pointer-tick { animation: wheel-pointer-tick 0.22s ease-in-out infinite; transform-origin: 50% 0; }
@keyframes wheel-pointer-tick {
  0%, 100% { transform: rotate(0deg); }
  50%      { transform: rotate(-14deg); }
}
.reduced-motion .pack-click-jolt,
.reduced-motion .rip-urge,
.reduced-motion .wheel-pointer-tick { animation: none !important; }
.reduced-motion .pack-top-tear,
.reduced-motion .pack-bursting-delayed { animation-duration: 0.01s !important; animation-delay: 0s !important; }
/* Foil shrapnel flung out when the pack tears open */
.pack-shrapnel {
  position: absolute; left: 50%; top: 50%;
  width: 9px; height: 9px; border-radius: 2px; pointer-events: none;
  animation: pack-shrapnel 0.7s cubic-bezier(0.2, 0.8, 0.4, 1) forwards;
}
@keyframes pack-shrapnel {
  from { transform: translate(-50%, -50%) rotate(0deg) scale(1); opacity: 1; }
  to   { transform: translate(calc(-50% + var(--sx)), calc(-50% + var(--sy))) rotate(560deg) scale(0.5); opacity: 0; }
}
.reduced-motion .pack-shrapnel { display: none !important; }

/* ============================================================
   CARD-PACK OPENING — "VAULT-DROP LUXE" overhaul
   Escalating rip → inhale → bass-drop tear → landing → per-rarity reveals.
   EVERY loop/particle animates ONLY transform/opacity on a promoted layer
   (will-change + translateZ). All rainbow/gold is a STATIC gradient rotated
   by transform — never animated filter/box-shadow/background-position — so
   the full spectacle runs at 60fps on every device (perf-mode included).
   ============================================================ */

/* --- Rip phase: tension --------------------------------------------------- */
/* Full-content screen-shake, replayed by alternating a/b class (no remount of
   the rip button). Intensity scales with --shake-amt (1 → ~2). */
.pack-shake-a, .pack-shake-b { animation: pack-screen-shake 0.16s ease-out; will-change: transform; }
@keyframes pack-screen-shake {
  0%,100% { transform: translate3d(0,0,0); }
  20% { transform: translate3d(calc(-4px*var(--shake-amt,1)), calc(2px*var(--shake-amt,1)),0); }
  40% { transform: translate3d(calc(5px*var(--shake-amt,1)), calc(-3px*var(--shake-amt,1)),0); }
  60% { transform: translate3d(calc(-3px*var(--shake-amt,1)), calc(2px*var(--shake-amt,1)),0); }
  80% { transform: translate3d(calc(2px*var(--shake-amt,1)), calc(-1px*var(--shake-amt,1)),0); }
}
/* Pouch squash — composes with pack-click-jolt on the parent */
.pack-squash { animation: pack-squash 0.18s cubic-bezier(0.3,1.6,0.5,1); will-change: transform; }
@keyframes pack-squash {
  0% { transform: scale3d(1,1,1); }
  30% { transform: scale3d(calc(1 + 0.12*var(--jolt-amt,1)), calc(1 - 0.10*var(--jolt-amt,1)),1); }
  60% { transform: scale3d(calc(1 - 0.06*var(--jolt-amt,1)), calc(1 + 0.06*var(--jolt-amt,1)),1); }
  100% { transform: scale3d(1,1,1); }
}
/* Darkening tension vignette that closes in as the meter fills */
.pack-rip-tense {
  position:absolute; inset:-40%; pointer-events:none; z-index:5;
  background: radial-gradient(circle at 50% 50%,
    transparent calc(58% - var(--tense,0)*22%), rgba(0,0,0,calc(0.15 + var(--tense,0)*0.45)) 100%);
  animation: pack-rip-tense 0.32s ease-out; will-change: transform, opacity;
}
@keyframes pack-rip-tense { 0%{opacity:0;transform:scale(1.04);} 40%{opacity:1;transform:scale(1);} 100%{opacity:0.85;} }
/* Gilded foil sheen sweeping across the pack (idle shimmer) */
/* Shimmer CONTAINED to the pouch graphic (inset to the PackVisual pouch rect,
   clipped) so it never slides off the card as a stray box. The band is a
   skewed ::before that TRANSLATES across (compositor-only) — it used to
   scroll background-position, a full repaint of the layer every frame. */
.lux-foil-sweep {
  position:absolute; left:12.5%; right:12.5%; top:14.5%; bottom:5%;
  pointer-events:none; border-radius:12px; mix-blend-mode:screen; overflow:hidden;
}
.lux-foil-sweep::before {
  content:''; position:absolute; top:-10%; bottom:-10%; left:0; width:45%;
  background-image: linear-gradient(90deg, transparent, rgba(255,255,255,0.5) 50%, transparent);
  transform: translateX(-130%) skewX(-15deg); opacity:0;
  animation: lux-foil-sweep 3.4s ease-in-out infinite;
}
.lux-foil-rainbow::before {  /* wild: rainbow sheen via STATIC gradient, not filter hue-rotate */
  background-image: linear-gradient(90deg, transparent,
    rgba(239,68,68,.45), rgba(250,204,21,.45), rgba(56,189,248,.45), rgba(168,85,247,.45), transparent);
}
@keyframes lux-foil-sweep {
  0%   { transform: translateX(-130%) skewX(-15deg); opacity:0; }
  15%  { opacity:0.5; }
  85%  { opacity:0.5; }
  100% { transform: translateX(260%) skewX(-15deg); opacity:0; }
}
/* Straining ring that pulses faster as the rip nears the tear */
.lux-strain-ring {
  position:absolute; left:50%; top:42%; width:62%; aspect-ratio:1; border:2px solid; border-radius:9999px;
  transform:translate(-50%,-50%); pointer-events:none;
  animation: lux-strain-pulse 0.9s ease-in-out infinite; will-change: transform, opacity;
}
@keyframes lux-strain-pulse {
  0%,100%{transform:translate(-50%,-50%) scale(1);opacity:0;} 50%{transform:translate(-50%,-50%) scale(1.05);opacity:0.5;}
}

/* --- Tear phase: inhale → drop ------------------------------------------- */
.pack-inhale {
  position:absolute; inset:0; pointer-events:none; z-index:8;
  background: radial-gradient(circle at 50% 42%, rgba(255,255,255,0), rgba(0,0,0,0.6) 90%);
  /* Base opacity:0 is load-bearing — the reduced-motion guard below kills the
     animation, and without a resting state that leaves a permanent dark
     vignette over the ceremony. */
  opacity: 0;
  animation: pack-inhale 0.7s cubic-bezier(0.7,0,0.3,1) forwards; will-change: opacity;
}
@keyframes pack-inhale { 0%{opacity:0;} 30%{opacity:0.9;} 42%{opacity:0;} 100%{opacity:0;} }
/* Godrays: outer SCALES, inner ROTATES (split so each animates one channel) */
.pack-godrays {
  position:absolute; left:50%; top:50%; width:200vmax; height:200vmax; margin:-100vmax 0 0 -100vmax;
  pointer-events:none; z-index:6; opacity:0; transform: translateZ(0) scale(0.2);
  animation: pack-godray-blast 0.9s cubic-bezier(0.15,0.7,0.25,1) 0.26s forwards; will-change: transform, opacity;
}
.pack-godray-spin { position:absolute; inset:0; mix-blend-mode:screen;
  animation: pack-godray-turn 8s linear 0.26s infinite; will-change: transform; }
@keyframes pack-godray-blast { 0%{opacity:0;transform:scale(0.2);} 30%{opacity:0.9;transform:scale(1.15);} 100%{opacity:0.35;transform:scale(1);} }
@keyframes pack-godray-turn { to { transform: rotate(30deg); } }
/* Expanding shockwave rings (also reused for the wild six-colour ripple) */
.pack-shockwave {
  position:absolute; left:50%; top:50%; width:80px; height:80px; margin:-40px 0 0 -40px;
  border:6px solid; border-radius:9999px; pointer-events:none; z-index:7;
  transform: translateZ(0) scale(0.3); opacity:0;
  animation: pack-shockwave 0.6s cubic-bezier(0.1,0.8,0.2,1) 0.26s forwards; will-change: transform, opacity;
}
.pack-shockwave.two { border-width:3px; animation-delay:0.34s; }
@keyframes pack-shockwave { 0%{opacity:0.9;transform:scale(0.3);} 100%{opacity:0;transform:scale(14);} }
/* Confetti cannon — physical particles flung out and down */
.pack-cannon {
  position:absolute; left:50%; top:50%; width:11px; height:15px; border-radius:2px; z-index:9;
  animation: pack-cannon 1.05s cubic-bezier(0.15,0.6,0.5,1) forwards; will-change: transform, opacity; pointer-events:none;
}
@keyframes pack-cannon {
  0%{transform:translate3d(0,0,0) rotate(0deg) scale(1);opacity:1;} 15%{opacity:1;}
  100%{transform:translate3d(var(--vx),var(--vy),0) rotate(720deg) scale(0.6);opacity:0;}
}

/* --- Cards phase: land + flip -------------------------------------------- */
/* `backwards`, NOT `both`: card-land-squash's 100% keyframe IS the identity
   transform, so this is a visual no-op — but `both` permanently pins
   `transform` on the per-card wrapper, and animations beat transitions, so
   the .card-bystander hush could never apply. Do not revert to `both`. */
/* No `will-change` here: it creates a stacking context UNCONDITIONALLY (not just
   while animating), and this class sits on every card wrapper for the whole
   ceremony. That trapped .card-hero-pop's z-30 inside the wrapper, so the
   root-level .hero-bar letterbox (z-12) painted OVER the hero card during its
   own hero moment. The land is a one-shot 0.42s — promotion buys ~nothing. */
.card-land { animation: card-land-squash 0.42s cubic-bezier(0.2,0.8,0.3,1.2) backwards; }
@keyframes card-land-squash {
  0%{transform:translateY(-40px) scale3d(0.8,1.15,1);opacity:0;}
  60%{transform:translateY(0) scale3d(1.12,0.85,1);opacity:1;}
  80%{transform:translateY(0) scale3d(0.96,1.05,1);}
  100%{transform:translateY(0) scale3d(1,1,1);}
}
/* A punchy 3D flip that overshoots toward the viewer, then settles flipped */
.flip-inner.flipped.flip-punch { animation: flip-punch 0.5s cubic-bezier(0.4,0,0.2,1) forwards; }
@keyframes flip-punch {
  0%{transform:rotateY(0) translateZ(0) scale(1);}
  50%{transform:rotateY(90deg) translateZ(40px) scale(1.08);}
  100%{transform:rotateY(180deg) translateZ(0) scale(1);}
}
/* Light sweep across the freshly-flipped face */
.lux-flip-flash {
  position:absolute; inset:0; pointer-events:none; border-radius:1rem; mix-blend-mode:screen;
  background: linear-gradient(100deg, transparent 40%, rgba(255,246,204,0.9) 50%, transparent 60%);
  animation: lux-flip-flash 0.5s ease-out forwards; will-change: transform, opacity;
}
@keyframes lux-flip-flash {
  0%{transform:translateX(-100%) skewX(-14deg);opacity:0;} 40%{opacity:0.8;}
  100%{transform:translateX(100%) skewX(-14deg);opacity:0;}
}
/* Hero scale-pop for a legendary/wild card (slow-mo hold) */
.card-hero-pop { animation: card-hero-pop 1.6s cubic-bezier(0.2,0.9,0.2,1); z-index:30; will-change: transform; }
@keyframes card-hero-pop {
  0%{transform:scale(1);} 22%{transform:scale(1.4) translateY(-14px);}
  70%{transform:scale(1.38) translateY(-14px);} 100%{transform:scale(1);}
}
/* Rainbow border CONTAINED to a revealed WILD card's edge. The box is static
   and hugs the card; only the conic-gradient's angle spins (via @property), so
   the rainbow flows around the border without any part sweeping past the card. */
@property --wild-ang { syntax: '<angle>'; inherits: false; initial-value: 0deg; }
.wild-hero-ring::after {
  content:''; position:absolute; inset:-2px; border-radius:16px; z-index:6; pointer-events:none;
  --wild-ang:0deg; padding:3.5px;
  background: conic-gradient(from var(--wild-ang), #ef4444,#f97316,#facc15,#4ade80,#38bdf8,#a855f7,#ef4444);
  -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;
  animation: wild-ring-spin 1.4s linear infinite; will-change: background;
}
@keyframes wild-ring-spin { to { --wild-ang: 360deg; } }

/* --- LEGENDARY hero spectacle (gold) ------------------------------------- */
/* The hero spectacle detonates ON the card that was revealed, not in the middle
   of the screen — --leg-x/--leg-y are measured from the card and set on the
   ceremony overlay (ui/CardsUI.js). They fall back to dead centre. */
.legend-godrays {
  position:absolute; left:var(--leg-x,50%); top:var(--leg-y,50%); width:200vmax; height:200vmax; margin:-100vmax 0 0 -100vmax;
  pointer-events:none; z-index:11; mix-blend-mode:screen;
  background: repeating-conic-gradient(from 0deg, rgba(255,224,138,0.30) 0deg 6deg, transparent 6deg 20deg);
  -webkit-mask-image: radial-gradient(closest-side,#000 20%,transparent 72%);
          mask-image: radial-gradient(closest-side,#000 20%,transparent 72%);
  animation: legend-godray-spin 2.4s ease-out forwards, legend-godray-fade 2.4s ease-out forwards;
  will-change: transform, opacity;
}
/* The negative margin already centres these on (left, top) — the extra
   translate(-50%,-50%) that used to sit in the keyframes shifted them a further
   HALF THEIR OWN SIZE up and left, which for a 200vmax ray fan meant its lit
   centre sat entirely off-screen. Rotation/scale only; the box does the
   centring. (The gold chest reuses these and gets the fix too.) */
@keyframes legend-godray-spin { from{transform:rotate(0) scale(0.6);} to{transform:rotate(38deg) scale(1.15);} }
@keyframes legend-godray-fade { 0%{opacity:0;} 18%{opacity:0.85;} 100%{opacity:0;} }
.legend-shock {
  position:absolute; left:var(--leg-x,50%); top:var(--leg-y,50%); width:100px; height:100px; margin:-50px 0 0 -50px;
  border:6px solid rgba(251,191,36,0.9); border-radius:9999px; pointer-events:none;
  animation: legend-shock 1.1s cubic-bezier(0.1,0.6,0.2,1) forwards; will-change: transform, opacity;
}
@keyframes legend-shock { 0%{transform:scale(0.3);opacity:1;} 100%{transform:scale(9);opacity:0;} }
/* A second, tighter halo hugging the card itself — the wide godrays alone read
   as a room effect, so the card it is celebrating got no ring of its own. */
.legend-halo {
  position:absolute; left:var(--leg-x,50%); top:var(--leg-y,50%);
  width:230px; height:230px; margin:-115px 0 0 -115px; border-radius:9999px;
  pointer-events:none; mix-blend-mode:screen;
  background: radial-gradient(circle, rgba(255,251,235,0.85) 0%, rgba(251,191,36,0.55) 34%, rgba(251,191,36,0.14) 58%, transparent 72%);
  animation: legend-halo 1.5s cubic-bezier(0.15,0.7,0.25,1) forwards; will-change: transform, opacity;
}
@keyframes legend-halo {
  0%   { transform:scale(0.35); opacity:0; }
  14%  { transform:scale(1.05); opacity:1; }
  45%  { transform:scale(1.25); opacity:0.7; }
  100% { transform:scale(1.75); opacity:0; }
}
.legend-ember {
  position:absolute; left:var(--leg-x,50%); top:var(--leg-y,50%); width:9px; height:9px; border-radius:9999px;
  background: radial-gradient(circle,#fde68a,#fbbf24); opacity:0;
  animation: legend-ember 1.6s ease-out forwards; will-change: transform, opacity;
}
@keyframes legend-ember {
  0%   { opacity:0; transform:translate(calc(var(--sx,0px) - 50%), 40px) scale(0.6); }
  20%  { opacity:1; }
  100% { opacity:0; transform:translate(calc(var(--sx,0px) + var(--ex,0px) - 50%), -190px) scale(1.1); }
}

/* --- WILD hero spectacle (rainbow) --------------------------------------- */
.lux-prism {
  position:absolute; left:50%; top:50%; width:200vmax; height:200vmax; margin:-100vmax 0 0 -100vmax;
  pointer-events:none; z-index:11; mix-blend-mode:screen; opacity:0;
  background: conic-gradient(from 0deg,#ef4444,#f97316,#facc15,#4ade80,#38bdf8,#a855f7,#ef4444);
  -webkit-mask-image: radial-gradient(closest-side,#000 12%,transparent 66%);
          mask-image: radial-gradient(closest-side,#000 12%,transparent 66%);
  animation: lux-prism-spin 2.2s linear forwards, lux-prism-fade 2.2s ease-out forwards; will-change: transform, opacity;
}
@keyframes lux-prism-spin { from{transform:translate(-50%,-50%) rotate(0);} to{transform:translate(-50%,-50%) rotate(-30deg);} }
@keyframes lux-prism-fade { 0%{opacity:0;} 16%{opacity:0.7;} 100%{opacity:0;} }
.lux-prism-chip {
  position:absolute; top:0; width:11px; height:15px; border-radius:2px; pointer-events:none;
  animation: lux-chip-fall 2.3s linear forwards; will-change: transform, opacity;
}
@keyframes lux-chip-fall { 0%{transform:translateY(-14vh) rotate(0);opacity:0;} 10%{opacity:1;} 100%{transform:translateY(112vh) rotate(540deg);opacity:0.9;} }

/* --- CEREMONY BACKDROP: the room a pack / chest opens in ------------------ */
/* Both ceremonies used to play against flat black with one faint ray layer, so
   a Common opened in exactly the same room as an Exotic. This is a tiered
   ambient set: every tier gets a colour-matched veil instead of black, and each
   step up the ladder adds one more thing happening back there —
     0 veil · 1 +aurora drift · 2 +star field · 3 +rising motes · 4 +sweeping beams
   — with the rainbow tiers hue-cycling the whole stack (.pack-rainbow).
   Gradients only, NO filter/blur: these overlays deliberately avoid
   backdrop-blur (every promoted child re-samples it and the seams show), and a
   blurred full-screen layer would cost more than the whole ceremony.
   Colours arrive as --cer-c1 / --cer-c2 / --cer-glow from the calling tier. */
.cer-bg { position:absolute; inset:0; overflow:hidden; pointer-events:none; z-index:0; }

.cer-veil {
  position:absolute; inset:-6%;
  background:
    radial-gradient(125% 80% at 50% -12%, rgba(var(--cer-glow),0.22), transparent 62%),
    radial-gradient(105% 62% at 50% 112%, rgba(var(--cer-glow),0.14), transparent 66%);
  animation: cer-veil-breathe 7s ease-in-out infinite, cer-veil-dolly 24s ease-in-out infinite alternate;
  will-change: opacity, transform;
}
@keyframes cer-veil-breathe { 0%,100%{opacity:0.72;} 50%{opacity:1;} }
/* A slow push-in. Paired with the parallax bands it's what stops the room
   reading as a flat sheet of colour behind the chest. */
@keyframes cer-veil-dolly { from{transform:scale(1);} to{transform:scale(1.1);} }

/* DEPTH BANDS. The star field is split across three of these; each drifts and
   scales at its own rate, so the near band slides visibly against the far one
   and the room gains parallax. Contents inherit the band's motion, which is why
   the stars themselves only ever animate opacity/scale. */
.cer-depth {
  position:absolute; inset:-10%; will-change: transform;
  animation: cer-depth-drift var(--cer-dur,70s) ease-in-out infinite alternate;
}
@keyframes cer-depth-drift {
  from { transform: translate3d(var(--cer-x0,0),var(--cer-y0,0),0) scale(var(--cer-s0,1)); }
  to   { transform: translate3d(var(--cer-x1,0),var(--cer-y1,0),0) scale(var(--cer-s1,1)); }
}

/* Perspective floor receding to a horizon — the single strongest depth cue
   here, and the reason the room has a FAR rather than just a background. The
   grid scrolls toward the viewer; the mask fades it out at the horizon line. */
.cer-floor {
  position:absolute; left:-60%; right:-60%; bottom:-8%; height:58vh; overflow:hidden;
  transform: perspective(320px) rotateX(66deg); transform-origin: 50% 100%;
  -webkit-mask-image: linear-gradient(to top, #000 2%, transparent 78%);
          mask-image: linear-gradient(to top, #000 2%, transparent 78%);
  opacity:0.75;
}
/* The grid is a transform-animated child, not an animated background-position
   on the parent: shifting the background repaints the whole layer every frame,
   translating a composited child does not. It overhangs by one cell top and
   bottom so the loop point never shows. */
.cer-floor::before {
  content:''; position:absolute; left:0; right:0; top:-86px; bottom:-86px;
  background-image:
    repeating-linear-gradient(to right, rgba(var(--cer-glow),0.34) 0 1px, transparent 1px 86px),
    repeating-linear-gradient(to bottom, rgba(var(--cer-glow),0.34) 0 1px, transparent 1px 86px);
  animation: cer-floor-run var(--cer-dur,7s) linear infinite; will-change: transform;
}
@keyframes cer-floor-run { to { transform: translate3d(0,86px,0); } }

/* Near-field bokeh — big, soft, out-of-focus orbs that pass IN FRONT of the
   star bands. Foreground blur is faked with a wide gradient falloff (no filter). */
/* `closest-side` is load-bearing: with the default farthest-corner sizing the
   gradient is still opaque where the box ends, so the orb reads as a hard-edged
   BALL instead of something out of focus. closest-side lands the transparent
   stop exactly on the (square) box edge. */
.cer-orb {
  position:absolute; mix-blend-mode:screen;
  background: radial-gradient(circle closest-side, rgba(255,255,255,0.14), var(--cer-o,rgba(255,255,255,0.10)) 45%, transparent 100%);
  animation: cer-orb-float var(--cer-dur,17s) ease-in-out infinite alternate; will-change: transform;
}
@keyframes cer-orb-float {
  from { transform: translate3d(var(--cer-x0,0),var(--cer-y0,0),0) scale(var(--cer-s0,1)); }
  to   { transform: translate3d(var(--cer-x1,0),var(--cer-y1,0),0) scale(var(--cer-s1,1.14)); }
}

/* Big soft colour masses sliding past each other behind everything. */
.cer-aurora {
  position:absolute; width:76vmax; height:76vmax; border-radius:50%;
  mix-blend-mode:screen; opacity:0.55;
  background: radial-gradient(circle at 50% 50%, var(--cer-a) 0%, transparent 58%);
  animation: cer-aurora-drift var(--cer-dur,26s) ease-in-out infinite alternate;
  will-change: transform;
}
@keyframes cer-aurora-drift {
  from { transform: translate3d(var(--cer-x0,0),var(--cer-y0,0),0) scale(0.85); }
  to   { transform: translate3d(var(--cer-x1,0),var(--cer-y1,0),0) scale(1.15); }
}

/* Size and brightness come from the band, so a "far" star is genuinely smaller
   and dimmer than a near one rather than being the same dot moved around. */
.cer-star {
  position:absolute; width:var(--cer-sz,2px); height:var(--cer-sz,2px);
  border-radius:9999px; background:#fff;
  box-shadow:0 0 6px rgba(255,255,255,0.75);
  animation: cer-twinkle var(--cer-dur,4s) ease-in-out infinite; will-change: opacity, transform;
}
@keyframes cer-twinkle { 0%,100%{opacity:0.10;transform:scale(0.7);} 50%{opacity:0.85;transform:scale(1.2);} }

/* Embers of the tier colour drifting up from the floor. Negative delays start
   them mid-flight, so the room is already alive on the first frame. */
.cer-mote {
  position:absolute; bottom:-4%; width:5px; height:5px; border-radius:9999px;
  background: radial-gradient(circle, #fff, var(--cer-c1)); opacity:0;
  animation: cer-mote-rise var(--cer-dur,9s) linear infinite; will-change: transform, opacity;
}
@keyframes cer-mote-rise {
  0%{opacity:0;transform:translate3d(0,0,0) scale(0.55);}
  12%{opacity:0.8;}
  100%{opacity:0;transform:translate3d(var(--cer-dx,0),-108vh,0) scale(1.15);}
}

/* Top-tier only: slow searchlights raking the room. Masked at both edges so
   they read as light in air rather than a hard-edged wedge. */
.cer-beam {
  position:absolute; left:50%; top:-28%; width:26vmax; height:168vh; margin-left:-13vmax;
  mix-blend-mode:screen; opacity:0.55; transform-origin:50% 0;
  background: linear-gradient(to bottom, rgba(var(--cer-glow),0.55), rgba(var(--cer-glow),0.11) 46%, transparent 78%);
  -webkit-mask-image: linear-gradient(to right, transparent, #000 32%, #000 68%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 32%, #000 68%, transparent);
  animation: cer-beam-sweep var(--cer-dur,15s) ease-in-out infinite alternate; will-change: transform;
}
@keyframes cer-beam-sweep {
  from { transform: rotate(var(--cer-a0,-26deg)); }
  to   { transform: rotate(var(--cer-a1,26deg)); }
}

/* --- Reduced-motion (accessibility) opt-in only: tone the ceremony down --- */
.reduced-motion .cer-veil, .reduced-motion .cer-aurora, .reduced-motion .cer-star,
.reduced-motion .cer-beam, .reduced-motion .cer-depth, .reduced-motion .cer-floor::before,
.reduced-motion .cer-orb, .reduced-motion .cer-tint { animation: none !important; }
.reduced-motion .cer-mote { display: none !important; }
.reduced-motion .pack-shake-a, .reduced-motion .pack-shake-b, .reduced-motion .pack-squash,
.reduced-motion .pack-rip-tense, .reduced-motion .lux-foil-sweep, .reduced-motion .lux-foil-sweep::before, .reduced-motion .lux-strain-ring,
.reduced-motion .pack-inhale, .reduced-motion .pack-godrays, .reduced-motion .pack-godray-spin,
.reduced-motion .legend-godrays, .reduced-motion .lux-prism, .reduced-motion .wild-hero-ring::after,
.reduced-motion .flip-inner.flipped.flip-punch { animation: none !important; }
.reduced-motion .pack-shockwave, .reduced-motion .card-land, .reduced-motion .lux-flip-flash,
.reduced-motion .legend-shock, .reduced-motion .card-hero-pop { animation-duration: 0.01s !important; animation-delay: 0s !important; }
.reduced-motion .pack-cannon, .reduced-motion .legend-ember, .reduced-motion .lux-prism-chip { display: none !important; }

/* ============================================================
   BUY-BUTTON SPARKLES — the SAME gold glow-dot burst the Show-Paylines
   button fires (.buy-spark), looping while you hover any "spend money"
   button. <BuySparkles/> can sit anywhere inside the button; the
   DESCENDANT selector (`button:hover .buy-sparkles`, no `>`) lights it
   even when it's nested a few layers down inside a shop tile.
   Two variants:
     • default — gold dots flung radially from centre (the paylines look)
     • --plus  — plus-shaped pips spread over the surface, for the
                 Battle Pass strip; pair with --always to run non-stop.
   ============================================================ */
.buy-sparkles {
  position: absolute; inset: 0; pointer-events: none;
  border-radius: inherit; opacity: 0; transition: opacity 0.15s ease;
  z-index: 3; color: #fbbf24;
}
button:hover .buy-sparkles,
button:focus-visible .buy-sparkles,
.buy-sparkle-host:hover .buy-sparkles { opacity: 1; }
.buy-sparkles.buy-sparkles--always { opacity: 1; }

/* Dot burst — the paylines toggle particle (.buy-spark), but bright and
   COLOR-CODED: the dot is a white core fading to the button's own colour
   (set via `color`/currentColor by <BuySparkles color=…/>), with a colour
   glow, flung radially and looped. Bigger + stronger glow = prominent. */
.buy-sparkles > i {
  position: absolute; top: 50%; left: 50%;
  width: 11px; height: 11px; border-radius: 9999px;
  background: radial-gradient(circle at 50% 50%, #fff 0%, currentColor 62%);
  box-shadow: 0 0 13px currentColor, 0 0 5px #fff;
  opacity: 0; animation: buy-spark-hover 0.95s ease-out infinite;
}
.buy-sparkles > i:nth-child(1) { --a: 18deg;  animation-delay: 0s; }
.buy-sparkles > i:nth-child(2) { --a: 74deg;  animation-delay: 0.13s; }
.buy-sparkles > i:nth-child(3) { --a: 130deg; animation-delay: 0.27s; }
.buy-sparkles > i:nth-child(4) { --a: 200deg; animation-delay: 0.42s; }
.buy-sparkles > i:nth-child(5) { --a: 262deg; animation-delay: 0.57s; }
.buy-sparkles > i:nth-child(6) { --a: 322deg; animation-delay: 0.72s; }
@keyframes buy-spark-hover {
  0%   { opacity: 0; transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translateY(0) scale(0.6); }
  20%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -50%) rotate(var(--a, 0deg)) translateY(-32px) scale(0.35); }
}

/* Plus variant (Battle Pass unlock strip): bright plus-shaped pips in the
   button's colour, spread across the surface, twinkling in place. */
.buy-sparkles--plus > i {
  top: auto; left: auto; width: 15px; height: 15px; border-radius: 0;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 2.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 2.5px 100% no-repeat;
  box-shadow: none; filter: drop-shadow(0 0 5px currentColor) drop-shadow(0 0 2px #fff);
  animation: buy-plus-twinkle 1.35s ease-in-out infinite;
}
.buy-sparkles--plus > i:nth-child(1) { left: 8%;  top: 55%; animation-delay: 0s; }
.buy-sparkles--plus > i:nth-child(2) { left: 24%; top: 24%; animation-delay: 0.2s; }
.buy-sparkles--plus > i:nth-child(3) { left: 44%; top: 66%; animation-delay: 0.4s; }
.buy-sparkles--plus > i:nth-child(4) { left: 62%; top: 28%; animation-delay: 0.6s; }
.buy-sparkles--plus > i:nth-child(5) { left: 80%; top: 60%; animation-delay: 0.8s; }
.buy-sparkles--plus > i:nth-child(6) { left: 92%; top: 34%; animation-delay: 1s; }
@keyframes buy-plus-twinkle {
  0%, 100% { opacity: 0; transform: translate(-50%, -50%) scale(0.5) rotate(0deg); }
  45%      { opacity: 1; transform: translate(-50%, -50%) scale(1.05) rotate(45deg); }
  75%      { opacity: 0.6; transform: translate(-50%, -50%) scale(0.85) rotate(90deg); }
}
.reduced-motion .buy-sparkles > i { animation: none !important; }
.reduced-motion .buy-sparkles { opacity: 0 !important; }

/* ============================================================
   BONUS CHOICE SCREEN — per-slot decorated "pick your path"
   (component: ui/BonusChoice.js). Colours are threaded in via the
   --cc / --cc-rgb custom props each card sets inline.
   ============================================================ */
.bonus-choice-card {
  background: linear-gradient(180deg, rgba(28, 25, 23, 0.92), rgba(12, 10, 9, 0.96));
  border-width: 1.5px;
  border-style: solid;
}
@media (hover: hover) {
  .bonus-choice-card:hover {
    transform: translateY(-4px) scale(1.03);
    border-color: var(--cc) !important;
    box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 30px rgba(var(--cc-rgb), 0.3);
  }
}
/* The machine's motif behind the choice: ONE layer drifting slowly (the
   compositor moves it; the icons inside never animate on their own). */
.bxc-field { animation: bxc-field-drift 16s ease-in-out infinite alternate; will-change: transform; }
@keyframes bxc-field-drift {
  from { transform: translateY(2.5%); }
  to   { transform: translateY(-2.5%); }
}
.reduced-motion .bxc-field,
.performance-mode .bxc-field { animation: none !important; }
/* Drifting themed motif that floats up behind the card. */
.bonus-float {
  will-change: transform, opacity;
  animation-name: bonus-float-up;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}
@keyframes bonus-float-up {
  0%   { transform: translateY(0) translateX(0) rotate(0deg); opacity: 0; }
  12%  { opacity: 0.18; }
  88%  { opacity: 0.18; }
  100% { transform: translateY(-560px) translateX(var(--drift, 0px)) rotate(22deg); opacity: 0; }
}
/* Gentle bob on the title glyphs. */
.bonus-glyph-pulse {
  display: inline-block;
  animation: bonus-glyph-pulse 2.4s ease-in-out infinite;
}
@keyframes bonus-glyph-pulse {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-4px) scale(1.08); }
}
.reduced-motion .bonus-float { animation: none !important; opacity: 0.12 !important; }
.reduced-motion .bonus-glyph-pulse { animation: none !important; }

/* ============================================================
   BONUS CLICK-REVEAL ANIMATIONS (Build 33)
   Themed ~1s "anticipation → reveal" beats added to every
   instant-reveal bonus so a pick/action builds suspense before
   its outcome shows. Each bonus's keyframes are uniquely prefixed.
   The pacing is JS-timed (setTimeout), so reduced-motion users
   still get the slower reveal without the motion.
   ============================================================ */

/* --- King of the Jungle · Jungle Fire Jackpot pick (torch catching) --- */
@keyframes jungle-fire-roar {
  0%   { transform: scale(0.9);  box-shadow: 0 0 6px rgba(249,115,22,0.4); }
  35%  { transform: scale(1.14); box-shadow: 0 0 26px rgba(251,146,60,0.9); }
  70%  { transform: scale(1.02); box-shadow: 0 0 18px rgba(234,88,12,0.75); }
  100% { transform: scale(1.06); box-shadow: 0 0 22px rgba(249,115,22,0.75); }
}
@keyframes jungle-fire-flicker {
  0%   { transform: translateY(0) scale(1);       filter: brightness(1)    hue-rotate(0deg); }
  50%  { transform: translateY(-2px) scale(1.18); filter: brightness(1.6)  hue-rotate(-8deg); }
  100% { transform: translateY(0) scale(1.05);    filter: brightness(1.25) hue-rotate(4deg); }
}

/* --- Cosmic Drift · Asteroid Crate anticipation --- */
@keyframes cosmic-crate-rattle {
  0%   { transform: translate(0,0) rotate(0deg) scale(1); }
  15%  { transform: translate(-1.5px,1px) rotate(-2deg) scale(1.01); }
  30%  { transform: translate(1.5px,-1px) rotate(2deg) scale(1.02); }
  45%  { transform: translate(-1px,-1.5px) rotate(-1.5deg) scale(1.03); }
  60%  { transform: translate(1px,1.5px) rotate(1.5deg) scale(1.04); }
  75%  { transform: translate(-1.5px,0.5px) rotate(-1deg) scale(1.05); }
  90%  { transform: translate(1px,-0.5px) rotate(1deg) scale(1.055); }
  100% { transform: translate(0,0) rotate(0deg) scale(1.06); }
}
@keyframes cosmic-crate-seam {
  0%   { filter: drop-shadow(0 0 0 rgba(6,182,212,0)); transform: scale(1); }
  50%  { filter: drop-shadow(0 0 8px rgba(6,182,212,0.85)); transform: scale(1.08); }
  80%  { filter: drop-shadow(0 0 14px rgba(34,211,238,1)); transform: scale(1.05); }
  100% { filter: drop-shadow(0 0 22px rgba(34,211,238,1)); transform: scale(1.12); }
}

/* --- Penguin Gold · Ice-Fishing Haul — line drop + bobber dip + yank --- */
@keyframes icefish-hole-tug {
  0%, 100% { transform: scale(0.95) translateY(0); }
  20% { transform: scale(0.95) translateY(1px); }
  35% { transform: scale(0.96) translateY(-1px); }
  50% { transform: scale(0.95) translateY(2px); }
  65% { transform: scale(0.96) translateY(-1px); }
  82% { transform: scale(0.94) translateY(3px); }
}
@keyframes icefish-line-drop {
  0% { transform: translateY(-140%) rotate(-8deg); opacity: 0; }
  30% { transform: translateY(-30%) rotate(0deg); opacity: 1; }
  100% { transform: translateY(-40%) rotate(4deg); opacity: 0.85; }
}
@keyframes icefish-bobber-dip {
  0% { transform: translateY(6px) scale(0.6); opacity: 0; }
  25% { transform: translateY(0) scale(1); opacity: 1; }
  40% { transform: translateY(3px); }
  55% { transform: translateY(-1px); }
  70% { transform: translateY(5px) scale(0.9); }
  85% { transform: translateY(9px) scale(0.7); opacity: 0.7; }
  100% { transform: translateY(14px) scale(0.5); opacity: 0; }
}
@keyframes icefish-splash {
  0%, 60% { transform: scale(0.4); opacity: 0; }
  75% { transform: scale(1.1) translateY(-2px); opacity: 0.9; }
  100% { transform: scale(1.4) translateY(-6px); opacity: 0; }
}
@keyframes icefish-yank {
  0% { transform: translateY(60%) scale(0.5); opacity: 0; }
  55% { transform: translateY(-12%) scale(1.15); opacity: 1; }
  75% { transform: translateY(4%) scale(0.95); }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}

/* --- Lava Loot · Volcano Climb — strain for the next hold --- */
@keyframes climb-strain {
  0%, 100% { transform: scale(1.10) translateX(0) rotate(0deg); box-shadow: 0 0 18px rgba(249,115,22,0.85); }
  25% { transform: scale(1.13) translateX(-1px) rotate(-1.5deg); box-shadow: 0 0 27px rgba(239,68,68,0.95); }
  50% { transform: scale(1.11) translateX(1px) rotate(1deg); box-shadow: 0 0 22px rgba(249,115,22,0.9); }
  75% { transform: scale(1.13) translateX(1px) rotate(1.5deg); box-shadow: 0 0 29px rgba(239,68,68,1); }
}
@keyframes climb-sparks {
  0% { opacity: 0; transform: translate(-50%, 0) scale(0.5); }
  30% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -150%) scale(1.15); }
}
@keyframes climb-reach-in {
  0% { opacity: 0; transform: translateY(6px); }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes climb-strain-bar {
  0% { width: 0%; }
  100% { width: 100%; }
}

/* --- Diamond Casino Heist · Vault Cracker — dial whir --- */
@keyframes cracker-vault-reel {
  0%   { transform: translateY(0);         filter: blur(0.4px); }
  100% { transform: translateY(-85.714%);  filter: blur(1.1px); }
}
@keyframes cracker-vault-glow {
  0%, 100% { box-shadow: 0 0 10px rgba(34,211,238,0.40); border-color: rgba(34,211,238,0.65); }
  50%      { box-shadow: 0 0 26px rgba(34,211,238,0.90); border-color: rgba(103,232,249,1); }
}
.cracker-reel-spin  { animation: cracker-vault-reel 0.14s linear infinite; will-change: transform; }
.cracker-dial-spinning { animation: cracker-vault-glow 0.8s ease-in-out infinite; }

/* --- Dead Man's Booty · Cannon Broadside — incoming shell + fog churn --- */
@keyframes broadside-shell-incoming {
  0%   { transform: translate(-55%, -145%) scale(0.35) rotate(-42deg); opacity: 0; }
  30%  { opacity: 1; }
  70%  { transform: translate(0, 0) scale(1.08) rotate(0deg); opacity: 1; }
  82%  { transform: translate(0, 7%) scale(0.9) rotate(5deg); }
  100% { transform: translate(0, 0) scale(1) rotate(0deg); opacity: 1; }
}
@keyframes broadside-fog-churn {
  0%   { box-shadow: 0 0 6px rgba(45,212,191,0.30); filter: brightness(0.9); }
  50%  { box-shadow: 0 0 22px rgba(45,212,191,0.75); filter: brightness(1.28); }
  100% { box-shadow: 0 0 10px rgba(45,212,191,0.50); filter: brightness(1.0); }
}

/* --- Dead Man's Booty · Tempest Voyage — storm crossing --- */
@keyframes voyage-ship-pitch {
  0%   { transform: translateY(0)    rotate(-8deg); }
  25%  { transform: translateY(-4px) rotate(6deg); }
  50%  { transform: translateY(2px)  rotate(-5deg); }
  75%  { transform: translateY(-3px) rotate(7deg); }
  100% { transform: translateY(0)    rotate(-8deg); }
}
@keyframes voyage-swell {
  0%   { transform: translateX(-12%) scaleY(0.8); }
  50%  { transform: translateX(12%)  scaleY(1.15); }
  100% { transform: translateX(-12%) scaleY(0.8); }
}
@keyframes voyage-spray {
  0%   { opacity: 0;   transform: translate(0, 0)     scale(0.6); }
  40%  { opacity: 1;   transform: translate(4px, -8px) scale(1); }
  100% { opacity: 0;   transform: translate(9px, -14px) scale(0.7); }
}
@keyframes voyage-lightning {
  0%, 100% { opacity: 0; }
  8%   { opacity: 1; }
  16%  { opacity: 0; }
  60%  { opacity: 0.9; }
  70%  { opacity: 0; }
}

/* --- High Noon Gold · Wanted Posters — 3D flip + dust puff --- */
.wanted-flip3d {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform 1s cubic-bezier(0.34, 1.35, 0.5, 1);
}
/* The poster RUSTLES in place for a beat, then slips over — a shake before the
   flip on every reveal (the paper-flip sound fires on the click that triggers it). */
.wanted-flip3d.wanted-flipped { transform: rotateY(180deg); animation: wanted-shake-flip 1.15s cubic-bezier(0.34, 1.35, 0.5, 1) both; }
@keyframes wanted-shake-flip {
  0%   { transform: rotateY(0deg)   rotateZ(0deg)    translateX(0); }
  6%   { transform: rotateY(0deg)   rotateZ(-4.5deg) translateX(-2px); }
  12%  { transform: rotateY(0deg)   rotateZ(4.5deg)  translateX(2px); }
  18%  { transform: rotateY(0deg)   rotateZ(-3deg)   translateX(-1px); }
  24%  { transform: rotateY(0deg)   rotateZ(2deg)    translateX(1px); }
  30%  { transform: rotateY(0deg)   rotateZ(0deg)    translateX(0); }
  100% { transform: rotateY(180deg) rotateZ(0deg)    translateX(0); }
}
.reduced-motion .wanted-flip3d.wanted-flipped { animation: none; }
.wanted-face {
  position: absolute; inset: 0;
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 2px;
  border-radius: 0.5rem;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
.wanted-face-front { transform: rotateY(180deg); }
@keyframes wanted-dust-puff {
  0%   { opacity: 0; transform: scale(0.35); }
  22%  { opacity: 0.9; }
  100% { opacity: 0; transform: scale(1.85); }
}
.wanted-dust {
  position: absolute; inset: -18%;
  z-index: 5; pointer-events: none;
  border-radius: 9999px;
  background: radial-gradient(circle, rgba(245,222,179,0.8) 0%, rgba(217,180,120,0.4) 45%, transparent 70%);
  animation: wanted-dust-puff 1s ease-out forwards;
}

/* --- Hive of Midas · Nectar Meadow — bloom unfurl --- */
@keyframes meadow-unfurl {
  0%   { transform: scale(0.3) rotate(-120deg); opacity: 0.4; }
  55%  { transform: scale(1.25) rotate(20deg); opacity: 1; }
  75%  { transform: scale(0.95) rotate(-6deg); opacity: 1; }
  100% { transform: scale(1.1) rotate(0deg); opacity: 1; }
}
@keyframes meadow-pollen-puff {
  0%   { transform: scale(0.2); opacity: 0.9;  box-shadow: 0 0 8px 3px rgba(245,158,11,0.70); }
  70%  { transform: scale(1.8); opacity: 0.35; box-shadow: 0 0 18px 8px rgba(190,242,100,0.40); }
  100% { transform: scale(2.4); opacity: 0;    box-shadow: 0 0 22px 10px rgba(190,242,100,0); }
}
@keyframes meadow-bloom-pulse {
  0%, 100% { box-shadow: 0 0 12px rgba(245,158,11,0.40); }
  50%      { box-shadow: 0 0 26px rgba(190,242,100,0.75); }
}

/* --- Mad Lab · Volatile Formula — chemical beaker bonus --- */
/* liquid bubbles rising inside the flask */
@keyframes madlab-flask-bubble {
  0%   { transform: translateY(4px) scale(0.4); opacity: 0; }
  25%  { opacity: 0.7; }
  100% { transform: translateY(-42px) scale(1.1); opacity: 0; }
}
/* a colored droplet falling from a poured vial into the beaker */
@keyframes madlab-flask-drop {
  0%   { transform: translateY(0) scale(0.85); opacity: 0; }
  12%  { opacity: 1; }
  85%  { opacity: 1; }
  100% { transform: translateY(88px) scale(0.5); opacity: 0; }
}
/* the flask shudders the instant a volatile vial goes in */
@keyframes madlab-flask-shake {
  0%,100% { transform: translateX(0) rotate(0deg); }
  15% { transform: translateX(-4px) rotate(-3deg); }
  30% { transform: translateX(4px) rotate(3deg); }
  45% { transform: translateX(-5px) rotate(-3.5deg); }
  60% { transform: translateX(5px) rotate(3.5deg); }
  80% { transform: translateX(-3px) rotate(-2deg); }
}
/* glass shards flying out on the explosion (custom --dx/--dy/--rot per shard) */
@keyframes madlab-shard-fly {
  0%   { opacity: 0; transform: translate(0,0) rotate(0) scale(0.5); }
  12%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(1); }
}
/* smoke puffs rising after the blast */
@keyframes madlab-smoke-rise {
  0%   { opacity: 0; transform: translateY(0) scale(0.5); }
  25%  { opacity: 0.5; }
  100% { opacity: 0; transform: translateY(-64px) scale(2.4); }
}
/* white flash on a blast or the grand */
@keyframes madlab-flash { 0% { opacity: 0; } 8% { opacity: 0.9; } 100% { opacity: 0; } }
/* the reward number floating up off the beaker */
@keyframes madlab-toast-rise {
  0%   { opacity: 0; transform: translate(-50%, 8px) scale(0.8); }
  18%  { opacity: 1; transform: translate(-50%, 0) scale(1.1); }
  80%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -16px) scale(1); }
}
/* rainbow jets erupting from the beaker on CRITICAL MASS (grand) */
@keyframes madlab-grand-jet {
  0%   { opacity: 0; transform: translateY(0) scale(0.6); }
  20%  { opacity: 1; }
  100% { opacity: 0; transform: translateY(var(--jy, -72px)) scale(1.2); }
}
.madlab-vial:not(:disabled) { cursor: pointer; }
.madlab-vial:not(:disabled):hover { transform: translateY(-6px) !important; filter: brightness(1.15); }
.madlab-vial:not(:disabled):active { transform: translateY(-2px) scale(0.96) !important; }
.reduced-motion [style*="madlab-flask"],
.reduced-motion [style*="madlab-grand"],
.reduced-motion [style*="madlab-shard"],
.reduced-motion [style*="madlab-smoke"],
.reduced-motion [style*="madlab-toast"] { animation-duration: 0.001s !important; }
/* The beaker's ambient bubbles are decoration: off in performance mode. */
.performance-mode .ml-bubble,
.reduced-motion .ml-bubble { animation: none !important; opacity: 0; }

/* --- The Abyss · Trench Descent — submarine bubble trail --- */
@keyframes abyss-bubble-rise {
  0%   { transform: translateY(0) scale(1);    opacity: 0.7; }
  100% { transform: translateY(-22px) scale(0.4); opacity: 0; }
}
.reduced-motion [style*="abyss-bubble"] { animation-duration: 0.001s !important; }

/* ============================================================
   SLOT SKINS — ambient chrome motion (see ui/SlotSkins.js)
   Cheap, slow loops for skinned wheels/frames. Respect reduced-motion.
   ============================================================ */
.ab-hub-pulse { transform-box: fill-box; transform-origin: center; animation: ab-hub-pulse 2.4s ease-in-out infinite; }
@keyframes ab-hub-pulse {
  0%, 100% { opacity: 0.75; transform: scale(0.97); }
  50%      { opacity: 1;    transform: scale(1.04); }
}
.reduced-motion .ab-hub-pulse { animation: none !important; }

/* ============================================================
   SHARED JACKPOT REVEAL — a bonus-game cell that turns out to be a JACKPOT
   gets a themed shake + pulse (.jackpot-cell-pop) and a radial spark burst
   (<JackpotBurst/> -> .jp-burst). Colour comes from --jp-color (the tier/theme
   hue), set inline on the cell. Reused across every machine's pick games.
   ============================================================ */
@keyframes jackpot-cell-pop {
  0%   { transform: scale(1)    translateX(0)   rotate(0deg); }
  12%  { transform: scale(1.2)  translateX(-3px) rotate(-2.5deg); }
  26%  { transform: scale(1.12) translateX(3px)  rotate(2.5deg); }
  40%  { transform: scale(1.16) translateX(-2px) rotate(-1.5deg); }
  54%  { transform: scale(1.1)  translateX(2px)  rotate(1.5deg); }
  70%  { transform: scale(1.13) translateX(-1px) rotate(-0.8deg); }
  100% { transform: scale(1)    translateX(0)   rotate(0deg); }
}
.jackpot-cell-pop { animation: jackpot-cell-pop 0.72s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; z-index: 5; }
.jp-burst { position: absolute; inset: 0; pointer-events: none; overflow: visible; z-index: 6; }
.jp-burst > i {
  position: absolute; left: 50%; top: 50%;
  width: 7px; height: 7px; margin: -3.5px 0 0 -3.5px; border-radius: 9999px;
  background: var(--jp-color, #fbbf24);
  box-shadow: 0 0 7px 1px var(--jp-color, #fbbf24);
  opacity: 0;
  animation: jp-spark-fly 0.72s ease-out forwards;
  animation-delay: var(--delay, 0s);
}
@keyframes jp-spark-fly {
  0%   { opacity: 0; transform: rotate(var(--a)) translateX(0)        scale(0.4); }
  18%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(var(--a)) translateX(var(--d)) scale(1); }
}
.reduced-motion .jackpot-cell-pop,
.reduced-motion .jp-burst > i { animation: none !important; }

/* ============================================================
   LOOT CHESTS — four-tap open ceremony (see ui/ChestOpening.js)
   Three taps strain the lid, the fourth blows it open. Every animation is
   transform/opacity on a promoted layer so the full spectacle — including
   the legendary godrays and the exotic supernova — runs at 60fps on phones.
   Tier escalation is driven by --chest-glow / --chest-c1 / --chest-c2 set
   inline from CHEST_TIERS, so one rule set serves all six tiers.
   ============================================================ */

/* Idle: the sealed chest breathes; higher tiers hover more. */
.chest-float { animation: chest-float 3.4s ease-in-out infinite; will-change: transform; }
@keyframes chest-float {
  0%,100% { transform: translateY(0) rotate(0deg); }
  50%     { transform: translateY(-6px) rotate(-0.6deg); }
}

/* Per-tap impact: the chest takes the hit and recoils. Intensity scales
   with --hit (0→1 across the taps) so tap 3 is visibly more violent. */
.chest-hit-a, .chest-hit-b { animation: chest-hit 0.26s cubic-bezier(0.3,1.5,0.5,1); will-change: transform; }
@keyframes chest-hit {
  0%   { transform: translate3d(0,0,0) scale3d(1,1,1); }
  18%  { transform: translate3d(0, calc(5px * var(--hit,1)), 0) scale3d(calc(1 + 0.10*var(--hit,1)), calc(1 - 0.12*var(--hit,1)), 1); }
  45%  { transform: translate3d(0, calc(-4px * var(--hit,1)), 0) scale3d(calc(1 - 0.07*var(--hit,1)), calc(1 + 0.09*var(--hit,1)), 1); }
  72%  { transform: translate3d(0, calc(1px * var(--hit,1)), 0) scale3d(1.02, 0.98, 1); }
  100% { transform: translate3d(0,0,0) scale3d(1,1,1); }
}

/* Whole-stage shake on impact — a/b alternate so it replays every tap. */
.chest-shake-a, .chest-shake-b { animation: chest-shake 0.22s ease-out; will-change: transform; }
@keyframes chest-shake {
  0%,100% { transform: translate3d(0,0,0); }
  20% { transform: translate3d(calc(-5px*var(--hit,1)), calc(3px*var(--hit,1)),0); }
  45% { transform: translate3d(calc(6px*var(--hit,1)), calc(-4px*var(--hit,1)),0); }
  70% { transform: translate3d(calc(-4px*var(--hit,1)), calc(2px*var(--hit,1)),0); }
}

/* The escaping seam light, brighter each stage via --seam (0→1). */
.chest-seam {
  position:absolute; left:50%; top:46%; width:74%; height:26%;
  transform: translate(-50%,-50%); pointer-events:none; z-index:4; border-radius:9999px;
  background: radial-gradient(closest-side, rgba(255,255,255,calc(0.25 + var(--seam,0)*0.6)),
              rgba(var(--chest-glow,251,191,36), calc(0.18 + var(--seam,0)*0.45)) 45%, transparent 75%);
  animation: chest-seam-pulse 1.5s ease-in-out infinite; will-change: transform, opacity;
}
@keyframes chest-seam-pulse {
  0%,100% { opacity: calc(0.45 + var(--seam,0)*0.35); transform: translate(-50%,-50%) scale(1); }
  50%     { opacity: calc(0.7 + var(--seam,0)*0.3);   transform: translate(-50%,-50%) scale(1.07); }
}

/* Tier aura ring behind the chest — pulses faster as the chest strains. */
.chest-aura {
  position:absolute; left:50%; top:50%; width:118%; aspect-ratio:1;
  transform: translate(-50%,-50%); border-radius:9999px; pointer-events:none; z-index:1;
  background: radial-gradient(closest-side, rgba(var(--chest-glow,251,191,36), calc(0.22 + var(--seam,0)*0.3)), transparent 72%);
  animation: chest-aura-pulse 2.2s ease-in-out infinite; will-change: transform, opacity;
}
@keyframes chest-aura-pulse {
  0%,100% { opacity:.55; transform: translate(-50%,-50%) scale(0.94); }
  50%     { opacity:.95; transform: translate(-50%,-50%) scale(1.06); }
}

/* Chips/splinters knocked off on each tap. */
.chest-chip {
  position:absolute; left:50%; top:50%; width:8px; height:8px; border-radius:2px;
  pointer-events:none; z-index:6;
  animation: chest-chip 0.72s cubic-bezier(0.2,0.8,0.4,1) forwards; will-change: transform, opacity;
}
@keyframes chest-chip {
  0%   { transform: translate3d(-50%,-50%,0) rotate(0) scale(1); opacity:1; }
  100% { transform: translate3d(calc(-50% + var(--cx)), calc(-50% + var(--cy)), 0) rotate(520deg) scale(0.4); opacity:0; }
}

/* The open moment: the chest kicks, then settles.
   NB: .chest-burst / @keyframes chest-burst were RETIMED — see "CHEST CEREMONY
   POLISH" at the bottom of this file. The old 0.9s curve peaked at 126ms while
   its own shockwave and godrays wait for 260ms, so the open read as two soft
   events instead of one hit. Defined once, down there. */

/* Treasure sparks drifting up out of the opened lid — replaces the old solid
   light column, which read as a flat grey beam on the lower tiers. */
.chest-spark {
  position:absolute; left:50%; bottom:50%; width:7px; height:7px; border-radius:9999px;
  pointer-events:none; z-index:3; opacity:0;
  animation: chest-spark 1.9s ease-out forwards; will-change: transform, opacity;
}
@keyframes chest-spark {
  0%   { opacity:0; transform: translate3d(0,0,0) scale(0.4); }
  12%  { opacity:1; }
  100% { opacity:0; transform: translate3d(var(--sx,0), var(--sy,-190px), 0) scale(var(--ss,1)); }
}
/* A soft glow sitting just above the open lid — replaces the hard beam. */
.chest-glowpool {
  position:absolute; left:50%; bottom:44%; width:62%; height:38%;
  transform: translateX(-50%); pointer-events:none; z-index:2; opacity:0;
  background: radial-gradient(closest-side, rgba(255,255,255,0.55), rgba(var(--chest-glow,251,191,36),0.35) 45%, transparent 78%);
  animation: chest-glowpool 1.6s ease-out forwards; will-change: transform, opacity;
}
@keyframes chest-glowpool {
  0%   { opacity:0; transform: translateX(-50%) scale(0.5); }
  25%  { opacity:1; transform: translateX(-50%) scale(1.08); }
  100% { opacity:0.55; transform: translateX(-50%) scale(1); }
}

/* Rewards flying out of the chest, one after another. */
/* No will-change here: a permanently promoted layer with a translucent pill
   background shows its own bounding box as a faint grey rectangle. */
.chest-item-pop { animation: chest-item-pop 0.62s cubic-bezier(0.2,1.5,0.35,1) both; }
@keyframes chest-item-pop {
  0%   { transform: translateY(48px) scale(0.3) rotate(-8deg); opacity:0; }
  55%  { transform: translateY(-10px) scale(1.14) rotate(2deg); opacity:1; }
  100% { transform: translateY(0) scale(1) rotate(0); opacity:1; }
}

/* Legendary: gold filigree ignites and embers rise off the lid. */
.chest-legendary-shimmer {
  position:absolute; inset:-6%; pointer-events:none; z-index:5; mix-blend-mode:screen; overflow:hidden;
  /* Feather the edges — as a bare rectangle this screen-blend layer drew a
     hard-edged bright SQUARE over the chest and clipped the godrays behind it. */
  -webkit-mask-image: radial-gradient(closest-side, #000 22%, rgba(0,0,0,0.45) 46%, transparent 66%);
          mask-image: radial-gradient(closest-side, #000 22%, rgba(0,0,0,0.45) 46%, transparent 66%);
}
/* The band TRANSLATES under the mask (it used to scroll background-position,
   repainting the whole masked layer every frame). */
.chest-legendary-shimmer::before {
  content:''; position:absolute; top:0; bottom:0; left:0; width:40%;
  background: linear-gradient(90deg, transparent, rgba(255,247,214,0.55) 50%, transparent);
  transform: translateX(-120%) skewX(-15deg);
  animation: chest-shimmer 2.6s ease-in-out infinite;
}
@keyframes chest-shimmer {
  0%   { transform: translateX(-120%) skewX(-15deg); } 100% { transform: translateX(270%) skewX(-15deg); }
}

/* Ray falloff. A repeating-conic-gradient has no distance fade of its own, so
   the rays run at full strength until something clips them — the viewport edge,
   or the ray square itself — and read as beams that just STOP mid-air. Mask
   them out with radius so they dissolve into the dark instead. */
.chest-ray-fade {
  -webkit-mask-image: radial-gradient(closest-side, #000 3%, rgba(0,0,0,0.9) 18%, rgba(0,0,0,0.45) 33%, rgba(0,0,0,0.12) 46%, transparent 58%);
          mask-image: radial-gradient(closest-side, #000 3%, rgba(0,0,0,0.9) 18%, rgba(0,0,0,0.45) 33%, rgba(0,0,0,0.12) 46%, transparent 58%);
}
/* The same falloff for a ray wheel sized to what the mask actually shows.
   On a 200vmax square everything past 58vmax from the centre is masked to
   nothing — yet all of it was composited (and screen-blended) every frame of
   the spin. At 120vmax with the stops rescaled (×100/60) the visible rays are
   identical and the spinning layer is 64% smaller. (ui/CardsUI.js,
   ui/ChestOpening.js) */
.cards-ray-fade {
  -webkit-mask-image: radial-gradient(closest-side, #000 5%, rgba(0,0,0,0.9) 30%, rgba(0,0,0,0.45) 55%, rgba(0,0,0,0.12) 77%, transparent 97%);
          mask-image: radial-gradient(closest-side, #000 5%, rgba(0,0,0,0.9) 30%, rgba(0,0,0,0.45) 55%, rgba(0,0,0,0.12) 77%, transparent 97%);
}
.pack-godrays.cards-godrays-120 { width:120vmax; height:120vmax; margin:-60vmax 0 0 -60vmax; }

/* Exotic: a slow prismatic halo that never stops turning. */
.chest-exotic-halo {
  position:absolute; left:50%; top:50%; width:150%; aspect-ratio:1; margin:0; z-index:0;
  transform: translate(-50%,-50%); border-radius:9999px; pointer-events:none; mix-blend-mode:screen;
  background: conic-gradient(from 0deg,#ef4444,#f97316,#facc15,#4ade80,#38bdf8,#a855f7,#ef4444);
  -webkit-mask-image: radial-gradient(closest-side,transparent 42%,#000 58%,transparent 78%);
          mask-image: radial-gradient(closest-side,transparent 42%,#000 58%,transparent 78%);
  opacity:.75; animation: chest-halo-spin 6s linear infinite; will-change: transform;
}
@keyframes chest-halo-spin { to { transform: translate(-50%,-50%) rotate(360deg); } }

/* Orbiting shards for the exotic chest. */
.chest-orbit {
  position:absolute; left:50%; top:50%; width:10px; height:10px; margin:-5px 0 0 -5px;
  pointer-events:none; z-index:5;
  animation: chest-orbit var(--orb-dur,4s) linear infinite; will-change: transform;
}
@keyframes chest-orbit {
  from { transform: rotate(0deg) translateX(var(--orb-r,54px)) rotate(0deg); }
  to   { transform: rotate(360deg) translateX(var(--orb-r,54px)) rotate(-360deg); }
}

/* Reduced-motion (explicit accessibility opt-in) only. */
.reduced-motion .chest-float, .reduced-motion .chest-hit-a, .reduced-motion .chest-hit-b,
.reduced-motion .chest-shake-a, .reduced-motion .chest-shake-b, .reduced-motion .chest-seam,
.reduced-motion .chest-aura, .reduced-motion .chest-legendary-shimmer, .reduced-motion .chest-legendary-shimmer::before,
.reduced-motion .chest-exotic-halo, .reduced-motion .chest-orbit { animation: none !important; }
.reduced-motion .chest-burst, .reduced-motion .chest-glowpool,
.reduced-motion .chest-item-pop { animation-duration: 0.01s !important; animation-delay: 0s !important; }
.reduced-motion .chest-chip, .reduced-motion .chest-spark { display: none !important; }

/* Chest upgraded mid-ceremony — a short celebratory pop above the chest. */
.chest-upgrade-flash { animation: chest-upgrade-flash 1.5s cubic-bezier(0.2,1.4,0.35,1) forwards; will-change: transform, opacity; }
@keyframes chest-upgrade-flash {
  0%   { opacity: 0; transform: translate(-50%, 10px) scale(0.6); }
  18%  { opacity: 1; transform: translate(-50%, -4px) scale(1.12); }
  34%  { transform: translate(-50%, 0) scale(1); }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -14px) scale(0.96); }
}
.reduced-motion .chest-upgrade-flash { animation-duration: 0.01s !important; }

/* --- Chest ceremony polish: nothing should hard-cut ----------------------- */
/* Stage art is stacked and cross-faded, so advancing a stage dissolves. */
.chest-art-layer {
  position: absolute; inset: 0;
  transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  will-change: opacity;
}
/* overflow:visible — the open-chest art draws its light rays and burst glow
   PAST the 96×96 viewBox (ray paths start at y=-14). An SVG viewport clips by
   default, which sliced the fan off in a dead-straight line across the top of
   the art box and left a faint rectangle around the chest. Let them spill; the
   ray gradients already fade to zero alpha at their far ends. */
.chest-art-layer > svg { width: 100%; height: 100%; display: block; overflow: visible; }
/* An upgrade swaps the whole art set — play it as a deliberate morph. */
.chest-art-stack { animation: chest-tier-morph 0.55s cubic-bezier(0.2, 1.3, 0.35, 1); }
/* transform + opacity only — the whiteout over the room carries the flash */
@keyframes chest-tier-morph {
  0%   { transform: scale(0.82) rotate(-3deg); opacity: 0.15; }
  45%  { transform: scale(1.08) rotate(1.5deg); opacity: 1; }
  100% { transform: scale(1) rotate(0); opacity: 1; }
}
/* Prompt lines fade+rise in rather than snapping between strings. */
.chest-prompt { animation: chest-prompt-in 0.34s ease-out both; }
@keyframes chest-prompt-in {
  from { opacity: 0; transform: translateY(5px); }
  to   { opacity: 1; transform: translateY(0); }
}
.reduced-motion .chest-art-stack, .reduced-motion .chest-prompt { animation: none !important; }
.reduced-motion .chest-art-layer { transition: none !important; }

/* ============================================================
   CHEST CEREMONY POLISH  (build 51 — see ui/ChestOpening.js)
   Adds the beats the ceremony was missing: the chest never rests
   between blows, it announces each structural failure, the final
   tap converges INWARD, the open lands on one detonation frame,
   the haul is ordered worst→best and the ending is a crest.
   Same rules as the pack kit above: transform/opacity only, on
   promoted layers, bounded counts, every class guarded below.
   ============================================================ */

/* --- Under load: the chest quivers between blows, harder as the meter fills.
   Lives on the strike <button> (background:none), so there is no translucent
   fill for a promoted layer to expose as a bounding box. */
.chest-strain { animation: chest-strain 0.28s linear infinite; will-change: transform; }
.chest-strain.calm  { animation-duration: 0.95s !important; }  /* clicking stopped → breathing */
.chest-strain.brink { animation-duration: 0.11s !important; }  /* final tap → buzzing */
@keyframes chest-strain {
  0%,100% { transform: translate3d(0,0,0) rotate(0deg); }
  25% { transform: translate3d(calc(-3px*var(--strain,0)), calc(1.8px*var(--strain,0)),0) rotate(calc(-0.35deg*var(--strain,0))); }
  50% { transform: translate3d(calc(3.6px*var(--strain,0)), calc(-2.4px*var(--strain,0)),0) rotate(calc(0.4deg*var(--strain,0))); }
  75% { transform: translate3d(calc(-2.1px*var(--strain,0)), calc(1.2px*var(--strain,0)),0) rotate(calc(-0.2deg*var(--strain,0))); }
}

/* --- BRINK (the last tap): everything converges INWARD, the visual opposite of
   the payoff, so it reads as charging rather than opening. */
.chest-brink-ring {
  position:absolute; left:50%; top:50%; width:240px; height:240px; margin:-120px 0 0 -120px;
  border:2px solid rgba(255,255,255,0.9); border-radius:9999px; pointer-events:none; z-index:3; opacity:0;
  animation: chest-brink-ring 1.1s ease-in infinite; will-change: transform, opacity;
}
.chest-brink-ring.two { animation-delay: 0.55s; border-color: rgba(var(--chest-glow,251,191,36),0.9); }
@keyframes chest-brink-ring {
  0%   { opacity:0;    transform: translateZ(0) scale(2.1); }
  30%  { opacity:0.75; }
  100% { opacity:0;    transform: translateZ(0) scale(0.72); }
}
.chest-seam.brink { animation-duration: 0.34s !important; }
.chest-aura.brink { animation-duration: 0.5s  !important; }
.chest-brink-beat { animation: chest-brink-beat 0.62s ease-in-out infinite; will-change: transform; }
@keyframes chest-brink-beat {
  0%,100% { transform: scale(1); } 14% { transform: scale(1.16); }
  28% { transform: scale(1.02); } 44% { transform: scale(1.22); } 62% { transform: scale(1); }
}

/* --- Stage breach: the chest names each structural failure. Transform/opacity
   only — deliberately NOT letter-spacing, which would relayout every frame. */
.chest-stage-callout {
  position:absolute; left:50%; top:50%; z-index:7; pointer-events:none; white-space:nowrap;
  font-weight:900; text-transform:uppercase; letter-spacing:0.3em; font-size:clamp(13px,3.2vw,20px); color:#fff;
  text-shadow: 0 0 20px rgba(var(--chest-glow,251,191,36),0.95), 0 2px 6px rgba(0,0,0,0.8);
  animation: chest-stage-callout 0.95s cubic-bezier(0.15,0.9,0.25,1) forwards; will-change: transform, opacity;
}
@keyframes chest-stage-callout {
  0%   { opacity:0; transform: translate3d(-50%,-50%,0) scale3d(0.5,0.5,1); }
  16%  { opacity:1; transform: translate3d(-50%,-50%,0) scale3d(1.14,1.14,1); }
  42%  { opacity:1; transform: translate3d(-50%,-50%,0) scale3d(1,1,1); }
  100% { opacity:0; transform: translate3d(-50%,-50%,0) scale3d(1.55,1.55,1); }
}

/* --- The open moment, retimed so crush → held beat → detonation land as ONE
   hit on the 0.26s beat the shockwave/godrays already wait for. */
.chest-burst { animation: chest-burst 1.05s cubic-bezier(0.16,0.9,0.3,1) both; will-change: transform; }
@keyframes chest-burst {
  0%   { transform: scale(1) translateY(0); }
  13%  { transform: scale(0.87) translateY(9px); }    /* the crush */
  22%  { transform: scale(0.90) translateY(7px); }    /* the held beat — nothing happens */
  25%  { transform: scale(1.34) translateY(-22px); }  /* 262ms = shockwave/godray delay */
  40%  { transform: scale(1.12) translateY(-8px); }
  100% { transform: scale(1.04) translateY(0); }
}
/* White-hot blowout, tier-tinted from the inherited --chest-glow. Radial, so it
   feathers itself — no conic, therefore no ray mask needed. */
.chest-detonate-flash {
  position:absolute; inset:0; pointer-events:none; z-index:9; opacity:0;
  background: radial-gradient(closest-side at 50% 50%, rgba(255,255,255,0.95),
              rgba(var(--chest-glow,251,191,36),0.55) 32%, transparent 68%);
  animation: chest-detonate-flash 0.62s ease-out 0.24s forwards; will-change: opacity;
}
@keyframes chest-detonate-flash { 0%{opacity:0;} 12%{opacity:1;} 100%{opacity:0;} }
/* The world squeezes shut around the open chest for the hold, then releases as
   the first reward lands. Its 26% transparent core always clears the 260px
   chest box, so it can never dim the hero object. */
.chest-charge {
  position:absolute; left:50%; top:50%; width:150vmax; height:150vmax; margin:-75vmax 0 0 -75vmax;
  pointer-events:none; z-index:4; opacity:0; transform: translateZ(0) scale(1.9);
  background: radial-gradient(closest-side, transparent 26%, rgba(0,0,0,0.55) 58%, rgba(0,0,0,0.92) 100%);
  animation: chest-charge var(--charge-ms,950ms) cubic-bezier(0.36,0,0.2,1) forwards;
  will-change: transform, opacity;
}
@keyframes chest-charge {
  0%   { opacity:0; transform: translateZ(0) scale(1.9); }
  18%  { opacity:0.9; transform: translateZ(0) scale(1.3); }
  72%  { opacity:1;  transform: translateZ(0) scale(0.94); }
  86%  { opacity:1;  transform: translateZ(0) scale(0.9); }
  100% { opacity:0;  transform: translateZ(0) scale(2.6); }
}
/* Snap the closed→open art swap instead of dissolving it over 0.3s. */
.chest-art-snap .chest-art-layer { transition-duration: 0.08s; }

/* --- Premonition: up to 3 rings, ascending rarity, fired inside the hold.
   Honest — `items` is already in state, so these preview a real haul. */
.chest-tell {
  position:absolute; left:50%; top:50%; width:190px; height:190px; margin:-95px 0 0 -95px;
  border-radius:9999px; pointer-events:none; z-index:5;
  border: var(--tell-w,3px) solid rgba(var(--tell-rgb,251,191,36),0.95);
  box-shadow: 0 0 26px rgba(var(--tell-rgb,251,191,36),0.5);   /* STATIC, never animated */
  opacity:0; transform: translateZ(0) scale(0.35);
  animation: chest-tell 0.72s cubic-bezier(0.12,0.8,0.25,1) both; will-change: transform, opacity;
}
@keyframes chest-tell {
  0%{opacity:0;transform:translateZ(0) scale(0.35);} 18%{opacity:0.9;}
  100%{opacity:0;transform:translateZ(0) scale(2.6);}
}

/* --- Loot is FIRED out of the lid: stretch-then-squash comets whose count and
   colour scale with the item's rank. Lives in the 260px box, never the haul
   list — that list is max-h/overflow-auto and would guillotine them. */
.chest-eject {
  position:absolute; left:50%; top:44%; width:9px; height:9px; border-radius:9999px;
  pointer-events:none; z-index:7; opacity:0;
  animation: chest-eject 0.52s cubic-bezier(0.2,0.75,0.35,1) forwards; will-change: transform, opacity;
}
@keyframes chest-eject {
  0%   { opacity:0; transform: translate3d(-50%,0,0) scale(0.4,2.2); }
  14%  { opacity:1; transform: translate3d(-50%,-46px,0) scale(0.55,2.4); }
  100% { opacity:0; transform: translate3d(calc(-50% + var(--ex,0px)), var(--ey,168px), 0) scale(1.5,0.5); }
}
/* The rarity word SLAMS in — reserved for rank >= 4 so it stays rare. */
.chest-rank-callout {
  position:absolute; left:50%; top:8%; z-index:12; pointer-events:none; white-space:nowrap;
  transform-origin:50% 50%;
  animation: chest-rank-callout 1.0s cubic-bezier(0.15,1.5,0.3,1) forwards; will-change: transform, opacity;
}
@keyframes chest-rank-callout {
  0%   { opacity:0; transform: translate(-50%,0) scale(2.4); }
  16%  { opacity:1; transform: translate(-50%,0) scale(0.94); }
  26%  { transform: translate(-50%,0) scale(1.04); }
  70%  { opacity:1; transform: translate(-50%,0) scale(1); }
  100% { opacity:0; transform: translate(-50%,-16px) scale(1); }
}

/* --- The ending: the spent chest steps back, a blade of light closes the haul.
   Settle rides the 260px box, NOT the burst wrapper — .chest-burst is `both`,
   so its forwards fill would pin transform and this transition would never run. */
.chest-settle {
  transform: scale(0.82) translateY(6px); opacity: 0.62;
  transition: transform 0.5s cubic-bezier(0.3,0.9,0.3,1), opacity 0.5s ease-out;
}
.chest-finale-bar {
  position:absolute; left:50%; top:0; width:min(760px,92vw); height:10px; border-radius:9999px;
  pointer-events:none; z-index:11; transform: translate(-50%,0) scaleX(0);
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.9) 35%,
              rgba(var(--best-glow,251,191,36),0.95) 50%, rgba(255,255,255,0.9) 65%, transparent);
  animation: chest-finale-bar 0.85s cubic-bezier(0.12,0.85,0.25,1) forwards; will-change: transform, opacity;
}
@keyframes chest-finale-bar {
  0%   { opacity:0; transform: translate(-50%,0) scaleX(0.02) scaleY(2.2); }
  22%  { opacity:1; transform: translate(-50%,0) scaleX(1) scaleY(1); }
  100% { opacity:0; transform: translate(-50%,0) scaleX(1.06) scaleY(0.3); }
}

/* --- The NEXT chest twitches at you from the Open Another button. */
.chest-next-tease { animation: chest-next-tease 2.4s ease-in-out infinite; transform-origin: 50% 85%; will-change: transform; }
@keyframes chest-next-tease {
  0%,72%,100% { transform: rotate(0deg) scale(1); }
  80%         { transform: rotate(-7deg) scale(1.08); }
  88%         { transform: rotate(5deg)  scale(1.06); }
}

/* --- Tier upgrade: a whiteout, and the OLD chest's husk balloons off the new
   one. The husk is a SIBLING of .chest-art-stack, never a child — that stack is
   keyed by tier and is destroyed the instant the upgrade lands. */
.chest-upgrade-whiteout {
  position:absolute; inset:0; pointer-events:none; z-index:25; opacity:0;
  background: radial-gradient(circle at 50% 46%, #fff 0%, rgba(255,255,255,0.85) 20%,
              rgba(var(--chest-glow,251,191,36),0.5) 44%, transparent 78%);
  animation: chest-upgrade-whiteout 0.72s cubic-bezier(0.1,0.7,0.2,1) forwards; will-change: transform, opacity;
}
@keyframes chest-upgrade-whiteout { 0%{opacity:0;transform:scale(0.5);} 7%{opacity:1;transform:scale(1);} 100%{opacity:0;transform:scale(1.25);} }
.chest-shed {
  position:absolute; inset:0; pointer-events:none; z-index:6;
  animation: chest-shed 0.7s cubic-bezier(0.15,0.85,0.25,1) forwards; will-change: transform, opacity;
}
/* overflow:visible for the same reason as .chest-art-layer — the art draws past its viewBox. */
.chest-shed > svg { width:100%; height:100%; display:block; overflow:visible; }
@keyframes chest-shed { 0%{opacity:0.95;transform:scale3d(1,1,1);} 100%{opacity:0;transform:scale3d(1.85,1.85,1);} }

/* --- new: chest ceremony polish (reduced-motion). .chest-brink-ring/.chest-tell/
   .chest-eject take display:none, NOT animation:none — they are opacity:0 in
   their base state, so freezing them would park a static ring on the chest. */
.reduced-motion .chest-strain, .reduced-motion .chest-brink-beat,
.reduced-motion .chest-next-tease { animation: none !important; }
.reduced-motion .chest-stage-callout, .reduced-motion .chest-charge,
.reduced-motion .chest-detonate-flash, .reduced-motion .chest-rank-callout,
.reduced-motion .chest-finale-bar, .reduced-motion .chest-upgrade-whiteout,
.reduced-motion .chest-shed { animation-duration: 0.01s !important; animation-delay: 0s !important; }
.reduced-motion .chest-brink-ring, .reduced-motion .chest-tell,
.reduced-motion .chest-eject { display: none !important; }
.reduced-motion .chest-settle { transition: none !important; }

/* ============================================================
   SHARED CEREMONY KIT — used by BOTH the chest ceremony
   (ui/ChestOpening.js) and the card-pack ceremony (ui/CardsUI.js).
   ============================================================ */

/* --- Segmented pressure meter. Both ceremonies' bars were a bare width
   transition — the most inert element on their screen. Notches make progress
   COUNTABLE, which matters most where a smooth bar barely moves per input
   (a 40-tap exotic chest, a 14-click legendary pack). The notches are a
   repeating gradient, never spans, so the tick count costs zero DOM. */
.meter-track {
  position:relative; height:12px; border-radius:9999px;
  background:#292524; border:1px solid #44403c; overflow:hidden; transform-origin:50% 50%;
}
.meter-track::after {
  content:''; position:absolute; inset:0; pointer-events:none; z-index:3;
  background-image: repeating-linear-gradient(90deg, transparent 0 calc(100%/var(--ticks,8) - 2px),
                     rgba(0,0,0,0.55) calc(100%/var(--ticks,8) - 2px) calc(100%/var(--ticks,8)));
}
/* KEEP THIS TRANSITION SHORTER THAN A FAST TAP INTERVAL (~110-125ms).
   Both meters back a rapid-tap speed test. Any transition that outlives the
   gap between taps gets restarted mid-flight by the next tap, so the bar
   drifts further and further behind the clicks and then lurches several
   notches at once — which reads as the game dropping inputs. 70ms linear is
   under the ~100ms "feels instant" threshold while still smoothing the step.
   The slow "you lost ground" read lives on .rip-ghost (0.55s), not here. */
.meter-fill { position:relative; z-index:2; height:100%; border-radius:9999px; transition: width 0.07s linear; }
/* The chest's tap meter is the primary progress read on that screen (the pack's
   sits under a much bigger pack), so it gets a taller track and fatter notches. */
.meter-track.meter-track-lg { height:18px; border-radius:9999px; }
.meter-track.meter-track-lg::after {
  background-image: repeating-linear-gradient(90deg, transparent 0 calc(100%/var(--ticks,8) - 3px),
                     rgba(0,0,0,0.6) calc(100%/var(--ticks,8) - 3px) calc(100%/var(--ticks,8)));
}
/* The pressure spike rides the TRACK, not the fill — the track is
   overflow:hidden, so a scaleY on the fill would just be clipped. */
.meter-kick-a, .meter-kick-b { animation: meter-kick 0.24s cubic-bezier(0.2,1.6,0.4,1); will-change: transform; }
@keyframes meter-kick { 0%{transform:scaleY(1);} 30%{transform:scaleY(1.7);} 100%{transform:scaleY(1);} }
/* Redline — names the danger zone without a word of copy. */
.meter-redline {
  position:absolute; inset:0; border-radius:9999px; z-index:4; pointer-events:none;
  box-shadow: inset 0 0 0 2px rgba(255,255,255,0.95);
  animation: meter-redline 0.3s ease-in-out infinite; will-change: opacity;
}
@keyframes meter-redline { 0%,100%{opacity:0.15;} 50%{opacity:1;} }

/* --- Object arrival. The pouch and the chest used to simply EXIST when the
   overlay faded up; now they drop in and land with weight.
   `backwards`, NOT `both`: the 100% keyframe is the identity transform, and a
   forwards fill would pin `transform` forever and block .chest-settle's
   transition at the end of the chest ceremony. */
.ceremony-arrive { animation: ceremony-arrive 0.62s cubic-bezier(0.2,0.9,0.25,1.15) 0.12s backwards; will-change: transform; }
@keyframes ceremony-arrive {
  0%   { transform: translate3d(0,-130px,0) scale3d(0.74,1.18,1); }
  46%  { transform: translate3d(0,0,0) scale3d(1.16,0.82,1); }   /* land squash */
  70%  { transform: translate3d(0,-8px,0) scale3d(0.95,1.06,1); }
  100% { transform: translate3d(0,0,0) scale3d(1,1,1); }
}

/* --- new: shared ceremony kit (reduced-motion) --- */
.reduced-motion .meter-redline { animation: none !important; }
.reduced-motion .meter-kick-a, .reduced-motion .meter-kick-b,
.reduced-motion .ceremony-arrive { animation-duration: 0.01s !important; animation-delay: 0s !important; }
.reduced-motion .meter-fill { transition: none !important; }

/* ============================================================
   PACK CEREMONY POLISH  (build 51 — see ui/CardsUI.js)
   The rip becomes a fight you can lose ground in, the tear lands
   as one camera hit, each rarity gets its own reveal, and the
   haul actually ends. Same rules as the kit above.
   ============================================================ */

/* --- The tear as ONE felt hit: suck in, SLAM out on the shared 0.26s beat the
   godrays/shockwave already wait for, then settle. */
.pack-drop-punch { animation: pack-drop-punch 0.9s cubic-bezier(0.2,0.9,0.25,1) both; will-change: transform; }
@keyframes pack-drop-punch {
  0%   { transform: scale3d(1,1,1); }
  22%  { transform: scale3d(0.93,0.93,1) translate3d(0,4px,0); }    /* .pack-inhale peaks here */
  29%  { transform: scale3d(1.09,1.09,1) translate3d(0,-6px,0); }   /* 0.29 x 0.9s = 0.26s */
  38%  { transform: scale3d(0.985,0.985,1); }
  55%  { transform: scale3d(1.015,1.015,1); }
  100% { transform: scale3d(1,1,1); }
}
/* NO will-change: the pack overlay root carries backdrop-blur-md, and a promoted
   translucent child re-samples the blurred backdrop. Mirrors .legendary-flash. */
.pack-drop-flash {
  background: radial-gradient(72% 72% at 50% 46%, rgba(255,255,255,0.92),
              rgba(var(--pk-glow,244,114,182),0.35) 55%, transparent 78%);
  opacity:0; animation: pack-drop-flash 0.34s ease-out 0.24s forwards;
}
@keyframes pack-drop-flash { 0%{opacity:0;} 12%{opacity:1;} 100%{opacity:0;} }

/* --- The drain (PACK_RIP_DRAIN_*) was a real, tuned mechanic that was 100%
   invisible — the bar just quietly shrank. Now the ground you lose is shown in
   red and the pouch sags in your grip. */
.rip-ghost {
  position:absolute; inset:0 auto 0 0; width:100%; transform-origin:0 50%; z-index:1; pointer-events:none;
  border-radius:9999px;
  background: linear-gradient(90deg, rgba(239,68,68,0.5), rgba(248,113,113,0.9));
  transition: transform 0.55s cubic-bezier(0.3,0,0.2,1) 0.18s;
}
.pack-slip { animation: pack-slip 0.42s ease-in-out infinite; will-change: transform; }
@keyframes pack-slip {
  0%,100% { transform: translate3d(0,0,0) rotate(-0.9deg); }
  50%     { transform: translate3d(0,3px,0) rotate(0.9deg); }
}
.rip-panic { animation: rip-urge 0.17s ease-in-out infinite; }   /* existing keyframe, 2x tempo */
/* Class form of the pack's idle float. It used to be an INLINE animation, which
   no .reduced-motion rule can reach — the one known hole in the kit. */
.pack-idle-float { animation: lobby-float 3s ease-in-out infinite; }

/* --- Treasure light escaping the crimp as the seal gives: the rip had effort
   but no promise. Radial and self-feathering — no clip-path, no mix-blend, so
   nothing can draw a hard rectangle. The LOOP animates opacity only; the
   --tense response rides transform via a transition. */
.pack-tear-glow {
  position:absolute; left:20%; right:7%; top:2%; height:26%;
  pointer-events:none; z-index:5; border-radius:9999px; transform-origin:0% 100%;
  background: radial-gradient(closest-side, rgba(255,255,255,0.92),
              rgba(var(--pk-glow,244,114,182),0.55) 42%, transparent 78%);
  opacity: calc(0.20 + var(--tense,0) * 0.62);          /* base = the reduced-motion resting state */
  transform: scale(calc(0.3 + var(--tense,0) * 0.85));
  transition: transform 0.16s ease-out;
  animation: pack-tear-glow 1s ease-in-out infinite;
}
@keyframes pack-tear-glow {
  0%,100% { opacity: calc(0.18 + var(--tense,0) * 0.62); }
  50%     { opacity: calc(0.30 + var(--tense,0) * 0.70); }
}

/* --- Each tap physically kicks the card, harder as its upgrade chances burn.
   ~3 of every 4 taps used to produce zero card feedback. */
.card-kick-a, .card-kick-b { animation: card-kick 0.22s cubic-bezier(0.3,1.5,0.5,1); will-change: transform; }
@keyframes card-kick {
  0%   { transform: scale3d(1,1,1) rotate(0); }
  30%  { transform: scale3d(calc(1 - 0.03 - 0.05*var(--charge,0)), calc(1 + 0.03 + 0.06*var(--charge,0)),1) rotate(calc(-1.5deg*var(--charge,0))); }
  60%  { transform: scale3d(calc(1 + 0.04*var(--charge,0)), calc(1 - 0.04*var(--charge,0)),1) rotate(calc(1deg*var(--charge,0))); }
  100% { transform: scale3d(1,1,1) rotate(0); }
}
/* Chances spent — the card is cocked and straining to open. */
.card-primed { animation: card-primed 0.62s ease-in-out infinite; will-change: transform; }
@keyframes card-primed {
  0%,100% { transform: translate3d(0,0,0) rotate(0); }
  25% { transform: translate3d(-1.5px,0.5px,0) rotate(-0.6deg); }
  50% { transform: translate3d(0,-1px,0) rotate(0); }
  75% { transform: translate3d(1.5px,0.5px,0) rotate(0.6deg); }
}

/* --- A failed upgrade roll was invisible; the player never knew a roll
   happened. A ring snapping INWARD is the exact opposite direction from
   .upgrade-burst, so "no" vs "YES" is readable before you parse a label.
   z-19 sits just under .upgrade-burst's z-20, so a hit always paints over a miss. */
.card-roll-ring {
  position:absolute; inset:-6px; z-index:19; border:2px solid; border-radius:0.95rem; pointer-events:none;
  animation: card-roll-ring 0.34s cubic-bezier(0.25,0.9,0.3,1) forwards; will-change: transform, opacity;
}
@keyframes card-roll-ring {
  0%   { transform: scale(1.4);  opacity:0; }
  35%  { transform: scale(1.06); opacity:0.85; }
  100% { transform: scale(0.88); opacity:0; }
}

/* --- Rarity signatures. Common and rare shared one reveal and epic had NO
   visual at all. Ladder: common = flip flash only (the restraint is what makes
   the rest read) -> rare = sonar double-ping -> epic = shards + the room takes
   a violet wash -> legendary/wild = the existing spectacles. */
.rvl-rare {
  position:absolute; inset:0; border:3px solid; border-radius:0.9rem; z-index:12; pointer-events:none;
  animation: rvl-rare 0.6s cubic-bezier(0.15,0.8,0.25,1) forwards; will-change: transform, opacity;
}
.rvl-rare.two { border-width:2px; animation-delay:0.13s; }
@keyframes rvl-rare { 0%{transform:scale(0.9);opacity:0.95;} 100%{transform:scale(1.9);opacity:0;} }
/* Radial (self-feathering), no mix-blend-mode, and NO will-change — it lives
   under the blurred pack overlay. */
.rvl-room-tint {
  position:absolute; inset:0; pointer-events:none; z-index:9;
  background: radial-gradient(70% 70% at 50% 50%, rgba(var(--rvl-rgb),0.34), rgba(var(--rvl-rgb),0.10) 55%, transparent 78%);
  animation: rvl-room-tint 0.6s ease-out forwards;
}
@keyframes rvl-room-tint { 0%{opacity:0;} 22%{opacity:1;} 100%{opacity:0;} }

/* --- Hero hush: for the 1800ms legendary/wild hold the room actually stops.
   Plain opacity + scale — no blur, no backdrop-filter, so no backdrop
   re-sampling. The 1.8s duration is locked to the existing hold. */
.card-bystander { transform: scale(0.88); opacity: 0.22; transition: transform 0.32s ease-out, opacity 0.32s ease-out; }
.hero-bar { position:absolute; left:0; right:0; height:11vh; background:#000; z-index:12; pointer-events:none; will-change: transform; }
.hero-bar.top    { top:0;    animation: hero-bar-top 1.8s cubic-bezier(0.2,0.9,0.3,1) both; }
.hero-bar.bottom { bottom:0; animation: hero-bar-bot 1.8s cubic-bezier(0.2,0.9,0.3,1) both; }
@keyframes hero-bar-top { 0%{transform:translateY(-100%);} 14%{transform:translateY(0);} 80%{transform:translateY(0);} 100%{transform:translateY(-100%);} }
@keyframes hero-bar-bot { 0%{transform:translateY(100%);}  14%{transform:translateY(0);} 80%{transform:translateY(0);} 100%{transform:translateY(100%);} }

/* --- The most valuable card in the pack used to flip with the same 0.5s punch
   as a common. Declared AFTER .flip-punch so it wins on order AND specificity. */
.flip-inner.flipped.flip-punch.flip-hero { animation: flip-hero 1.05s cubic-bezier(0.35,0,0.2,1) forwards; }
@keyframes flip-hero {
  0%   { transform: rotateY(0)       translateZ(0)    scale(1); }
  18%  { transform: rotateY(-22deg)  translateZ(50px) scale(1.06); }   /* wind back */
  32%  { transform: rotateY(-22deg)  translateZ(50px) scale(1.06); }   /* the HELD breath */
  64%  { transform: rotateY(150deg)  translateZ(55px) scale(1.08); }   /* the whip */
  80%  { transform: rotateY(186deg)  translateZ(16px) scale(1.03); }   /* overshoot */
  100% { transform: rotateY(180deg)  translateZ(0)    scale(1); }
}
.lux-flip-flash.hero-flash-delay { animation-delay: 0.52s; }   /* light hits when the art arrives */

/* --- The haul actually ends: a spotlight passes over the finished row card by
   card and the best pull is crowned. Driven by transform, NOT an animated
   background-position. background-repeat:no-repeat is load-bearing (see
   .card-foil) or the band tiles across the card. */
.haul-shine {
  position:absolute; inset:0; border-radius:1rem; z-index:13; pointer-events:none; mix-blend-mode:screen;
  background: linear-gradient(105deg, transparent 38%, rgba(255,255,255,0.85) 50%, transparent 62%);
  background-size: 240% 100%;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 18%, #000 82%, transparent);
  animation: haul-shine 0.55s ease-out both; will-change: transform, opacity;
}
@keyframes haul-shine { 0%{transform:translateX(-120%);opacity:0;} 30%{opacity:0.9;} 100%{transform:translateX(120%);opacity:0;} }
.haul-crown {
  /* -20px clears the card's own rarity badge, which sits just inside the top edge. */
  position:absolute; left:50%; top:-20px; z-index:14; white-space:nowrap;
  padding:2px 9px; border-radius:9999px; font-size:9px; font-weight:900; letter-spacing:0.18em;
  text-transform:uppercase; pointer-events:none;
  animation: haul-crown-in 0.5s cubic-bezier(0.2,1.7,0.4,1) both; will-change: transform, opacity;
}
@keyframes haul-crown-in {
  0%   { opacity:0; transform: translateX(-50%) translateY(10px) scale(0.4) rotate(-8deg); }
  60%  { opacity:1; transform: translateX(-50%) translateY(0) scale(1.15) rotate(3deg); }
  100% { opacity:1; transform: translateX(-50%) translateY(0) scale(1) rotate(0); }
}
.haul-summary-in { animation: haul-summary-in 0.45s cubic-bezier(0.2,1.3,0.4,1) 0.42s both; }
@keyframes haul-summary-in { 0%{opacity:0;transform:translateY(14px) scale(0.94);} 100%{opacity:1;transform:translateY(0) scale(1);} }

/* --- new: pack ceremony polish (reduced-motion) --- */
.reduced-motion .pack-slip, .reduced-motion .rip-panic, .reduced-motion .pack-idle-float,
.reduced-motion .pack-tear-glow, .reduced-motion .card-primed,
.reduced-motion .flip-inner.flipped.flip-punch.flip-hero { animation: none !important; }
.reduced-motion .pack-drop-punch, .reduced-motion .pack-drop-flash, .reduced-motion .card-kick-a,
.reduced-motion .card-kick-b, .reduced-motion .card-roll-ring, .reduced-motion .rvl-rare,
.reduced-motion .rvl-room-tint, .reduced-motion .haul-shine, .reduced-motion .haul-crown,
.reduced-motion .haul-summary-in { animation-duration: 0.01s !important; animation-delay: 0s !important; }
.reduced-motion .hero-bar { display: none !important; }
.reduced-motion .card-bystander, .reduced-motion .rip-ghost { transition: none !important; }


/* ============================================================
   SPECIAL OFFER POLISH
   The offer's light now falls from the very top edge of the panel (the
   gradient used to start on the hero-art block, a third of the way down, so
   it read as switching on mid-panel). This adds the slow breath on top of it
   plus the tile treatment the contents rows share across all four layouts.
   ============================================================ */
.offer-beam { animation: offer-beam-breathe 5.2s ease-in-out infinite; transform-origin: 50% 0%; }
@keyframes offer-beam-breathe {
  0%, 100% { opacity: 0.82; transform: scaleY(1); }
  50%      { opacity: 1;    transform: scaleY(1.06); }
}
.offer-tile {
  transition: transform 0.22s cubic-bezier(0.22,1,0.36,1), box-shadow 0.22s ease-out, border-color 0.22s ease-out;
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.06), 0 2px 10px rgba(0,0,0,0.35);
}
.offer-tile:hover { transform: translateY(-2px); box-shadow: inset 0 1px 0 rgba(255,255,255,0.1), 0 8px 20px rgba(0,0,0,0.45); }
.reduced-motion .offer-beam { animation: none; }
.reduced-motion .offer-tile { transition: none; }
.reduced-motion .offer-tile:hover { transform: none; }

/* ============================================================
   SLOT ICON IDLE — every settled reel symbol breathes
   ------------------------------------------------------------
   A shallow scale with a touch of sway and a hint of lift. Deliberately small
   (2.5% scale, 1.2deg) — a reel of symbols that visibly bounces reads as broken,
   not premium. Callers stagger it with animationDelay so a board never pulses in
   lockstep.

   This moves the symbol as ONE glyph, which is all a 3-across reel cell wants.
   Motion of the art's own PARTS — the vault handle turning, the dragon breathing
   fire — is a different thing entirely and lives in styles-emblems.css, where it
   is gated to the places the art is the subject: a hovered lobby tile, the crest
   the jackpot capsule throws, and a cell that just won.
   ============================================================ */
@keyframes slot-icon-live {
  0%,  100% { transform: scale(1)      rotate(0deg)    translateY(0); }
  28%       { transform: scale(1.025)  rotate(1.2deg)  translateY(-1.5px); }
  54%       { transform: scale(0.992)  rotate(0deg)    translateY(0.5px); }
  78%       { transform: scale(1.012)  rotate(-1.1deg) translateY(-0.8px); }
}
.slot-icon-live {
  animation: slot-icon-live 3.6s ease-in-out infinite;
  will-change: transform;
}
.reduced-motion .slot-icon-live,
.performance-mode .slot-icon-live { animation: none !important; }

/* ============================================================
   GEM ARCADE — the five games' own motion (ui/ArcadeArt.js)
   ------------------------------------------------------------
   Each game owns a world, and these are the few moving parts that sell it.
   Everything here is short and event-driven — a dial spinning while a crack
   resolves, a gate swinging, a shell popping open — rather than an always-on
   decorative loop, so none of it needs the performance-mode kill switch that
   the ambient animations do. The one genuinely continuous piece (the comet's
   flame flicker) is silenced under reduced-motion below.
   ============================================================ */

/* --- THE VAULT: the dial spins only while a crack is resolving ----------- */
@keyframes aa-dial-spin { to { transform: rotate(1080deg); } }
.aa-cracking .aa-vault-dial { animation: aa-dial-spin 1.5s cubic-bezier(0.25, 0.1, 0.2, 1) both; }
/* The door itself shudders as the bolts throw. */
@keyframes aa-vault-shudder {
  0%, 100% { transform: translate3d(0, 0, 0); }
  20%      { transform: translate3d(-2px, 1px, 0); }
  45%      { transform: translate3d(2px, -1px, 0); }
  70%      { transform: translate3d(-1px, -1px, 0); }
}
.aa-cracking { animation: aa-vault-shudder 0.34s ease-in-out 4; }

/* --- THE DEEP: a shell hinging open, and the sting when an urchin is hit -- */
@keyframes aa-shell-open {
  0%   { transform: scale(0.72) rotate(-8deg); opacity: 0; }
  55%  { transform: scale(1.09) rotate(2deg);  opacity: 1; }
  100% { transform: scale(1)    rotate(0deg);  opacity: 1; }
}
.aa-shell-open { animation: aa-shell-open 0.42s cubic-bezier(0.2, 1.1, 0.35, 1) both; }
@keyframes aa-sting {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  25%      { transform: translate3d(-3px, 0, 0) scale(1.08); }
  60%      { transform: translate3d(3px, 0, 0)  scale(1.05); }
}
.aa-sting { animation: aa-sting 0.4s ease-in-out 2; }
/* Idle drift so the reef bed is never quite still. Staggered off the cell
   index by an inline --d so the whole board doesn't breathe in lockstep. */
@keyframes aa-kelp-sway {
  0%, 100% { transform: translateY(0)      rotate(0deg); }
  50%      { transform: translateY(-2.5px) rotate(1.4deg); }
}
.aa-kelp { animation: aa-kelp-sway 4.2s ease-in-out infinite; animation-delay: var(--d, 0s); }

/* --- THE DYNASTY: the tile flip on every call --------------------------- */
@keyframes aa-tile-flip {
  0%   { transform: perspective(600px) rotateY(-92deg) scale(0.92); opacity: 0; }
  60%  { transform: perspective(600px) rotateY(8deg)   scale(1.03); opacity: 1; }
  100% { transform: perspective(600px) rotateY(0deg)   scale(1);    opacity: 1; }
}
.aa-tile-flip { animation: aa-tile-flip 0.42s cubic-bezier(0.2, 0.9, 0.3, 1) both; }

/* --- DEEP SPACE: the comet's flame, and the burn-out ---------------------- */
@keyframes aa-comet-flicker {
  0%, 100% { transform: translate3d(0, 0, 0) scale(1); }
  30%      { transform: translate3d(1.5px, -1px, 0) scale(1.03); }
  65%      { transform: translate3d(-1px, 1.5px, 0) scale(0.985); }
}
.aa-comet-live { animation: aa-comet-flicker 0.62s ease-in-out infinite; }
@keyframes aa-comet-burn {
  0%   { transform: scale(1)    rotate(0deg);   opacity: 1; filter: brightness(1); }
  35%  { transform: scale(1.35) rotate(-6deg);  opacity: 1; filter: brightness(2.4); }
  100% { transform: scale(0.35) rotate(14deg);  opacity: 0; filter: brightness(0.6); }
}
.aa-comet-burn { animation: aa-comet-burn 0.72s cubic-bezier(0.3, 0, 0.6, 1) both; }
/* The track the multiplier is read off — a thin ion trail that fills. */
.aa-track-fill { transition: width 0.09s linear; }

/* --- THE TOMB: gates swinging in, and the cobra's strike ------------------ */
@keyframes aa-gate-rise {
  0%   { transform: translateY(16px) scale(0.94); opacity: 0; }
  100% { transform: translateY(0)    scale(1);    opacity: 1; }
}
.aa-gate-rise { animation: aa-gate-rise 0.36s cubic-bezier(0.2, 1, 0.35, 1) both; animation-delay: var(--d, 0s); }
@keyframes aa-strike {
  0%   { transform: scale(1)    translateY(0); }
  30%  { transform: scale(1.12) translateY(-6px); }
  55%  { transform: scale(0.98) translateY(2px); }
  100% { transform: scale(1)    translateY(0); }
}
.aa-strike { animation: aa-strike 0.5s cubic-bezier(0.3, 1.4, 0.4, 1) both; }

/* Accessibility: hold every arcade animation still under reduced-motion. The
   games stay fully playable — each one's state is readable from the art and the
   numbers, never from the movement. */
.reduced-motion .aa-cracking,
.reduced-motion .aa-cracking .aa-vault-dial,
.reduced-motion .aa-shell-open,
.reduced-motion .aa-sting,
.reduced-motion .aa-kelp,
.reduced-motion .aa-tile-flip,
.reduced-motion .aa-comet-live,
.reduced-motion .aa-comet-burn,
.reduced-motion .aa-gate-rise,
.reduced-motion .aa-strike { animation: none !important; }
/* Performance mode only silences the always-running one. */
.performance-mode .aa-kelp,
.performance-mode .aa-comet-live { animation: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   STORE (ui/ShopModal.js · CosmeticShop.js · GemFeatures.js · SpecialOffer.js)
   ───────────────────────────────────────────────────────────────────────────
   Everything here is namespaced `sto-`. The store is still at rest: entrances
   and the purchase beat are one-shots, and grids of looping rarity effects
   hold their frame until that item is hovered / focused (.sto-still).
   Transform + opacity only; every keyframe is gated in the guards below.
   ═══════════════════════════════════════════════════════════════════════════ */
@keyframes sto-rise {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}
/* fill-mode backwards (never both): a finished entrance must not pin the
   transform, or the .shop-pack hover lift would silently stop working. */
.sto-rise,
.sto-in { animation: sto-rise 0.32s var(--prm-glide) backwards; }
@keyframes sto-pop {
  0%   { opacity: 0; transform: scale(0.6); }
  60%  { opacity: 1; transform: scale(1.06); }
  100% { opacity: 1; transform: scale(1); }
}
.sto-pop { animation: sto-pop 0.42s var(--prm-pop) backwards; }
/* The purchase beat: one sweep, exactly as long as the beat (--sto-beat). */
@keyframes sto-fill {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
.sto-fill { transform-origin: left center; animation: sto-fill var(--sto-beat, 1.5s) linear forwards; }
/* Purchase splash rays: swing in once, then hold still. */
@keyframes sto-rays {
  from { opacity: 0; transform: translate(-50%, -50%) rotate(-24deg) scale(0.8); }
  to   { opacity: 1; transform: translate(-50%, -50%) rotate(0deg) scale(1); }
}
.sto-rays { transform: translate(-50%, -50%); animation: sto-rays 1.1s var(--prm-glide) backwards; }

/* Pack price chip (desktop pack cards): an outlined chip in the pack's own
   colour (--pk/--pk2 from .shop-pack) that fills on hover and presses in. */
.sto-price {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 46px; border-radius: 12px;
  font-weight: 900; font-size: 1rem; letter-spacing: 0.12em; color: #fff;
  font-variant-numeric: tabular-nums;
  background: linear-gradient(180deg, rgba(255, 255, 255, 0.07), rgba(0, 0, 0, 0.3)), #1c1917;
  box-shadow: inset 0 0 0 1.5px var(--pk2, #fbbf24), inset 0 1px 0 rgba(255, 255, 255, 0.14), 0 4px 0 rgba(0, 0, 0, 0.35);
  transition: transform 0.26s var(--prm-snap);
}
@media (hover: hover) {
  .shop-pack:hover .sto-price {
    transform: translateY(-1px);
    background: linear-gradient(180deg, var(--pk2, #fbbf24), var(--pk, #b45309));
    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55);
  }
}
.shop-pack:active .sto-price { transform: translateY(2px) scale(0.97); }

/* The one PAY / BUY button every confirm card uses (pairs with .prm-tactile). */
.sto-pay {
  background: linear-gradient(180deg, #34d399, #059669 55%, #047857);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.35), 0 4px 0 #065f46, 0 0 22px rgba(16, 185, 129, 0.35);
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.35);
}
.sto-pay:active:not(:disabled) { box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.3), 0 1px 0 #065f46, 0 0 14px rgba(16, 185, 129, 0.3); }

/* Grids whose items carry looping effects (legendary / epic cosmetics, rarity
   badges): each item's loops hold their frame until it is hovered or focused,
   so a tab of legendaries costs nothing at rest. Only what is INSIDE an item
   holds — the item itself keeps any entrance animation it runs. */
.sto-still > :not(:hover):not(:focus-within) *,
.sto-still > :not(:hover):not(:focus-within) *::before,
.sto-still > :not(:hover):not(:focus-within) *::after { animation-play-state: paused !important; }

/* A sideways-scrolling chip row without a scrollbar track. */
.sto-noscroll { scrollbar-width: none; -webkit-overflow-scrolling: touch; }
.sto-noscroll::-webkit-scrollbar { display: none; }

/* Guards */
.reduced-motion .sto-rise,
.reduced-motion .sto-in,
.reduced-motion .sto-pop,
.reduced-motion .sto-rays { animation: none !important; }
.reduced-motion .sto-fill { animation: none !important; transform: none; }
.reduced-motion .sto-confetti,
.performance-mode .sto-confetti { display: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   TOWER OF FORTUNE  (ui/TowerFortune.js)
   ───────────────────────────────────────────────────────────────────────────
   The free daily climb. Everything here is namespaced `tof-`.

   THE ONE RULE THAT SHAPES THIS BLOCK: the card flip is a real 3-D rotation
   (.tof-card-inner carries `transform-style: preserve-3d`), and any `filter`,
   `opacity` or extra `transform` applied to that element — or to the two faces
   inside it — flattens the 3-D context and the flip collapses into a squash.
   So the moving parts are deliberately split across three elements:

     .tof-card        the <button>. Deal-in animation + hover/pick transforms.
     .tof-card-inner  ONLY the flip. Nothing else may touch it.
     .tof-back-sheen  the light travelling over a face-down card (a plain div
                      inside the back face) — that is what makes the deal feel
                      alive without putting a filter anywhere near the 3-D.

   The deal animation uses fill-mode `backwards`, not `both`, on purpose: with
   `both` the finished animation would pin the card transform forever and the
   hover lift / pick lift below would silently never apply.
   ═══════════════════════════════════════════════════════════════════════════ */

/* --- the room: light from above, dust in the air ------------------------- */
.tof-ambient { position: fixed; inset: 0; z-index: 0; }
.tof-motes { position: fixed; inset: 0; z-index: 0; overflow: hidden; }
.tof-motes i {
  position: absolute; left: var(--x, 50%); bottom: -12px;
  width: 3px; height: 3px; border-radius: 50%;
  background: rgba(251, 191, 36, 0.85);
  box-shadow: 0 0 8px rgba(251, 191, 36, 0.7);
  animation: tof-mote var(--s, 7s) linear var(--d, 0s) infinite;
}
@keyframes tof-mote {
  0%   { transform: translate3d(0, 0, 0) scale(0.6); opacity: 0; }
  12%  { opacity: 0.85; }
  85%  { opacity: 0.5; }
  100% { transform: translate3d(14px, -102vh, 0) scale(1.1); opacity: 0; }
}

/* --- THE CARD ------------------------------------------------------------ */
.tof-card {
  position: relative; display: block; width: 100%;
  padding: 0; border: 0; background: none; -webkit-appearance: none; appearance: none;
  perspective: 1100px;
  transition: transform 0.34s cubic-bezier(0.2, 0.9, 0.3, 1), opacity 0.34s ease, filter 0.34s ease;
}
.tof-card-inner {
  position: relative; width: 100%; aspect-ratio: 3 / 4;
  transform-style: preserve-3d;
  transition: transform 0.66s cubic-bezier(0.25, 0.9, 0.25, 1);
}
.tof-card-inner.is-flipped { transform: rotateY(180deg); }
.tof-face {
  position: absolute; inset: 0;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
}
.tof-face-front { transform: rotateY(180deg); }

/* the deal: four cards thrown up onto the floor, staggered */
/* The cards come UP from below with you, overshooting and settling — the same
   direction of travel as the shaft, so the deal reads as arriving at a new
   floor rather than as cards being placed on a table. */
@keyframes tof-deal {
  0%   { transform: translate3d(0, 190px, 0) rotate(var(--r, 0deg)) scale(0.7); opacity: 0; filter: blur(3px); }
  45%  { opacity: 1; filter: blur(0); }
  68%  { transform: translate3d(0, -16px, 0) rotate(0deg) scale(1.06); }
  85%  { transform: translate3d(0, 5px, 0) rotate(0deg) scale(0.99); }
  100% { transform: translate3d(0, 0, 0) rotate(0deg) scale(1); opacity: 1; }
}
.tof-deal { animation: tof-deal 0.66s cubic-bezier(0.18, 0.9, 0.28, 1) backwards; animation-delay: var(--d, 0s); }

/* a face-down card is choosable: it lifts under the cursor, and a band of
   light keeps crossing its back so the board never sits dead */
.tof-live { cursor: pointer; }
.tof-live:hover { transform: translate3d(0, -10px, 0) scale(1.045); }
.tof-live:active { transform: translate3d(0, -4px, 0) scale(1.01); }
.tof-back-sheen { position: absolute; inset: 0; border-radius: 0.75rem; overflow: hidden; opacity: 0; }
.tof-back-sheen::after {
  content: ''; position: absolute; top: -60%; bottom: -60%; width: 42%; left: -60%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.34), transparent);
  transform: skewX(-18deg);
}
.tof-live .tof-back-sheen { opacity: 1; }
.tof-live .tof-back-sheen::after { animation: tof-sheen 2.6s ease-in-out infinite; animation-delay: var(--d, 0s); }
/* Every sheen in the Tower TRANSLATES (it used to animate `left`, a layout
   property, on every card back and button at once). ~190% of the host's
   width, expressed in the ~40%-wide band's own width. */
@keyframes tof-sheen {
  0%       { transform: translateX(0) skewX(-18deg); }
  55%,100% { transform: translateX(470%) skewX(-18deg); }
}

/* the chosen card steps forward; the rest step back */
.tof-picked { transform: translate3d(0, -18px, 0) scale(1.07); z-index: 5; }
.tof-muted  { transform: scale(0.92); opacity: 0.42; filter: grayscale(0.72); }

/* the shine that runs across a card the instant it turns face-up */
.tof-reveal-shine { position: absolute; inset: 0; border-radius: 0.75rem; overflow: hidden; }
.tof-reveal-shine::after {
  content: ''; position: absolute; top: -60%; bottom: -60%; width: 55%; left: -70%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.55), transparent);
  transform: skewX(-16deg);
  animation: tof-reveal-shine 0.85s cubic-bezier(0.25, 0.8, 0.35, 1) 0.42s both;
}
@keyframes tof-reveal-shine { 0% { transform: translateX(0) skewX(-16deg); } 100% { transform: translateX(382%) skewX(-16deg); } }

/* a winning flip: the card punches forward and rings */
@keyframes tof-won {
  0%   { transform: translate3d(0, -18px, 0) scale(1.07); }
  35%  { transform: translate3d(0, -30px, 0) scale(1.16); }
  70%  { transform: translate3d(0, -14px, 0) scale(1.03); }
  100% { transform: translate3d(0, -18px, 0) scale(1.07); }
}
/* NOTE THE tof-deal IN THIS LIST. `animation` is a shorthand, so declaring it
   here USED to drop tof-deal from the element entirely — and the moment the win
   animation was removed again (flash clears ~800ms in, right as the other three
   cards flip) the shorthand reverted to .tof-deal's declaration, which
   re-introduces the animation name and therefore RESTARTS it. The card you'd
   just picked silently replayed its fly-in: it vanished and flew back up from
   below. Keeping tof-deal pinned at index 0 in both lists means removing the
   second animation leaves the first matched by position and untouched. */
.tof-won {
  animation: tof-deal 0.66s cubic-bezier(0.18, 0.9, 0.28, 1) backwards,
             tof-won 0.62s cubic-bezier(0.3, 1.5, 0.4, 1) both;
  animation-delay: var(--d, 0s), 0s;
}

/* the thief: the card snaps toward you and shudders */
@keyframes tof-struck {
  0%   { transform: translate3d(0, -18px, 0) scale(1.07) rotate(0deg); }
  18%  { transform: translate3d(0, -30px, 0) scale(1.22) rotate(-3deg); }
  34%  { transform: translate3d(-8px, -22px, 0) scale(1.14) rotate(3deg); }
  50%  { transform: translate3d(8px, -26px, 0) scale(1.16) rotate(-2deg); }
  70%  { transform: translate3d(-4px, -20px, 0) scale(1.1) rotate(1deg); }
  100% { transform: translate3d(0, -18px, 0) scale(1.08) rotate(0deg); }
}
.tof-struck {
  animation: tof-deal 0.66s cubic-bezier(0.18, 0.9, 0.28, 1) backwards,
             tof-struck 0.75s cubic-bezier(0.3, 0.9, 0.4, 1) both;
  animation-delay: var(--d, 0s), 0s;
}
.tof-thief-word { animation: tof-thief-word 0.9s ease-in-out infinite; }
@keyframes tof-thief-word { 0%, 100% { opacity: 1; } 50% { opacity: 0.45; } }

/* coins thrown out of a winning card */
.tof-burst { position: absolute; inset: 0; z-index: 6; }
.tof-burst i {
  position: absolute; left: 50%; top: 50%; width: 10px; height: 10px; margin: -5px 0 0 -5px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fef3c7, #f59e0b 58%, #b45309);
  box-shadow: 0 0 9px rgba(251, 191, 36, 0.9);
  animation: tof-burst 0.9s cubic-bezier(0.2, 0.8, 0.3, 1) var(--dd, 0s) both;
}
@keyframes tof-burst {
  0%   { transform: rotate(var(--a, 0deg)) translateY(0) scale(0.3); opacity: 0; }
  16%  { opacity: 1; }
  100% { transform: rotate(var(--a, 0deg)) translateY(-128px) scale(0.85); opacity: 0; }
}

/* coins dragged off the board by the thief */
.tof-steal { position: absolute; inset: 0; z-index: 6; }
.tof-steal i {
  position: absolute; left: 50%; top: 18%; width: 12px; height: 12px; margin-left: -6px;
  border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fef3c7, #f59e0b 58%, #92400e);
  box-shadow: 0 0 8px rgba(251, 191, 36, 0.55);
  animation: tof-steal 1s cubic-bezier(0.45, 0, 0.75, 1) var(--dd, 0s) both;
}
@keyframes tof-steal {
  0%   { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); opacity: 1; }
  100% { transform: translate3d(var(--tx, 0), 150px, 0) scale(0.28) rotate(220deg); opacity: 0; }
}

/* --- the board reacting -------------------------------------------------- */
@keyframes tof-quake {
  0%, 100% { transform: translate3d(0, 0, 0); }
  15% { transform: translate3d(-9px, 4px, 0); }
  30% { transform: translate3d(8px, -5px, 0); }
  45% { transform: translate3d(-6px, -3px, 0); }
  60% { transform: translate3d(6px, 4px, 0); }
  80% { transform: translate3d(-3px, 1px, 0); }
}
.tof-quake { animation: tof-quake 0.44s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; }
.tof-board-lost { filter: grayscale(0.55) brightness(0.72); transition: filter 0.6s ease; }

.tof-prompt { animation: tof-prompt 1.8s ease-in-out infinite; }
@keyframes tof-prompt { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }

.tof-gain { animation: tof-gain 1.1s cubic-bezier(0.2, 0.9, 0.3, 1) both; display: inline-block; }
@keyframes tof-gain {
  0%   { transform: translateY(16px) scale(0.7); opacity: 0; }
  25%  { transform: translateY(0) scale(1.18); opacity: 1; }
  45%  { transform: translateY(0) scale(1); }
  100% { transform: translateY(-10px) scale(1); opacity: 0.92; }
}
.tof-robbed { animation: tof-robbed 0.5s cubic-bezier(0.3, 1.6, 0.4, 1) both; display: inline-block; }
@keyframes tof-robbed {
  0%   { transform: scale(2.4) rotate(-9deg); opacity: 0; filter: blur(7px); }
  60%  { transform: scale(0.94) rotate(2deg); opacity: 1; filter: blur(0); }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}

/* the haul readout: swells when it grows, bleeds when it is taken */
.tof-haul-up { animation: tof-haul-up 0.6s cubic-bezier(0.3, 1.4, 0.4, 1) both; }
@keyframes tof-haul-up {
  0% { transform: scale(1); } 40% { transform: scale(1.11); } 100% { transform: scale(1); }
}
.tof-haul-robbed { animation: tof-haul-robbed 0.7s ease-in-out both; }
@keyframes tof-haul-robbed {
  0%, 100% { transform: translateX(0); }
  20% { transform: translateX(-8px); } 40% { transform: translateX(7px); }
  60% { transform: translateX(-5px); } 80% { transform: translateX(3px); }
}

/* --- the vault-floor announcement ---------------------------------------- */
.tof-banner { animation: tof-banner 1.6s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes tof-banner {
  0%   { transform: translate3d(-120%, 0, 0) skewX(-14deg); opacity: 0; }
  18%  { transform: translate3d(0, 0, 0) skewX(0deg); opacity: 1; }
  75%  { transform: translate3d(0, 0, 0) skewX(0deg); opacity: 1; }
  100% { transform: translate3d(0, -26px, 0) scale(0.94); opacity: 0; }
}

/* --- the tower spine ----------------------------------------------------- */
.tof-floor-live { animation: tof-floor-live 1.9s ease-in-out infinite; }
@keyframes tof-floor-live {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.035); }
}

/* --- the two buttons the whole game hangs on ----------------------------- */
.tof-btn { position: relative; overflow: hidden; border: 0; }
.tof-btn::after {
  content: ''; position: absolute; top: -60%; bottom: -60%; width: 40%; left: -60%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.45), transparent);
  transform: skewX(-18deg); animation: tof-sheen 3.4s ease-in-out infinite;
}
.tof-btn-bank  { background: linear-gradient(to bottom, #e7e5e4, #a8a29e); box-shadow: 0 5px 0 rgba(0, 0, 0, 0.5); }
/* A steady glow — the travelling sheen (::after, transform) is the life. The
   old "hum" animated box-shadow: a repaint of the button every frame. */
.tof-btn-climb,
.tof-btn-start { background: linear-gradient(to bottom, #fde68a, #f59e0b); box-shadow: 0 5px 0 rgba(120, 53, 15, 0.9), 0 0 34px rgba(245, 158, 11, 0.5); }
.tof-choices { animation: tof-choices 0.42s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes tof-choices {
  0%   { transform: translateY(22px); opacity: 0; }
  100% { transform: translateY(0); opacity: 1; }
}

/* --- intro / resume ------------------------------------------------------ */
.tof-intro-tower { animation: tof-intro-tower 3.4s ease-in-out infinite; }
@keyframes tof-intro-tower {
  0%, 100% { transform: translateY(0) rotate(-1deg); }
  50%      { transform: translateY(-12px) rotate(1deg); }
}

/* --- ROBBED: the ending nobody wants ------------------------------------- */
.tof-bust-glow {
  background: radial-gradient(60% 55% at 50% 45%, rgba(244, 63, 94, 0.32), transparent 68%);
  animation: tof-bust-glow 0.9s ease-out both;
}
@keyframes tof-bust-glow { 0% { opacity: 0; transform: scale(1.5); } 100% { opacity: 1; transform: scale(1); } }
.tof-bust-in { animation: tof-bust-in 0.5s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes tof-bust-in { 0% { transform: translateY(26px); opacity: 0; } 100% { transform: translateY(0); opacity: 1; } }
.tof-thief-loom { animation: tof-thief-loom 2.6s ease-in-out infinite; filter: drop-shadow(0 0 26px rgba(244, 63, 94, 0.55)); }
@keyframes tof-thief-loom {
  0%, 100% { transform: scale(1) rotate(-1.5deg); }
  50%      { transform: scale(1.06) rotate(1.5deg); }
}
.tof-stamp { animation: tof-stamp 0.6s cubic-bezier(0.3, 1.5, 0.4, 1) 0.15s both; text-shadow: 0 0 34px rgba(244, 63, 94, 0.75); }
@keyframes tof-stamp {
  0%   { transform: scale(3) rotate(-12deg); opacity: 0; filter: blur(10px); }
  55%  { transform: scale(0.92) rotate(3deg); opacity: 1; filter: blur(0); }
  100% { transform: scale(1) rotate(0deg); opacity: 1; }
}

/* --- BANKED: the ending everybody wants ---------------------------------- */
.tof-bank-in { animation: tof-bank-in 0.5s cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes tof-bank-in { 0% { transform: translateY(30px) scale(0.94); opacity: 0; } 100% { transform: translateY(0) scale(1); opacity: 1; } }
.tof-bank-total { animation: tof-bank-total 0.8s cubic-bezier(0.25, 1.5, 0.4, 1) 0.1s both; filter: drop-shadow(0 0 30px rgba(255, 255, 255, 0.35)); }
@keyframes tof-bank-total {
  0%   { transform: scale(0.4); opacity: 0; }
  55%  { transform: scale(1.12); opacity: 1; }
  100% { transform: scale(1); opacity: 1; }
}
.tof-crown-word { animation: tof-crown-word 1.6s ease-in-out infinite; }
@keyframes tof-crown-word {
  0%, 100% { opacity: 0.7; letter-spacing: 0.42em; }
  50%      { opacity: 1;   letter-spacing: 0.5em; }
}
.tof-confetti { position: absolute; inset: 0; overflow: hidden; }
.tof-confetti i {
  position: absolute; left: var(--x, 50%); top: -24px;
  width: 8px; height: 14px; border-radius: 2px; background: var(--c, #fbbf24);
  animation: tof-confetti 2.6s linear var(--d, 0s) infinite;
}
@keyframes tof-confetti {
  0%   { transform: translate3d(0, 0, 0) rotate(0deg); opacity: 0; }
  10%  { opacity: 1; }
  100% { transform: translate3d(28px, 104vh, 0) rotate(760deg); opacity: 0.9; }
}

/* --- ACCESSIBILITY -------------------------------------------------------
   Under Reduced Motion the tower stays fully playable: the flip becomes an
   instant cut and every decorative loop stands down. A card's state is read
   from its art and its number, never from the movement. */
.reduced-motion .tof-motes,
.reduced-motion .tof-confetti,
.reduced-motion .tof-back-sheen,
.reduced-motion .tof-reveal-shine,
.reduced-motion .tof-btn::after { display: none !important; }
.reduced-motion .tof-card-inner { transition: none !important; }
.reduced-motion .tof-deal,
.reduced-motion .tof-won,
.reduced-motion .tof-struck,
.reduced-motion .tof-quake,
.reduced-motion .tof-banner,
.reduced-motion .tof-floor-live,
.reduced-motion .tof-btn-climb,
.reduced-motion .tof-btn-start,
.reduced-motion .tof-choices,
.reduced-motion .tof-intro-tower,
.reduced-motion .tof-thief-loom,
.reduced-motion .tof-thief-word,
.reduced-motion .tof-prompt,
.reduced-motion .tof-stamp,
.reduced-motion .tof-robbed,
.reduced-motion .tof-gain,
.reduced-motion .tof-haul-up,
.reduced-motion .tof-haul-robbed,
.reduced-motion .tof-bank-total,
.reduced-motion .tof-crown-word,
.reduced-motion .tof-burst i,
.reduced-motion .tof-steal i { animation: none !important; }
.reduced-motion .tof-live:hover { transform: none; }
/* Performance mode only silences the always-running loops. */
.performance-mode .tof-motes,
.performance-mode .tof-confetti { display: none !important; }
.performance-mode .tof-back-sheen::after,
.performance-mode .tof-btn::after,
.performance-mode .tof-floor-live,
.performance-mode .tof-thief-loom,
.performance-mode .tof-intro-tower { animation: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   WHEEL FRAME — stop the rim being sliced square
   ───────────────────────────────────────────────────────────────────────────
   Every wheel skin draws its rim into a `viewBox="0 0 200 200"`, which means
   the furthest a shape can reach from the centre and still be inside the box
   is exactly r=100. The rims all draw PAST that on purpose — the outer dark
   edge sits at r=101 with a 3-wide stroke (so r=102.5), and the gold bevel
   ring above it at r=102.5 — because those rings are meant to read as the
   frame standing proud of the wheel.

   An <svg> defaults to `overflow: hidden`. So those outer rings were being
   clipped flat against the four sides of the viewBox, which is why the wheel
   lost its rim at the top, the bottom and both sides while keeping it on the
   diagonals (where a circle sits comfortably inside a square). The wheel read
   as squared-off rather than round.

   One rule fixes every skin, present and future: nothing inside a wheel frame
   clips at its own viewBox. It is safe to apply broadly — the slice SVG that
   spins inside is clipped by its `rounded-full overflow-hidden` PARENT, not
   by its own box, so releasing the SVG-level clip changes nothing there. */
.wheel-frame svg { overflow: visible; }

/* ═══════════════════════════════════════════════════════════════════════════
   DAILY WHEEL — THE TESLA WHEEL (ui/DailyWheel.js)            [ew-* classes]
   ───────────────────────────────────────────────────────────────────────────
   The daily wheel in the Daily Circuit's electric language: navy-black field,
   cyan/blue/violet plasma, white-hot cores. Everything here is scoped to
   ew-* classes (the shared .wheel-frame rule above also serves the slot bonus
   wheels, so nothing below touches it).

   Motion contract — only transform and opacity move, always on HTML boxes:
   · The rotor, the bolt pointer's kick, the hub's plasma ring and the LED
     chase WHILE SPINNING are written by the wheel's frame loop (JS), not CSS.
   · Rim arcs are SVG art inside a wrapper div whose OPACITY strikes.
   · Idle loops run a few iterations on open and then rest lit; the spin-time
     loops only run for the ~13s spin (.ew-stage[data-ew-phase]).
   · .reduced-motion silences every decorative animation (the spin itself is
     the content and still runs); .performance-mode silences the loops but
     keeps the brief landing one-shots.
   ═══════════════════════════════════════════════════════════════════════════ */
.ew-screen { color-scheme: dark; }
.ew-grid {
  position: absolute; inset: 0; min-height: 100%; pointer-events: none;
  background-image:
    linear-gradient(rgba(34, 211, 238, 0.055) 1px, transparent 1px),
    linear-gradient(90deg, rgba(34, 211, 238, 0.055) 1px, transparent 1px);
  background-size: 46px 46px;
  background-position: center center;
  -webkit-mask-image: radial-gradient(75% 65% at 50% 60%, #000 15%, transparent 78%);
  mask-image: radial-gradient(75% 65% at 50% 60%, #000 15%, transparent 78%);
}
.ew-close {
  background: rgba(8, 16, 32, 0.88);
  border: 1px solid rgba(34, 211, 238, 0.35);
  box-shadow: 0 0 14px rgba(34, 211, 238, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.ew-close:hover { border-color: #67e8f9; background: rgba(14, 42, 68, 0.95); }

/* ── title: white-hot → cyan neon, ignites with a flicker on open ── */
.ew-title {
  font-family: Oswald, 'Arial Narrow', sans-serif;
  font-weight: 700;
  color: transparent;
  background: linear-gradient(180deg, #ffffff 0%, #ecfeff 36%, #67e8f9 70%, #22d3ee 100%);
  -webkit-background-clip: text;
  background-clip: text;
  filter: drop-shadow(0 0 10px rgba(34, 211, 238, 0.6)) drop-shadow(0 0 26px rgba(59, 130, 246, 0.4));
  padding: 0.04em 0.02em;
}
.ew-title-bolt { filter: drop-shadow(0 0 6px rgba(103, 232, 249, 0.9)); flex: none; }
@keyframes ew-ignite {
  0%   { opacity: 0; }
  7%   { opacity: 0.9; }
  10%  { opacity: 0.15; }
  15%  { opacity: 1; }
  19%  { opacity: 0.3; }
  24%  { opacity: 1; }
  44%  { opacity: 0.75; }
  47%  { opacity: 1; }
  100% { opacity: 1; }
}
.ew-title-row { animation: ew-ignite 1.4s linear 0.1s both; will-change: opacity; }
/* the narrowest phones: shrink the title so it clears the ✕ in the corner */
@media (max-width: 359px) {
  .ew-title { font-size: 1.6rem; letter-spacing: 0.05em; }
  .ew-title-row { gap: 0.3rem; padding-right: 1.75rem; }
}

.ew-chip {
  background: linear-gradient(180deg, rgba(12, 26, 50, 0.92), rgba(5, 9, 18, 0.92));
  border: 1px solid rgba(34, 211, 238, 0.32);
  box-shadow: 0 0 18px rgba(34, 211, 238, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.06);
}
.ew-chip-sep { width: 1px; height: 14px; background: rgba(103, 232, 249, 0.35); }

/* ── streak ladder: charge cells ── */
.ew-cell {
  background: rgba(8, 15, 30, 0.72);
  border: 1px solid rgba(51, 65, 85, 0.75);
  transition: transform 0.2s ease;
}
.ew-cell.is-done { border-color: rgba(56, 189, 248, 0.45); background: rgba(14, 40, 70, 0.5); }
.ew-cell.is-current {
  border-color: #67e8f9;
  background: linear-gradient(180deg, rgba(34, 211, 238, 0.3), rgba(14, 116, 144, 0.16));
  box-shadow: 0 0 16px rgba(34, 211, 238, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.22);
  transform: scale(1.06);
}

/* ── circuit traces feeding the wheel (static SVG + a few sparks) ── */
.ew-traces {
  position: absolute; left: 50%; top: 50%;
  width: 1800px; height: 520px;
  transform: translate(-50%, -50%) scale(0.76);
  pointer-events: none;
}
.ew-traces svg { display: block; }
@media (min-width: 640px) { .ew-traces { transform: translate(-50%, -50%); } }
.ew-spark {
  position: absolute; width: 28px; height: 3px; opacity: 0;
  will-change: transform, opacity;
  animation: ew-spark-run 2.8s cubic-bezier(0.55, 0, 0.85, 0.6) 3 both;
}
.ew-spark i {
  display: block; width: 100%; height: 100%; border-radius: 3px;
  background: linear-gradient(90deg, transparent, rgba(103, 232, 249, 0.85) 60%, #ffffff);
  box-shadow: 0 0 8px rgba(103, 232, 249, 0.9);
}
.ew-spark i.is-flip { transform: scaleX(-1); }
@keyframes ew-spark-run {
  0%       { transform: translateX(0); opacity: 0; }
  8%       { opacity: 1; }
  52%      { transform: translateX(var(--ew-dx, 0px)); opacity: 1; }
  56%, 100% { transform: translateX(var(--ew-dx, 0px)); opacity: 0; }
}
.ew-traces[data-ew-phase="spin"] .ew-spark { animation: ew-spark-run 1s cubic-bezier(0.55, 0, 0.85, 0.6) infinite both; }
.ew-traces[data-ew-phase="landed"] .ew-spark { animation: none; opacity: 0; }

/* ── the stage: halo, tesla ring, LEDs, disc ── */
.ew-stage { --ew-ring: 18px; }
@media (min-width: 640px) { .ew-stage { --ew-ring: 24px; } }
.ew-halo {
  position: absolute; inset: -10%; border-radius: 50%; pointer-events: none;
  background:
    radial-gradient(closest-side, rgba(34, 211, 238, 0.30), rgba(59, 130, 246, 0.12) 60%, transparent 100%),
    radial-gradient(closest-side, var(--ew-tier-glow, transparent), transparent 100%);
}
/* HUD corner brackets framing the wheel (static) */
.ew-hud {
  position: absolute; inset: -22px; pointer-events: none;
  --c: rgba(103, 232, 249, 0.55);
  background:
    linear-gradient(var(--c), var(--c)) top left / 22px 2px no-repeat,
    linear-gradient(var(--c), var(--c)) top left / 2px 22px no-repeat,
    linear-gradient(var(--c), var(--c)) top right / 22px 2px no-repeat,
    linear-gradient(var(--c), var(--c)) top right / 2px 22px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom left / 22px 2px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom left / 2px 22px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom right / 22px 2px no-repeat,
    linear-gradient(var(--c), var(--c)) bottom right / 2px 22px no-repeat;
}
@media (min-width: 640px) { .ew-hud { inset: -34px; } }
.ew-frame { z-index: 1; }
.ew-ring {
  position: absolute; inset: 0; border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%, transparent 60%, rgba(103, 232, 249, 0.10) 70%, transparent 72%),
    conic-gradient(from 20deg, #0a1222, #1f2d48 12%, #0a1222 25%, #25365a 38%, #0a1222 50%, #1f2d48 62%, #0a1222 75%, #25365a 88%, #0a1222);
  box-shadow:
    0 0 0 1.5px rgba(103, 232, 249, 0.7),
    0 0 0 4px #020617,
    0 0 0 5.5px rgba(56, 189, 248, 0.35),
    0 0 30px 4px rgba(34, 211, 238, 0.45),
    0 0 90px rgba(59, 130, 246, 0.35),
    inset 0 2px 0 rgba(255, 255, 255, 0.10);
}
.ew-ring::after {
  content: ""; position: absolute; inset: calc(var(--ew-ring) - 3px); border-radius: 50%;
  border: 1.5px solid rgba(103, 232, 249, 0.8);
  box-shadow: 0 0 10px rgba(34, 211, 238, 0.75), inset 0 0 12px rgba(34, 211, 238, 0.55);
}
.ew-led-arm { position: absolute; inset: 0; pointer-events: none; }
.ew-led {
  position: absolute; left: 50%; top: calc(var(--ew-ring) / 2);
  width: 8px; height: 8px; margin: -4px 0 0 -4px; border-radius: 50%;
  background: radial-gradient(circle at 40% 35%, #ffffff, #a5f3fc 45%, #0891b2 88%);
  box-shadow: 0 0 6px 2px rgba(103, 232, 249, 0.85), 0 0 14px rgba(34, 211, 238, 0.55);
  opacity: 0.7;
  will-change: opacity;
  animation: ew-led-idle 2.4s ease-in-out 4 both;
}
.ew-led-alt {
  background: radial-gradient(circle at 40% 35%, #ffffff, #c7d2fe 45%, #6366f1 88%);
  box-shadow: 0 0 6px 2px rgba(165, 180, 252, 0.8), 0 0 14px rgba(129, 140, 248, 0.5);
}
@media (min-width: 640px) { .ew-led { width: 10px; height: 10px; margin: -5px 0 0 -5px; } }
/* idle: a slow wave runs round the ring four times, then it rests lit */
@keyframes ew-led-idle {
  0%   { opacity: 0.7; }
  10%  { opacity: 1; }
  28%  { opacity: 0.25; }
  64%  { opacity: 0.45; }
  100% { opacity: 0.7; }
}
/* spinning: the frame loop writes each LED's opacity (a chase that rides the rotor) */
.ew-stage[data-ew-phase="spin"] .ew-led { animation: none; }
/* landed: every bulb pulses in unison (a soft ~1.7Hz swell, not a hard
   strobe), then holds lit */
@keyframes ew-led-win {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.4; }
}
.ew-stage[data-ew-phase="landed"] .ew-led { animation: ew-led-win 0.6s ease-in-out 0s 4 both; }

.ew-disc {
  position: absolute; inset: var(--ew-ring); border-radius: 50%; overflow: hidden;
  background: #020617; isolation: isolate;
  box-shadow: 0 0 0 2px #020617;
}
.ew-rotor { will-change: transform; }
/* the rotor's 15°-periodic detail (dividers, rim studs) on its own layer: the
   frame loop fades it at speed, where it would strobe */
.ew-rotor-fine { position: absolute; inset: 0; will-change: opacity; }
/* the speed smear: what the rim looks like blurred by a fast spin — the studs
   become one bright ring, the labels a pale band. Round, so it doesn't turn;
   the frame loop fades it in with speed (opacity only). */
.ew-smear {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none; opacity: 0;
  will-change: opacity;
  background: radial-gradient(closest-side,
    transparent 28%, rgba(59, 130, 246, 0.10) 36%,
    rgba(226, 232, 240, 0.12) 56%, rgba(226, 232, 240, 0.17) 63%, rgba(226, 232, 240, 0.12) 70%,
    rgba(59, 130, 246, 0.10) 82%, rgba(56, 189, 248, 0.30) 90%,
    rgba(165, 243, 252, 0.55) 93.5%, rgba(236, 254, 255, 0.85) 95%, rgba(165, 243, 252, 0.55) 96.5%,
    rgba(56, 189, 248, 0.30) 98.5%, transparent 100%);
}
.ew-win { position: absolute; inset: 0; pointer-events: none; animation: ew-win-blink 1.1s ease-out both; }
@keyframes ew-win-blink {
  0%   { opacity: 0; }
  8%   { opacity: 1; }
  24%  { opacity: 0.6; }
  44%, 100% { opacity: 1; }
}
.ew-glass {
  position: absolute; inset: 0; border-radius: 50%; pointer-events: none;
  background:
    radial-gradient(110% 60% at 32% 6%, rgba(255, 255, 255, 0.13), transparent 46%),
    radial-gradient(closest-side, transparent 86%, rgba(2, 6, 23, 0.5));
  box-shadow: inset 0 0 18px rgba(2, 6, 23, 0.85), inset 0 0 0 1px rgba(103, 232, 249, 0.3);
}

/* ── plasma-core SPIN hub ── */
.ew-hub {
  background: #04121f; border: 0; padding: 0; overflow: hidden;
  box-shadow:
    0 0 0 3px #020617,
    0 0 0 4.5px rgba(103, 232, 249, 0.75),
    0 0 26px rgba(34, 211, 238, 0.65),
    0 10px 24px rgba(0, 0, 0, 0.7);
}
.ew-plasma-drive { position: absolute; inset: 0; border-radius: 50%; will-change: transform; }
.ew-plasma {
  position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from 0deg,
    rgba(34, 211, 238, 0) 0deg, #67e8f9 40deg, #ffffff 58deg, rgba(99, 102, 241, 0.85) 92deg,
    rgba(34, 211, 238, 0) 140deg, rgba(34, 211, 238, 0) 180deg, #818cf8 220deg, #ecfeff 240deg,
    rgba(34, 211, 238, 0.75) 272deg, rgba(34, 211, 238, 0) 320deg);
  -webkit-mask: radial-gradient(closest-side, transparent 70%, #000 75%, #000 96%, transparent 100%);
  mask: radial-gradient(closest-side, transparent 70%, #000 75%, #000 96%, transparent 100%);
  will-change: transform;
  animation: ew-plasma-spin 3.2s linear 4; /* a few turns on open, then it rests lit */
}
@keyframes ew-plasma-spin { to { transform: rotate(360deg); } }
.ew-hub.is-live .ew-plasma { animation: none; }
.ew-hub-core {
  position: absolute; inset: 15%; border-radius: 50%;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  background: radial-gradient(circle at 50% 36%, #0f2e4a, #04121f 72%);
  box-shadow: inset 0 0 12px rgba(34, 211, 238, 0.45), inset 0 0 0 1px rgba(103, 232, 249, 0.5);
}
.ew-hub.is-live .ew-hub-core {
  box-shadow: inset 0 0 20px rgba(103, 232, 249, 0.85), inset 0 0 0 1px rgba(236, 254, 255, 0.75);
}
.ew-hub-text {
  font-family: Oswald, 'Arial Narrow', sans-serif;
  color: #ecfeff;
  text-shadow: 0 0 8px rgba(103, 232, 249, 0.95), 0 0 18px rgba(34, 211, 238, 0.7);
  padding-left: 0.1em; /* optically centre the tracked word */
}
.ew-hub-bolt { filter: drop-shadow(0 0 4px rgba(103, 232, 249, 0.95)); }

/* ── bolt pointer (the frame loop kicks .ew-pointer; the glow is on the SVG) ── */
.ew-pointer-pos {
  position: absolute; left: 50%; top: -19px; z-index: 30;
  width: 32px; height: 52px; margin-left: -16px; pointer-events: none;
}
@media (min-width: 640px) { .ew-pointer-pos { top: -24px; width: 40px; height: 64px; margin-left: -20px; } }
.ew-pointer { width: 100%; height: 100%; transform-origin: 52% 12%; will-change: transform; }
.ew-pointer svg { filter: drop-shadow(0 0 6px rgba(103, 232, 249, 0.9)) drop-shadow(0 3px 3px rgba(0, 0, 0, 0.7)); }

/* ── rim arcs: jagged forked lightning that FLASHES ── */
.ew-arc {
  position: absolute; inset: -10%; pointer-events: none; opacity: 0; will-change: opacity; z-index: 2;
}
.ew-arc svg { overflow: visible; }
.ew-arc-0 { --ew-di: -3.9s; --ew-sd: 0.86s; --ew-sdl: 0s; }
.ew-arc-1 { --ew-di: -1.6s; --ew-sd: 1.24s; --ew-sdl: -0.4s; }
.ew-arc-2 { --ew-di: -2.8s; --ew-sd: 1.05s; --ew-sdl: -0.7s; }
.ew-arc-3 { --ew-di: -0.5s; --ew-sd: 0.72s; --ew-sdl: -0.2s; }
/* near-instant attack, ragged double hit, exponential decay */
@keyframes ew-strike {
  0%, 60%  { opacity: 0; }
  60.6%    { opacity: 1; }
  62.5%    { opacity: 0.15; }
  64%      { opacity: 1; }
  66.5%    { opacity: 0.4; }
  68%      { opacity: 0.9; }
  72%      { opacity: 0.28; }
  80%, 100% { opacity: 0; }
}
@keyframes ew-strike-fast {
  0%        { opacity: 0; }
  4%        { opacity: 1; }
  10%       { opacity: 0.18; }
  15%       { opacity: 0.95; }
  24%       { opacity: 0.35; }
  38%, 100% { opacity: 0; }
}
@keyframes ew-strike-once {
  0%   { opacity: 0; }
  4%   { opacity: 1; }
  12%  { opacity: 0.25; }
  20%  { opacity: 1; }
  45%  { opacity: 0.4; }
  100% { opacity: 0; }
}
.ew-stage[data-ew-phase="idle"] .ew-arc { animation: ew-strike 4.4s linear var(--ew-di, 0s) 3 both; }
.ew-stage[data-ew-phase="spin"] .ew-arc { animation: ew-strike-fast var(--ew-sd, 1s) linear var(--ew-sdl, 0s) infinite both; }
.ew-stage[data-ew-phase="landed"] .ew-arc { animation: ew-strike-once 1.2s linear 0s 1 both; }

/* ── the landing strike: ONE soft flash from the wheel (one-shot) — a single
   peak at half strength, no re-strike, so the landing never strobes ── */
.ew-flash {
  position: fixed; inset: 0; z-index: 150; pointer-events: none; opacity: 0;
  background: radial-gradient(52vmax 52vmax at 50% 58%, rgba(255, 255, 255, 0.9), rgba(165, 243, 252, 0.45) 32%, rgba(34, 211, 238, 0.1) 62%, transparent 80%);
  will-change: opacity;
  animation: ew-flash 0.8s ease-out both;
}
@keyframes ew-flash {
  0%   { opacity: 0; }
  7%   { opacity: 0.5; }
  100% { opacity: 0; }
}

/* ── celebration ── */
.ew-cel-rays {
  position: absolute; left: 50%; top: 50%;
  width: 170vmax; height: 170vmax; margin: -85vmax 0 0 -85vmax; border-radius: 50%;
  pointer-events: none;
  background: repeating-conic-gradient(from 0deg, rgba(103, 232, 249, 0.09) 0deg 1.6deg, transparent 1.6deg 12deg);
  -webkit-mask: radial-gradient(closest-side, #000 4%, transparent 46%);
  mask: radial-gradient(closest-side, #000 4%, transparent 46%);
  will-change: transform;
  animation: ew-rays-turn 14s linear both;
}
@keyframes ew-rays-turn { from { transform: rotate(0deg); } to { transform: rotate(80deg); } }
.ew-cel-shock {
  position: absolute; left: 50%; top: 50%;
  width: 60vmin; height: 60vmin; margin: -30vmin 0 0 -30vmin; border-radius: 50%;
  border: 3px solid rgba(165, 243, 252, 0.85);
  box-shadow: 0 0 40px rgba(34, 211, 238, 0.7), inset 0 0 40px rgba(34, 211, 238, 0.5);
  pointer-events: none; opacity: 0;
  animation: ew-shock 1.1s cubic-bezier(0.2, 0.7, 0.3, 1) 0.05s both;
}
@keyframes ew-shock {
  0%   { opacity: 0.95; transform: scale(0.2); }
  100% { opacity: 0; transform: scale(2.4); }
}
.ew-cel-emblem { width: 92px; height: 92px; }
@media (min-width: 640px) { .ew-cel-emblem { width: 124px; height: 124px; } }
.ew-cel-core {
  position: absolute; inset: 0; border-radius: 50%; overflow: hidden;
  background: #04121f;
  box-shadow: 0 0 0 3px #020617, 0 0 0 4.5px rgba(103, 232, 249, 0.8), 0 0 40px rgba(34, 211, 238, 0.75), 0 0 90px rgba(59, 130, 246, 0.45);
}
.ew-cel-plasma {
  position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from 30deg, #22d3ee, #ffffff 12%, #818cf8 30%, #22d3ee 48%, #ecfeff 60%, #6366f1 78%, #22d3ee);
  -webkit-mask: radial-gradient(closest-side, transparent 68%, #000 73%, #000 96%, transparent 100%);
  mask: radial-gradient(closest-side, transparent 68%, #000 73%, #000 96%, transparent 100%);
  will-change: transform;
  animation: ew-plasma-spin 1.6s linear 3;
}
.ew-cel-core-in {
  position: absolute; inset: 17%; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle at 50% 40%, #134063, #04121f 72%);
  box-shadow: inset 0 0 18px rgba(103, 232, 249, 0.7), inset 0 0 0 1px rgba(236, 254, 255, 0.6);
}
.ew-cel-core-bolt { width: 40%; height: 64%; filter: drop-shadow(0 0 8px rgba(165, 243, 252, 0.95)); }
.ew-cel-arc {
  position: absolute; inset: -26%;
  pointer-events: none; opacity: 0; will-change: opacity;
  animation: ew-strike-fast 1.2s linear var(--ew-cd, 0s) 4 both;
}
.ew-cel-arc svg { overflow: visible; }
.ew-cel-arc-0 { --ew-cd: 0.15s; }
.ew-cel-arc-1 { --ew-cd: 0.5s; }
.ew-cel-arc-2 { --ew-cd: 0.3s; }
.ew-cel-arc-3 { --ew-cd: 0.8s; }
.ew-cel-amount {
  /* phones: as big as fits $10,000,000 (sm:/md:/lg: sizes take over above) */
  font-size: clamp(2.4rem, 13vw, 3.25rem); line-height: 1;
  /* cyan glow + the VIP tier's colour underneath (the fill itself stays cyan) */
  filter: drop-shadow(0 0 20px rgba(103, 232, 249, 0.55)) drop-shadow(0 0 2px rgba(255, 255, 255, 0.6)) drop-shadow(0 6px 22px var(--ew-cel-tier, transparent));
  padding: 0 0.05em;
}
/* a jackpot (0.5–2% of spins) gets a bigger core and brighter rays */
.ew-celebrate.is-jackpot .ew-cel-emblem { width: 124px; height: 124px; }
@media (min-width: 640px) { .ew-celebrate.is-jackpot .ew-cel-emblem { width: 164px; height: 164px; } }
.ew-celebrate.is-jackpot .ew-cel-rays {
  background: repeating-conic-gradient(from 0deg, rgba(165, 243, 252, 0.2) 0deg 2deg, transparent 2deg 10deg);
}
.ew-jackpot-tag {
  color: #04121f;
  background: linear-gradient(180deg, #ecfeff, #67e8f9 45%, #22d3ee 60%, #0891b2);
  box-shadow: 0 3px 0 #0e4f66, 0 0 22px rgba(34, 211, 238, 0.7), 0 0 60px var(--ew-cel-tier, transparent);
}

/* short windows (a 1024x768 laptop): tighter stage padding so the whole wheel
   screen, HUD brackets included, fits without scrolling */
@media (max-height: 820px) {
  .ew-screen .ew-stage-row { padding-top: 1.75rem; padding-bottom: 1.75rem; }
}

/* The Daily Circuit (hub / phone Today) stays mounted under this opaque
   screen: freeze its loops while the wheel is open (html.ew-open). */
.ew-open .daily-hub-card,
.ew-open .daily-hub-card *,
.ew-open .daily-hub-card *::before,
.ew-open .daily-hub-card *::after,
.ew-open .tdy-card *,
.ew-open .tdy-card *::before,
.ew-open .tdy-card *::after { animation-play-state: paused !important; }

/* ── guards ── */
.reduced-motion .ew-title-row,
.reduced-motion .ew-led,
.reduced-motion .ew-plasma,
.reduced-motion .ew-win,
.reduced-motion .ew-cel-rays,
.reduced-motion .ew-cel-plasma { animation: none !important; }
.reduced-motion .ew-led { opacity: 0.8; }
.reduced-motion .ew-spark,
.reduced-motion .ew-arc,
.reduced-motion .ew-flash,
.reduced-motion .ew-cel-shock,
.reduced-motion .ew-cel-arc { display: none !important; }
.performance-mode .ew-spark { display: none !important; }
.performance-mode .ew-plasma,
.performance-mode .ew-cel-rays,
.performance-mode .ew-cel-plasma { animation: none !important; }
.performance-mode .ew-stage[data-ew-phase="idle"] .ew-led { animation: none !important; opacity: 0.75; }
.performance-mode .ew-stage:not([data-ew-phase="landed"]) .ew-arc { animation: none !important; opacity: 0; }
/* ═══ /DAILY WHEEL — THE TESLA WHEEL ═══════════════════════════════════════ */

/* The Tower's revive button — the one thing on the Robbed screen that should
   look like a way out. Emerald against the red, and it breathes. */
.tof-revive {
  position: relative; overflow: hidden; border: 0;
  background: linear-gradient(to bottom, #6ee7b7, #059669);
  /* steady glow; the sheen below carries the motion (no box-shadow loop) */
  box-shadow: 0 5px 0 rgba(6, 78, 59, 0.9), 0 0 34px rgba(16, 185, 129, 0.6);
}
.tof-revive::after {
  content: ''; position: absolute; top: -60%; bottom: -60%; width: 40%; left: -60%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.5), transparent);
  transform: skewX(-18deg); animation: tof-sheen 2.8s ease-in-out infinite;
}
.reduced-motion .tof-revive { animation: none !important; }
.reduced-motion .tof-revive::after { display: none !important; }
.performance-mode .tof-revive::after { animation: none !important; }
/* ═══════════════════════════════════════════════════════════════════════════
   THE SHOP — pack cards (ui/ShopModal.js · ui/ShopArt.js)
   ───────────────────────────────────────────────────────────────────────────
   The currency packs used to be a blurred coloured rectangle behind a black
   panel, which read as a glow bolted onto a flat card rather than as an object
   with an edge. Each pack is now built like a piece of hardware: a 1.5px lit
   RIM (a real gradient border via padding + a background layer, so it catches
   the light at the top and falls away at the bottom), a recessed body, a pool
   of the pack's own colour behind the product shot, and a foil sweep on hover.

   Everything keys off three custom properties the card sets inline — --pk,
   --pk2 and --pkGlow — so a tier is re-coloured in one place and the rim, the
   halo, the ribbon and the hover glow all follow.
   ═══════════════════════════════════════════════════════════════════════════ */

.shop-pack {
  --pk: #fbbf24;
  --pk2: #b45309;
  --pkGlow: rgba(251, 191, 36, 0.45);
  position: relative;
  transition: transform 0.28s cubic-bezier(0.2, 0.9, 0.3, 1), filter 0.28s ease;
  will-change: transform;
}
.shop-pack:hover { transform: translateY(-6px); }
.shop-pack:active { transform: translateY(-2px) scale(0.995); }

/* the lit rim */
.shop-pack-edge {
  position: absolute; inset: 0; border-radius: 1rem; pointer-events: none;
  background: linear-gradient(160deg, var(--pk), rgba(255, 255, 255, 0.28) 22%, var(--pk2) 55%, rgba(0, 0, 0, 0.7));
  opacity: 0.55; transition: opacity 0.28s ease;
}
.shop-pack:hover .shop-pack-edge { opacity: 1; }

/* the recessed body */
.shop-pack-body {
  background:
    radial-gradient(120% 70% at 50% 0%, color-mix(in srgb, var(--pk) 16%, transparent), transparent 62%),
    linear-gradient(180deg, #1c1917, #0c0a09 70%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09), inset 0 -20px 40px rgba(0, 0, 0, 0.55);
  transition: box-shadow 0.28s ease;
}
/* color-mix is recent; browsers without it just get the flat gradient below. */
@supports not (background: color-mix(in srgb, red 10%, transparent)) {
  .shop-pack-body { background: linear-gradient(180deg, #211d1a, #0c0a09 70%); }
}
.shop-pack:hover .shop-pack-body {
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16), inset 0 -20px 40px rgba(0, 0, 0, 0.5), 0 0 34px -6px var(--pkGlow);
}

/* the pool of light the product sits in */
.shop-pack-halo {
  position: absolute; left: 50%; top: 52%; width: 86%; aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: radial-gradient(circle, var(--pkGlow), transparent 68%);
  opacity: 0.55; filter: blur(6px); pointer-events: none;
  transition: opacity 0.3s ease, transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1);
}
.shop-pack:hover .shop-pack-halo { opacity: 0.95; transform: translate(-50%, -50%) scale(1.08); }

/* the product itself lifts a little under the cursor */
.shop-pack-art { transition: transform 0.4s cubic-bezier(0.2, 0.9, 0.3, 1); }
.shop-pack:hover .shop-pack-art { transform: translateY(-4px) scale(1.06); }

/* a foil sweep, on hover only — a permanent one on six cards at once is noise */
.shop-pack-body::after {
  content: ''; position: absolute; top: -60%; bottom: -60%; width: 38%; left: -70%;
  background: linear-gradient(100deg, transparent, rgba(255, 255, 255, 0.16), transparent);
  transform: skewX(-18deg); pointer-events: none; opacity: 0;
}
.shop-pack:hover .shop-pack-body::after { opacity: 1; animation: shop-foil 0.85s ease-out; }
@keyframes shop-foil { 0% { left: -70%; } 100% { left: 140%; } }

/* the corner ribbon */
/* A fixed-width band centred on the visible corner chord, so a long label
   (MOST POPULAR) sits in the middle of what the card shows instead of
   running off the band's top-left end. */
.shop-ribbon {
  position: absolute; right: -40px; top: 32px; z-index: 20;
  width: 164px; padding: 0.2rem 0; text-align: center; transform: rotate(45deg);
  font-size: 10px; font-weight: 900; letter-spacing: 0.18em; text-transform: uppercase;
  color: #fff; background: linear-gradient(90deg, #c026d3, #db2777);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), inset 0 1px 0 rgba(255, 255, 255, 0.3);
}
.shop-ribbon-gold {
  color: #1c1917; background: linear-gradient(90deg, #f59e0b, #fde68a 50%, #f59e0b);
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.6), 0 0 18px rgba(251, 191, 36, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.6);
}

.reduced-motion .shop-pack,
.reduced-motion .shop-pack-art,
.reduced-motion .shop-pack-halo { transition: none !important; }
.reduced-motion .shop-pack:hover { transform: none; }
.reduced-motion .shop-pack:hover .shop-pack-body::after { animation: none !important; opacity: 0; }
.performance-mode .shop-pack:hover .shop-pack-body::after { animation: none !important; opacity: 0; }

/* ═══════════════════════════════════════════════════════════════════════════
   TOWER OF FORTUNE — THE SHAFT
   ───────────────────────────────────────────────────────────────────────────
   The backdrop used to be a flat radial glow, which meant climbing fifty
   floors looked exactly like standing still on one. This is a lift shaft: three
   layers of masonry and floor-edges falling PAST the camera at different
   speeds, so the parallax reads as upward travel. Everything is a repeating
   gradient translated on the Y axis — no DOM churn, one composited transform
   per layer.

     .tof-shaft-far    distant wall courses, slow, dim
     .tof-shaft-mid    lit floor-edges — the storeys you pass
     .tof-shaft-near   foreground pillars, fast, blurred by proximity

   `.tof-climbing` (set while a floor deals) speeds every layer up and pulls the
   near layer harder, so the moment you press CLIMB the shaft surges.
   ═══════════════════════════════════════════════════════════════════════════ */
.tof-shaft { position: fixed; inset: 0; z-index: 0; overflow: hidden; }
.tof-shaft > span { position: absolute; left: 0; right: 0; top: -120%; bottom: -120%; display: block; }

/* distant courses of stone */
.tof-shaft-far {
  background-image: repeating-linear-gradient(180deg,
    rgba(255, 255, 255, 0.035) 0px, rgba(255, 255, 255, 0.035) 2px,
    transparent 2px, transparent 54px);
  animation: tof-shaft-fall 9s linear infinite;
  opacity: 0.75;
}
/* the storeys — a lit lip and a shadow under it, tinted by the floor you're on */
.tof-shaft-mid {
  background-image: repeating-linear-gradient(180deg,
    color-mix(in srgb, var(--shaftTint, #fbbf24) 26%, transparent) 0px,
    color-mix(in srgb, var(--shaftTint, #fbbf24) 26%, transparent) 3px,
    rgba(0, 0, 0, 0.5) 3px, rgba(0, 0, 0, 0.5) 12px,
    transparent 12px, transparent 168px);
  animation: tof-shaft-fall 5.4s linear infinite;
  opacity: 0.5;
}
@supports not (background: color-mix(in srgb, red 10%, transparent)) {
  .tof-shaft-mid {
    background-image: repeating-linear-gradient(180deg,
      rgba(251, 191, 36, 0.26) 0px, rgba(251, 191, 36, 0.26) 3px,
      rgba(0, 0, 0, 0.5) 3px, rgba(0, 0, 0, 0.5) 12px,
      transparent 12px, transparent 168px);
  }
}
/* foreground pillars down both edges, rushing */
.tof-shaft-near {
  background-image:
    linear-gradient(90deg, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0) 13%),
    linear-gradient(270deg, rgba(0, 0, 0, 0.85) 0%, rgba(0, 0, 0, 0) 13%),
    repeating-linear-gradient(180deg,
      rgba(255, 255, 255, 0.05) 0px, rgba(255, 255, 255, 0.05) 5px,
      transparent 5px, transparent 300px);
  animation: tof-shaft-fall 3.2s linear infinite;
  opacity: 0.85;
}
/* keeps the middle of the screen readable under all that motion */
.tof-shaft-vignette {
  animation: none;
  background: radial-gradient(58% 46% at 50% 46%, rgba(0, 0, 0, 0.72), rgba(0, 0, 0, 0.34) 55%, transparent 80%);
}
@keyframes tof-shaft-fall {
  0%   { transform: translate3d(0, 0, 0); }
  100% { transform: translate3d(0, 168px, 0); }
}

/* the surge as a floor is dealt — you just went up */
.tof-shaft.tof-climbing .tof-shaft-far  { animation-duration: 1.5s; }
.tof-shaft.tof-climbing .tof-shaft-mid  { animation-duration: 0.62s; opacity: 0.85; }
.tof-shaft.tof-climbing .tof-shaft-near { animation-duration: 0.34s; opacity: 1; }
.tof-shaft-far, .tof-shaft-mid, .tof-shaft-near {
  transition: animation-duration 0.4s linear, opacity 0.4s ease;
}

/* the haul's item tiles land rather than appear */
.tof-haul-item { animation: tof-haul-item 0.42s cubic-bezier(0.2, 1.5, 0.4, 1) both; }
@keyframes tof-haul-item {
  0%   { transform: translateY(-8px) scale(0.6); opacity: 0; }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}

.reduced-motion .tof-shaft > span { animation: none !important; }
.reduced-motion .tof-haul-item { animation: none !important; }
.performance-mode .tof-shaft-far,
.performance-mode .tof-shaft-near { display: none !important; }

/* A card's non-cash rewards announce themselves rather than sit in a footnote. */
.tof-rider { animation: tof-rider-in 0.34s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
.tof-rider:nth-of-type(2) { animation-delay: 0.06s; }
.tof-rider:nth-of-type(3) { animation-delay: 0.12s; }
@keyframes tof-rider-in {
  0%   { transform: translateY(6px) scale(0.86); opacity: 0; }
  100% { transform: translateY(0) scale(1); opacity: 1; }
}

/* TOWER OF FORTUNE CONQUERED — the biggest type in the game, because it is the
   rarest thing in it (0.001% of climbs). Slams in, then breathes. */
.tof-conquered {
  animation: tof-conquered-in 0.9s cubic-bezier(0.2, 1.35, 0.35, 1) both;
  filter: drop-shadow(0 0 40px rgba(251, 191, 36, 0.55)) drop-shadow(0 6px 0 rgba(120, 53, 15, 0.4));
}
@keyframes tof-conquered-in {
  0%   { transform: scale(2.6) rotate(-4deg); opacity: 0; filter: blur(18px); }
  45%  { opacity: 1; }
  62%  { transform: scale(0.94) rotate(1.2deg); filter: blur(0); }
  100% { transform: scale(1) rotate(0deg); opacity: 1; filter: blur(0); }
}
.reduced-motion .tof-rider,
.reduced-motion .tof-conquered { animation: none !important; }
/* ═══════════════════════════════════════════════════════════════════════════
   THE LEGENDARY CARD  (ui/CardArt.js — LegendaryRays / LegendaryFrame)
   ───────────────────────────────────────────────────────────────────────────
   A legendary was previously an ordinary card with a gold border and the same
   foil sweep an epic gets, so the rarest thing in the game read as the
   second-rarest. What separates it now is light and craft: rays turning behind
   the emblem, a built frame over the top, a breathing outer glow, and gold dust
   drifting inside the glass.

   LAYER ORDER MATTERS and is fixed here rather than in the markup:
     .lg-rays   z 1  — behind the emblem, so the art looks like the light source
     (art)      z 2
     (foil)     z 5  — the existing sweep
     .lg-frame  z 6  — over everything, including the foil
   ═══════════════════════════════════════════════════════════════════════════ */

/* the card itself: a deeper glow and a hairline gold inner edge. Static — the
   old "breathing" glow animated box-shadow, a full repaint per frame on every
   legendary in the album at once. The rays and the foil carry the life. */
.card-legendary {
  box-shadow:
    0 0 18px rgba(251, 191, 36, 0.45),
    0 0 42px -6px rgba(251, 191, 36, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.16) !important;
}

/* --- the rays behind the emblem ---------------------------------------- */
.lg-rays {
  position: absolute; inset: 0; z-index: 1;
  mix-blend-mode: screen;
}
.lg-ray-spin { animation: lg-ray-spin 26s linear infinite; }
@keyframes lg-ray-spin { to { transform: rotate(360deg); } }

/* the emblem lifts off the rays a little and carries its own warm light */
.lg-art {
  z-index: 2;
  filter: drop-shadow(0 0 7px rgba(253, 230, 138, 0.5));
  animation: lg-art-float 5.5s ease-in-out infinite;
}
@keyframes lg-art-float {
  0%, 100% { transform: translateY(0) scale(1); }
  50%      { transform: translateY(-2.5%) scale(1.025); }
}

/* --- the frame over the top -------------------------------------------- */
.lg-frame { position: absolute; inset: 0; z-index: 6; }

/* gold dust inside the glass, drifting UP — it reads as heat off the artefact */
.lg-motes { position: absolute; inset: 0; overflow: hidden; }
.lg-motes i {
  position: absolute; left: var(--x, 50%); bottom: -6px;
  width: 2px; height: 2px; border-radius: 50%;
  background: #fef3c7;
  box-shadow: 0 0 5px 1px rgba(253, 230, 138, 0.85);
  animation: lg-mote var(--s, 5s) linear var(--d, 0s) infinite;
}
@keyframes lg-mote {
  0%   { transform: translate3d(0, 0, 0) scale(0.5); opacity: 0; }
  15%  { opacity: 0.95; }
  80%  { opacity: 0.5; }
  100% { transform: translate3d(6px, -320%, 0) scale(1.1); opacity: 0; }
}

/* the rarity plate gets a moving gold gradient rather than a flat fill, and a
   gilded edge — on a legendary the plate is part of the FRAME's goldwork, so it
   carries the same dark-gold rule the frame's own rules are drawn with */
.lg-rarity {
  background-image: linear-gradient(100deg, #b45309, #fde68a 45%, #fffbeb 52%, #fde68a 60%, #b45309) !important;
  background-size: 260% 100%;
  animation: lg-rarity-shine 3.6s linear infinite;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.35);
  box-shadow:
    inset 0 0 0 1px rgba(120, 53, 15, 0.5),
    inset 0 1px 0 rgba(255, 251, 235, 0.75),
    0 1px 0 rgba(0, 0, 0, 0.55),
    1px 0 0 rgba(0, 0, 0, 0.35);
}
@keyframes lg-rarity-shine {
  0%   { background-position: 130% 0; }
  100% { background-position: -30% 0; }
}

/* the nameplate: gilded type on a rule */
.lg-plate::before {
  content: ''; position: absolute; left: 14%; right: 14%; top: 0.35rem; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(253, 230, 138, 0.85), transparent);
}
.lg-name {
  color: transparent;
  background-image: linear-gradient(180deg, #fffbeb, #fde68a 45%, #f59e0b);
  -webkit-background-clip: text; background-clip: text;
  text-shadow: 0 0 10px rgba(251, 191, 36, 0.35);
}

.reduced-motion .card-legendary { animation: none !important; }
.reduced-motion .lg-ray-spin,
.reduced-motion .lg-art,
.reduced-motion .lg-rarity { animation: none !important; }
.reduced-motion .lg-motes { display: none !important; }
/* Performance mode keeps the frame and the glow (they're static) and drops the
   three always-running loops. */
.performance-mode .lg-ray-spin,
.performance-mode .lg-art,
.performance-mode .lg-rarity { animation: none !important; }
.performance-mode .lg-motes,
.performance-mode .card-foil { display: none !important; }

/* ═══════════════════════════════════════════════════════════════════════════
   REVIEW PASS (Build 57.9)
   ═══════════════════════════════════════════════════════════════════════════ */
/* Desktop machine fit — App.js sets --machine-fit so the top bar + cabinet fit
   a short laptop window, SPIN included, without scrolling. */
.machine-fit { zoom: var(--machine-fit, 1); }

/* Battle Pass bundle stack: the card at the back keeps its rainbow edge
   peeking out, but not its ribbon, which sat right behind the front card's
   and printed as "…DLE". It fades back in when that card comes forward. */
.bp-ribbon { transition: opacity 0.3s ease; }
.bp-stack-back .bp-ribbon { opacity: 0; }

/* Phone Pass: a fade behind the floating CLAIM ALL / PREMIUM pair, so the
   track dissolves under them instead of printing through the gaps. */
.bpp-actions::before {
  content: ''; position: absolute; z-index: -1; pointer-events: none;
  left: -12px; right: -12px; bottom: -12px; top: -36px;
  background: linear-gradient(to bottom, rgba(10, 9, 8, 0), rgba(10, 9, 8, 0.94) 55%);
}

/* ═══════════════════════════════════════════════════════════════════════════
   SLOT WHEEL STAGE (ui/WheelEngine.js · slots/<id>.wheel.js)      [wx-* classes]
   ───────────────────────────────────────────────────────────────────────────
   Every slot bonus wheel is a full-screen, PHYSICAL object on its own themed
   stage. The engine draws no wheel: it provides the structure below, a frame
   loop that writes transform/opacity on HTML wrappers, and these PRIMITIVES
   that themes compose instead of adding CSS. A theme's own keyframes are
   named '<pfx>-wx-…' and scoped under .wx-stage[data-wx-theme="<id>"].

   Motion contract: only transform and opacity animate, only on HTML boxes (no
   CSS filter / box-shadow / backdrop-filter on anything that moves; SVG filters
   only inside static art). Every animated theme node carries a guard tag —
   .wx-amb (a loop), .wx-fx (decor / one-shot), .wx-heavy (optional, costly) —
   or one of the primitives, so the guards below reach it:
   · reduced motion — the rotor still spins (it is the content) and its smear
     still fades; no windup / settle / pointer kick / shake / chase / ambient
     loops / flash / decorative one-shots;
   · performance mode — loops paused, particles hidden, the frame-loop physics
     and the landing one-shots kept;
   · covered (the jackpot ceremony is up) — everything paused.
   Phase gates key off data-wx-phase (idle | spin | landed) on the stage (and
   the box): .wx-only-idle / .wx-only-spin / .wx-only-landed / .wx-hide-landed.
   ═══════════════════════════════════════════════════════════════════════════ */
.wx-stage {
  position: fixed; inset: 0; z-index: 45;
  overflow: hidden; contain: layout paint;
  color-scheme: dark; background: #07060a;
  -webkit-tap-highlight-color: transparent;
  -webkit-font-smoothing: antialiased;
  --wx-foot: 136px;
}
@media (max-width: 640px) { .wx-stage { --wx-foot: 128px; } }
.wx-enter { animation: wx-enter 0.32s ease-out both; }
@keyframes wx-enter { from { opacity: 0; } to { opacity: 1; } }
.wx-stage.wx-exit { animation: wx-exit 0.3s ease-in both !important; pointer-events: none; }
@keyframes wx-exit { from { opacity: 1; } to { opacity: 0; } }

/* ── structure ── */
/* The scene (behind everything) and the foreground (in front of the object
   AND the chrome, under the flash) are full-bleed, viewport-anchored hosts.
   Both are size containers: cqw / cqh / cqmin / cqmax there are % of the
   stage. Never raw vw / vh / vmin / vmax inside the stage — the desktop root
   is zoomed 1.15× while playing, so they come out 15% too big; the stage
   publishes --wx-vw / --wx-vh / --wx-vmin / --wx-vmax (1% of itself, in its
   own px) for use anywhere (ctx.vw(n) … wrap them). */
.wx-scene { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; container-type: size; }
.wx-scene-in, .wx-fg-in { position: absolute; inset: 0; }
.wx-fg { position: absolute; inset: 0; z-index: 2; overflow: hidden; pointer-events: none; container-type: size; }
.wx-fg * { pointer-events: none !important; }
.wx-scroll {
  position: absolute; inset: 0; z-index: 1;
  overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch; scrollbar-width: none;
}
.wx-scroll::-webkit-scrollbar { display: none; }
/* [header auto][wheel row ≥ min box][footer FIXED]. The row is a size
   container, so the box is sized from the viewport only — never from the
   phase (the footer keeps one height whatever it shows). Too short a window
   scrolls instead of squeezing the wheel onto the footer. */
.wx-grid {
  position: relative; box-sizing: border-box; min-height: 100%;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(var(--wx-minh, 260px), 1fr) var(--wx-foot);
  grid-template-areas: "head" "wheel" "foot";
  padding: calc(env(safe-area-inset-top, 0px) + 14px) calc(env(safe-area-inset-right, 0px) + 12px) calc(env(safe-area-inset-bottom, 0px) + 12px) calc(env(safe-area-inset-left, 0px) + 12px);
  overflow: hidden; overflow: clip;
}
.wx-head { grid-area: head; position: relative; z-index: 3; text-align: center; padding: 0 8px 6px; pointer-events: none; }
.wx-head > * { pointer-events: auto; }
.wx-row {
  grid-area: wheel; position: relative; z-index: 1; min-width: 0; min-height: 0;
  container-type: size; display: flex; align-items: center; justify-content: center;
}
.wx-foot { grid-area: foot; position: relative; z-index: 3; height: var(--wx-foot); min-height: 0; display: flex; align-items: flex-end; justify-content: center; }
.wx-jolt { position: relative; display: flex; align-items: center; justify-content: center; max-width: 100%; }
.wx-overlay { position: absolute; inset: 0; z-index: 5; pointer-events: none; }
.wx-box {
  position: relative; flex: none; overflow: visible;
  aspect-ratio: var(--wx-aspect, 1);
  width: min(94vw, calc((100vh - 300px) * var(--wx-aspect, 1)), var(--wx-maxw, 640px));
  width: min(94vw, calc((100svh - 300px) * var(--wx-aspect, 1)), var(--wx-maxw, 640px));
}
@supports (width: 1cqw) {
  .wx-box { width: min(100cqw, calc(100cqh * var(--wx-aspect, 1)), var(--wx-maxw, 640px)); }
}
/* theme.stage { align, shift } — a wide desktop may seat the object off-centre
   (--wx-shift = ±shift % of the stage, signed by align) */
@media (min-width: 1100px) and (min-aspect-ratio: 4/3) {
  .wx-stage[data-wx-align]:not([data-wx-layout]) .wx-box { left: var(--wx-shift, 0px); }
  /* theme.stage.layout 'side': the object fills the height on one side and the
     title + controls stack in a column beside it (align 'right' mirrors it) —
     nothing is drawn over a top-mounted mechanism */
  .wx-stage[data-wx-layout="side"] .wx-grid {
    grid-template-columns: minmax(0, 1fr) minmax(300px, 30%);
    grid-template-rows: minmax(0, 1fr) auto var(--wx-foot) minmax(0, 1fr);
    grid-template-areas: "wheel ." "wheel head" "wheel foot" "wheel .";
    column-gap: 28px;
  }
  .wx-stage[data-wx-layout="side"][data-wx-align="right"] .wx-grid {
    grid-template-columns: minmax(300px, 30%) minmax(0, 1fr);
    grid-template-areas: ". wheel" "head wheel" "foot wheel" ". wheel";
  }
  .wx-stage[data-wx-layout="side"] .wx-head { padding-bottom: 18px; }
  .wx-stage[data-wx-layout="side"] .wx-foot { align-items: flex-start; }
}
/* landscape phones: the wheel takes the full height on the left, the title
   and the controls stack in a side column */
@media (orientation: landscape) and (max-height: 540px) {
  .wx-stage { --wx-foot: 118px; }
  .wx-grid {
    grid-template-columns: minmax(0, 1fr) minmax(250px, 36%);
    grid-template-rows: auto minmax(0, 1fr) var(--wx-foot);
    grid-template-areas: "wheel head" "wheel ." "wheel foot";
    column-gap: 14px;
    padding-top: calc(env(safe-area-inset-top, 0px) + 8px);
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 8px);
  }
  .wx-head { align-self: start; padding-top: 4px; }
  .wx-title { font-size: clamp(1.1rem, 3.4vw, 1.8rem) !important; }
  /* the side column's height never moves the wheel (it spans every row), so
     the status may wrap instead of being cut off */
  .wx-stage .wx-status { white-space: normal; letter-spacing: 0.16em; }
}

/* ── the object's layers (positions come from the theme, in box units) ── */
.wx-objj { position: absolute; inset: 0; }              /* the 'object' jolt wrapper */
.wx-assembly { position: absolute; inset: 0; isolation: isolate; }
.wx-assembly.wx-drive { will-change: transform; }
.wx-layer { position: absolute; pointer-events: none; }
.wx-fill { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
svg.wx-fill, .wx-fill > svg { overflow: visible; }
.wx-plane, .wx-still, .wx-rotor, .wx-rotor-art, .wx-rotor-in, .wx-winner { position: absolute; inset: 0; transform-origin: 50% 50%; }
.wx-rotor { will-change: transform; }                    /* written by the frame loop */
.wx-fine { position: absolute; inset: 0; will-change: opacity; }
.wx-smear { position: absolute; inset: 0; opacity: 0; will-change: opacity; pointer-events: none; }
.wx-pointer { position: absolute; inset: 0; will-change: transform; }
.wx-drive { will-change: transform, opacity; }
.wx-winner { pointer-events: none; animation: wx-fade-in 0.35s ease-out both; }
.wx-winner-lit { animation: wx-win-blink 1.1s ease-out both; }
@keyframes wx-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes wx-win-blink { 0% { opacity: 0; } 8% { opacity: 1; } 24% { opacity: 0.55; } 44%, 100% { opacity: 1; } }
/* variant swap (the Prize Reel's ring promotion): the old wheel slides off the
   arbor toward you while the new one clanks on */
.wx-rotor-out-wrap { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
.wx-rotor-out { position: absolute; inset: 0; will-change: transform, opacity; animation: wx-rotor-out 0.72s cubic-bezier(0.5, 0, 0.75, 0.2) both; }
@keyframes wx-rotor-out {
  0%   { transform: translate3d(0, 0, 0) scale(1); opacity: 1; }
  32%  { transform: translate3d(0, -1.5%, 0) scale(1.035); opacity: 1; }
  100% { transform: translate3d(0, 16%, 0) scale(1.1); opacity: 0; }
}
.wx-rotor-in { animation: wx-rotor-in 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.25) 0.22s both; }
@keyframes wx-rotor-in { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* ── the hub: the engine's <button> around the theme's art ── */
.wx-hub {
  position: absolute; margin: 0; padding: 0; border: 0; background: none; color: inherit;
  border-radius: 50%; cursor: pointer; pointer-events: auto; outline: none;
  -webkit-appearance: none; appearance: none; transition: transform 0.12s ease;
}
.wx-hub:disabled { cursor: default; }
.wx-hub:not(:disabled):active { transform: scale(0.965); }
.wx-hub:focus-visible { box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.85), 0 0 0 6px var(--wx-accent, #fbbf24); }
.wx-hub-art { position: absolute; inset: 0; display: block; border-radius: 50%; }
.wx-hub-ready { animation: wx-hub-breathe 1.8s ease-in-out 0.6s 3 both; }
@keyframes wx-hub-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.035); } }

/* ── chase lamps (ref={ctx.light(i)} on each lamp's LIT sprite) ── */
.wx-light { opacity: 0.85; will-change: opacity; animation: wx-light-idle 2.4s ease-in-out 4 both; }
.wx-light.wx-light-steady { animation: none; }
@keyframes wx-light-idle { 0% { opacity: 0.7; } 10% { opacity: 1; } 28% { opacity: 0.25; } 64% { opacity: 0.45; } 100% { opacity: 0.85; } }
[data-wx-phase="spin"] .wx-light, [data-wx-phase="landed"] .wx-light { animation: none; }
[data-wx-phase="landed"] .wx-light[data-wx-win] { animation: wx-light-win 0.6s ease-in-out 0s 4 both; }
@keyframes wx-light-win { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }

/* ── the landing flash (one shot, half strength, never strobes) ── */
.wx-flash {
  position: absolute; inset: 0; z-index: 6; pointer-events: none; opacity: 0; will-change: opacity;
  background: radial-gradient(calc(56 * var(--wx-vmax, 1vmax)) calc(56 * var(--wx-vmax, 1vmax)) at var(--wx-fx, 50%) var(--wx-fy, 55%), var(--wx-flash, #fff4d6), transparent 72%);
  animation: wx-flash 0.8s ease-out both;
}
@keyframes wx-flash { 0% { opacity: 0; } 7% { opacity: 0.5; } 100% { opacity: 0; } }

/* ── flow chrome (themed through inline tokens: theme.chrome) ── */
.wx-title {
  margin: 0; font-weight: 700; line-height: 1.04; text-transform: uppercase;
  font-size: clamp(1.5rem, 6.6vw, 3.2rem); letter-spacing: 0.07em;
  color: transparent; -webkit-background-clip: text; background-clip: text;
  filter: drop-shadow(0 0 12px var(--wx-title-glow, rgba(251, 191, 36, 0.5))) drop-shadow(0 3px 2px rgba(0, 0, 0, 0.7));
  padding: 0.04em 0.06em; text-wrap: balance;
  animation: wx-ignite 1.3s linear 0.12s both;
}
@keyframes wx-ignite { 0% { opacity: 0; } 8% { opacity: 0.9; } 11% { opacity: 0.2; } 16% { opacity: 1; } 21% { opacity: 0.35; } 26%, 100% { opacity: 1; } }
.wx-sub { margin-top: 2px; font-size: clamp(0.6rem, 2.3vw, 0.8rem); font-weight: 700; letter-spacing: 0.34em; text-transform: uppercase; opacity: 0.92; }
.wx-status {
  margin-top: 6px; min-height: 1.4em; line-height: 1.4; font-size: clamp(0.62rem, 2.4vw, 0.75rem);
  font-weight: 800; letter-spacing: 0.28em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.wx-foot-in { position: relative; width: 100%; max-width: 520px; height: 100%; display: flex; flex-direction: column; justify-content: flex-end; gap: 8px; container: wxfoot / inline-size; }
.wx-foot-row { height: 48px; flex: none; }
.wx-upg { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; height: 100%; }
.wx-upg-btn {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  height: 100%; padding: 0 6px; border-radius: 12px; cursor: pointer;
  border: 1px solid var(--wx-tone); border-color: color-mix(in srgb, var(--wx-tone) 58%, transparent);
  color: var(--wx-tone); font-size: 11px; font-weight: 900; letter-spacing: 0.08em; line-height: 1.15;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.07), 0 2px 0 rgba(0, 0, 0, 0.45);
  transition: transform 0.12s ease;
}
.wx-upg-btn:active { transform: scale(0.97); }
.wx-upg-btn.is-short { opacity: 0.55; }
.wx-upg-name { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
.wx-upg-cost { opacity: 0.85; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.wx-upg-short { display: none; }
/* narrow footers (landscape side column, 320px phones): one line per label —
   smaller type and the short label ('SURE JACKPOT'; the full one stays in the
   DOM for QA/screen readers), never a 2–3-line wrap */
@container wxfoot (max-width: 340px) {
  .wx-upg-btn { font-size: 10px; letter-spacing: 0.03em; padding: 0 4px; gap: 1px; line-height: 1.05; }
  .wx-upg-name { justify-content: center; gap: 4px; max-width: 100%; overflow: hidden; }
  .wx-upg-full { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .wx-upg-short { display: inline; }
  .wx-upg-cost { font-size: 9.5px; }
}
@container wxfoot (max-width: 250px) {
  .wx-upg-name > :first-child:not(:only-child) { display: none; }
}
.wx-plate {
  height: 100%; display: flex; align-items: center; justify-content: center; padding: 0 12px; text-align: center;
  border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(0, 0, 0, 0.45);
  color: rgba(231, 229, 228, 0.78); font-size: 11px; font-weight: 900; letter-spacing: 0.2em; text-transform: uppercase;
}
.wx-plate-good { border-color: rgba(16, 185, 129, 0.55); background: rgba(6, 78, 59, 0.45); color: #6ee7b7; }
.wx-btn {
  display: flex; align-items: center; justify-content: center; width: 100%; height: 58px; flex: none;
  border-radius: 14px; border: 1px solid transparent; cursor: pointer;
  font-size: 22px; font-weight: 700; letter-spacing: 0.22em; text-transform: uppercase; line-height: 1;
}
.wx-btn-spin:not(:disabled):not(.is-live) { animation: wx-btn-breathe 1.9s ease-in-out infinite; }
@keyframes wx-btn-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.018); } }
.wx-btn-spin:not(.is-live):active { transform: scale(0.97); animation: none; }
.wx-btn-spin.is-live { opacity: 0.42; cursor: default; box-shadow: none !important; }
.wx-toast {
  position: absolute; left: 50%; bottom: calc(100% + 6px); transform: translateX(-50%);
  padding: 6px 14px; border-radius: 999px; white-space: nowrap; pointer-events: none;
  background: rgba(69, 10, 10, 0.94); border: 1px solid rgba(248, 113, 113, 0.6); color: #fecaca;
  font-size: 11px; font-weight: 900; letter-spacing: 0.18em; text-transform: uppercase;
  animation: wx-toast 1.9s ease-out both;
}
@keyframes wx-toast { 0% { opacity: 0; transform: translate(-50%, 6px); } 12% { opacity: 1; transform: translate(-50%, 0); } 80% { opacity: 1; } 100% { opacity: 0; transform: translate(-50%, -4px); } }
.wx-won {
  position: relative; height: 100%; display: flex; align-items: center; justify-content: center; gap: 14px;
  padding: 8px 18px 12px; border-radius: 16px; border: 1px solid; overflow: hidden;
  animation: wx-won-in 0.5s cubic-bezier(0.2, 0.9, 0.3, 1.2) both;
}
@keyframes wx-won-in { from { opacity: 0; transform: translate3d(0, 22px, 0) scale(0.96); } to { opacity: 1; transform: none; } }
.wx-won-glyph { flex: none; line-height: 0; }
.wx-won-text { min-width: 0; text-align: left; }
.wx-won-label { font-size: 11px; font-weight: 800; letter-spacing: 0.3em; text-transform: uppercase; }
.wx-won-prize { font-weight: 700; letter-spacing: 0.03em; white-space: nowrap; overflow: hidden; overflow-wrap: anywhere; }
/* the prize line starts from the footer's width (WxWonPanel then measures
   and shrinks it to fit exactly, wrapping as the last resort);
   (the longest line, 'GRAND JACKPOT' / '15 FREE SPINS', is ~7.3 em wide) */
@supports (width: 1cqw) {
  .wx-won .wx-won-prize { font-size: clamp(0.95rem, calc((100cqw - 112px) / 7.6), 1.875rem); line-height: 1.2; }
  @container wxfoot (max-width: 300px) {
    .wx-won { gap: 8px; padding-left: 10px; padding-right: 10px; }
    .wx-won-glyph { transform: scale(0.8); margin: 0 -5px; }
    .wx-won .wx-won-prize { font-size: clamp(0.95rem, calc((100cqw - 84px) / 7.6), 1.875rem); }
  }
}
.wx-won-pay { margin-top: 2px; color: #4ade80; font-weight: 900; font-size: 1.3rem; line-height: 1.1; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.wx-won-bar { position: absolute; left: 16px; right: 16px; bottom: 6px; height: 3px; border-radius: 3px; background: rgba(255, 255, 255, 0.1); overflow: hidden; }
.wx-won-bar > i { display: block; height: 100%; transform-origin: 0 50%; animation: wx-bar 1s linear both; }
@keyframes wx-bar { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ── AMBIENT PRIMITIVES (compose these; tune with the CSS variables) ── */
/* .wx-strike — a flash-strike (lightning, flame licks, laser flicker, sparkle
   pops): idle 4.4s × 3 (delay --wx-d), spin --wx-sd infinite (delay --wx-sdl),
   landed once. */
.wx-strike { opacity: 0; will-change: opacity; }
[data-wx-phase="idle"] .wx-strike { animation: wx-strike 4.4s linear var(--wx-d, 0s) 3 both; }
[data-wx-phase="spin"] .wx-strike { animation: wx-strike-fast var(--wx-sd, 1s) linear var(--wx-sdl, 0s) infinite both; }
[data-wx-phase="landed"] .wx-strike { animation: wx-strike-once 1.2s linear 0s 1 both; }
@keyframes wx-strike { 0%, 60% { opacity: 0; } 60.6% { opacity: 1; } 62.5% { opacity: 0.15; } 64% { opacity: 1; } 66.5% { opacity: 0.4; } 68% { opacity: 0.9; } 72% { opacity: 0.28; } 80%, 100% { opacity: 0; } }
@keyframes wx-strike-fast { 0% { opacity: 0; } 4% { opacity: 1; } 10% { opacity: 0.18; } 15% { opacity: 0.95; } 24% { opacity: 0.35; } 38%, 100% { opacity: 0; } }
@keyframes wx-strike-once { 0% { opacity: 0; } 4% { opacity: 1; } 12% { opacity: 0.25; } 20% { opacity: 1; } 45% { opacity: 0.4; } 100% { opacity: 0; } }
/* .wx-flicker — gaslight / torch / neon: --wx-fl period, --wx-lo floor */
.wx-flicker { will-change: opacity; animation: wx-flicker var(--wx-fl, 2.3s) linear var(--wx-delay, 0s) infinite; }
@keyframes wx-flicker { 0%, 100% { opacity: 1; } 7% { opacity: var(--wx-lo, 0.72); } 9% { opacity: 1; } 38% { opacity: 0.93; } 41% { opacity: var(--wx-lo, 0.72); } 44% { opacity: 1; } 71% { opacity: 0.96; } 73% { opacity: var(--wx-lo, 0.72); } 76% { opacity: 0.9; } }
/* .wx-breathe — a slow swell for glow pools / auras: --wx-br, --wx-lo */
.wx-breathe { will-change: opacity; animation: wx-breathe var(--wx-br, 4s) ease-in-out var(--wx-delay, 0s) infinite alternate; }
@keyframes wx-breathe { from { opacity: var(--wx-lo, 0.55); } to { opacity: 1; } }
/* .wx-drift — smoke / fog / clouds / nebula: by (--wx-dx, --wx-dy) over --wx-dur */
.wx-drift { will-change: transform; animation: wx-drift var(--wx-dur, 18s) ease-in-out var(--wx-delay, 0s) infinite alternate; }
@keyframes wx-drift { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(var(--wx-dx, 40px), var(--wx-dy, 0px), 0); } }
/* .wx-rise / .wx-fall — particles (embers, bubbles, motes / snow, sand,
   sprinkles): travel by (--wx-dx, --wx-dy) fading in and out; --wx-o peak */
.wx-rise, .wx-fall { opacity: 0; will-change: transform, opacity; animation: wx-rise var(--wx-dur, 6s) linear var(--wx-delay, 0s) infinite; }
.wx-fall { --wx-dy: 140px; }
@keyframes wx-rise { 0% { transform: translate3d(0, 0, 0); opacity: 0; } 12% { opacity: var(--wx-o, 1); } 82% { opacity: var(--wx-o, 1); } 100% { transform: translate3d(var(--wx-dx, 0px), var(--wx-dy, -140px), 0); opacity: 0; } }
/* .wx-sway — lanterns / flags / vines / cords: ±--wx-sw about --wx-origin */
.wx-sway { transform-origin: var(--wx-origin, 50% 0%); will-change: transform; animation: wx-sway var(--wx-dur, 5s) ease-in-out var(--wx-delay, 0s) infinite alternate; }
@keyframes wx-sway { from { transform: rotate(calc(-1 * var(--wx-sw, 3deg))); } to { transform: rotate(var(--wx-sw, 3deg)); } }
/* .wx-bob — ±--wx-bob vertical float */
.wx-bob { will-change: transform; animation: wx-bob var(--wx-dur, 3.6s) ease-in-out var(--wx-delay, 0s) infinite alternate; }
@keyframes wx-bob { from { transform: translate3d(0, calc(-1 * var(--wx-bob, 4px)), 0); } to { transform: translate3d(0, var(--wx-bob, 4px), 0); } }
/* .wx-turn — background gears, a wormhole, rays: --wx-dur, --wx-dir */
.wx-turn { will-change: transform; animation: wx-turn var(--wx-dur, 40s) linear var(--wx-delay, 0s) infinite; animation-direction: var(--wx-dir, normal); }
@keyframes wx-turn { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
/* .wx-sweep — searchlights: between --wx-a0 and --wx-a1 about --wx-origin */
.wx-sweep { transform-origin: var(--wx-origin, 50% 0%); will-change: transform; animation: wx-sweep var(--wx-dur, 9s) ease-in-out var(--wx-delay, 0s) infinite alternate; }
@keyframes wx-sweep { from { transform: rotate(var(--wx-a0, -20deg)); } to { transform: rotate(var(--wx-a1, 20deg)); } }
/* .wx-shimmer — heat haze */
.wx-shimmer { will-change: transform, opacity; animation: wx-shimmer var(--wx-dur, 3.2s) ease-in-out var(--wx-delay, 0s) infinite; }
@keyframes wx-shimmer { 0%, 100% { transform: translate3d(0, 0, 0); opacity: 1; } 30% { transform: translate3d(1px, -1px, 0); opacity: 0.86; } 60% { transform: translate3d(-1px, 0, 0); opacity: 0.95; } }
/* .wx-glint — a bright bar crossing its overflow-hidden parent every --wx-dur */
.wx-glint { will-change: transform; transform: translateX(-130%); animation: wx-glint var(--wx-dur, 6s) ease-in-out var(--wx-delay, 0s) infinite; }
@keyframes wx-glint { 0% { transform: translateX(-130%); } 28%, 100% { transform: translateX(260%); } }
/* .wx-pulse — heartbeat scale to --wx-ps */
.wx-pulse { will-change: transform; animation: wx-pulse var(--wx-dur, 1.6s) ease-in-out var(--wx-delay, 0s) infinite; }
@keyframes wx-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(var(--wx-ps, 1.06)); } }
/* one-shots for the win moment (mount them in layers.win; --wx-delay staggers) */
.wx-once-pop { animation: wx-once-pop 0.6s cubic-bezier(0.2, 0.9, 0.3, 1.3) var(--wx-delay, 0s) both; }
@keyframes wx-once-pop { 0% { transform: scale(0.4); opacity: 0; } 60% { transform: scale(1.08); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
.wx-once-puff { animation: wx-once-puff 0.9s ease-out var(--wx-delay, 0s) both; }
@keyframes wx-once-puff { 0% { transform: scale(0.5); opacity: 0.9; } 100% { transform: scale(1.7); opacity: 0; } }
.wx-once-shock { animation: wx-once-shock 1.1s cubic-bezier(0.2, 0.7, 0.3, 1) var(--wx-delay, 0s) both; }
@keyframes wx-once-shock { 0% { transform: scale(0.2); opacity: 0.95; } 100% { transform: scale(2.3); opacity: 0; } }
.wx-once-rise { animation: wx-once-rise 0.7s cubic-bezier(0.2, 0.9, 0.3, 1) var(--wx-delay, 0s) both; }
@keyframes wx-once-rise { 0% { transform: translate3d(0, 18px, 0); opacity: 0; } 100% { transform: none; opacity: 1; } }
/* phase gates */
[data-wx-phase="spin"] .wx-only-idle, [data-wx-phase="landed"] .wx-only-idle,
[data-wx-phase="idle"] .wx-only-spin, [data-wx-phase="landed"] .wx-only-spin,
[data-wx-phase="idle"] .wx-only-landed, [data-wx-phase="spin"] .wx-only-landed,
[data-wx-phase="landed"] .wx-hide-landed { display: none !important; }
[data-wx-phase="spin"] .wx-spinfast { animation-duration: var(--wx-dur-spin, 1s) !important; }

/* ── guards ── */
/* the frame loop writes these every frame: the global reduced-motion
   transition (.05s on *) would start a new transition per write */
.reduced-motion .wx-stage :is(.wx-box, .wx-box *, .wx-scene-in *, .wx-fg-in *) { transition: none !important; }
.reduced-motion .wx-stage.wx-enter,
.reduced-motion .wx-stage :is(.wx-title, .wx-strike, .wx-flicker, .wx-breathe, .wx-drift, .wx-rise, .wx-fall, .wx-sway, .wx-bob,
  .wx-turn, .wx-sweep, .wx-shimmer, .wx-glint, .wx-pulse, .wx-hub-ready, .wx-light, .wx-amb, .wx-btn-spin,
  .wx-winner, .wx-winner-lit, .wx-rotor-in, .wx-rotor-out, .wx-won) { animation: none !important; }
.reduced-motion .wx-stage :is(.wx-strike, .wx-rise, .wx-fall, .wx-glint, .wx-flash, .wx-once-pop, .wx-once-puff,
  .wx-once-shock, .wx-once-rise, .wx-fx) { display: none !important; }
.reduced-motion .wx-stage .wx-light { opacity: 0.8 !important; }
.performance-mode .wx-stage :is(.wx-rise, .wx-fall, .wx-drift, .wx-glint, .wx-shimmer, .wx-heavy) { display: none !important; }
.performance-mode .wx-stage :is(.wx-flicker, .wx-breathe, .wx-sway, .wx-bob, .wx-turn, .wx-sweep, .wx-pulse, .wx-amb, .wx-btn-spin) { animation: none !important; }
.performance-mode .wx-stage:not([data-wx-phase="landed"]) .wx-strike { animation: none !important; opacity: 0 !important; }
.performance-mode .wx-stage[data-wx-phase="idle"] .wx-light { animation: none !important; }
.wx-stage[data-wx-covered] *,
.wx-stage[data-wx-covered] *::before,
.wx-stage[data-wx-covered] *::after { animation-play-state: paused !important; }
/* ═══ /SLOT WHEEL STAGE ═══════════════════════════════════════════════════ */

/* ── BONUS SCENES (ui/BonusScene.js) — the living world behind every bonus
   board. .bxs-stage is the viewport-sized, opaque backdrop (and, as
   .bxs-front, the pointer-less foreground framing over the board). ── */
.bxs-stage { position: fixed; inset: 0; z-index: 30; overflow: hidden; pointer-events: none; contain: strict; }
.bxs-front { background: transparent !important; z-index: 31; }
.bxs-layer, .bxs-fill { position: absolute; inset: 0; }
.bxs-in { animation: bxs-in 0.6s ease-out both; }
@keyframes bxs-in { from { opacity: 0; } to { opacity: 1; } }
.bxs-mote { position: absolute; width: 3px; height: 3px; border-radius: 50%; filter: blur(0.4px); }
.reduced-motion .bxs-stage :is(.bxs-in, .wx-strike, .wx-flicker, .wx-breathe, .wx-drift, .wx-rise, .wx-fall, .wx-sway, .wx-bob, .wx-turn, .wx-sweep, .wx-pulse, .wx-glint, .wx-shimmer, .wx-amb, .bxs-amb) { animation: none !important; }
.reduced-motion .bxs-stage :is(.wx-rise, .wx-fall, .wx-strike, .wx-glint) { opacity: 0 !important; }
/* Performance mode (the phone default) keeps a scene ALIVE — the machine
   under it is paused, so its ambient loops run; only layers marked heavy go. */
.performance-mode .bxs-stage :is(.wx-heavy, .bxs-heavy) { display: none !important; }
/* .wx-strike keys off the wheel stage's phase; in a scene it just strikes on
   its idle cadence, forever. */
.bxs-stage .wx-strike { animation: wx-strike 4.4s linear var(--wx-d, 0s) infinite both; }

/* ── MACHINE SCENES (ui/MachineScene.js) — the world behind the cabinet
   (.ms-stage: fixed, under every piece of the machine UI), its foreground
   framing (.ms-front-stage) and the backplate inside the reel window
   (.ms-reels-plate). Live state comes from data attributes on the machine
   root (game/App.js): the .ms-when-* wrappers fade in and out with it. ── */
.ms-stage { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; contain: strict; }
.ms-front-stage { background: transparent !important; z-index: 9; }
.ms-layer { position: absolute; inset: 0; }
.ms-stage > .ms-layer { animation: bxs-in 0.7s ease-out both; }
.ms-reels-plate { position: absolute; inset: 0; overflow: hidden; pointer-events: none; }
.ms-fill { position: absolute; inset: 0; }
.ms-when-spin, .ms-when-win, .ms-when-big, .ms-when-fs, .ms-when-tease { opacity: 0; transition: opacity 0.45s ease; }
.ms-when-idle { opacity: 1; transition: opacity 0.45s ease; }
[data-ms-spin="1"] .ms-when-spin,
[data-ms-win="1"] .ms-when-win,
[data-ms-big="1"] .ms-when-big,
[data-ms-fs="1"] .ms-when-fs,
[data-ms-tease="1"] .ms-when-tease { opacity: 1; }
[data-ms-spin="1"] .ms-when-idle { opacity: 0; }
/* A settled desktop reel column draws a dark plate under its tiles; with a
   machine backplate the plate shows through instead. */
.ms-has-plate .reel-col-plate { display: none; }
/* …and a spinning column's dark backing thins so the plate reads through the
   blur strip (it keeps enough weight for the motion to stay legible). */
.ms-has-plate .reel-spin-back { background: rgba(0, 0, 0, 0.38) !important; }
:is(.ms-stage, .ms-reels-plate) .wx-strike { animation: wx-strike 4.4s linear var(--wx-d, 0s) infinite both; }
[data-ms-spin="1"] :is(.ms-stage, .ms-reels-plate) .wx-strike { animation: wx-strike-fast var(--wx-sd, 1.2s) linear var(--wx-sdl, 0s) infinite both; }
.reduced-motion :is(.ms-stage, .ms-reels-plate) * { animation: none !important; transition: none !important; }
.reduced-motion :is(.ms-stage, .ms-reels-plate) :is(.wx-rise, .wx-fall, .wx-strike, .wx-glint) { opacity: 0 !important; }
.performance-mode :is(.ms-stage, .ms-reels-plate) :is(.ms-heavy, .wx-heavy) { display: none !important; }
/* (the layers' own fade-in is left to finish, or a scene opened under a
   toast would hold at opacity 0) */
.machine-quiet :is(.ms-stage, .ms-reels-plate) .ms-layer *,
.wx-stage-up :is(.ms-stage, .ms-reels-plate) .ms-layer * { animation-play-state: paused !important; }

/* ── SCREEN SCENES (ui/ScreenScene.js) — the world behind a screen (lobby,
   shop, tower, pass, openings…): .ss-stage is fixed at z -1 inside the
   screen's root (a stacking context), .ss-front-stage the pointer-less
   framing over it. Only the top-most scene animates and paints. ── */
.ss-stage { position: fixed; inset: 0; z-index: -1; overflow: hidden; pointer-events: none; contain: strict; }
.ss-front-stage { background: transparent !important; z-index: 1; }
.ss-layer, .ss-fill { position: absolute; inset: 0; }
.ss-stage > .ss-layer { animation: bxs-in 0.6s ease-out both; }
.ss-covered { visibility: hidden; }
.ss-covered .ss-layer *, .ss-paused .ss-layer * { animation-play-state: paused !important; }
.ss-stage .wx-strike { animation: wx-strike 4.4s linear var(--wx-d, 0s) infinite both; }
.reduced-motion .ss-stage * { animation: none !important; transition: none !important; }
.reduced-motion .ss-stage :is(.wx-rise, .wx-fall, .wx-strike, .wx-glint) { opacity: 0 !important; }
.performance-mode .ss-stage :is(.ss-heavy, .wx-heavy) { display: none !important; }

/* ── WIN SCENES (ui/WinScene.js) — the machine's own world behind the
   BIG / MEGA / SUPER / ULTRA ceremony. .ws-stage fills the ceremony overlay
   under its content; data-ws-tier steps up live as the amount counts, and
   the .ws-at-* wrappers fade in as each tier is reached. ── */
.ws-stage { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; contain: strict; }
.ws-front-stage { background: transparent !important; z-index: 30; }
.ws-layer, .ws-fill { position: absolute; inset: 0; }
.ws-stage > .ws-layer { animation: ws-in 0.45s ease-out both; }
@keyframes ws-in { from { opacity: 0; transform: scale(1.06); } to { opacity: 1; transform: none; } }
.ws-at-big, .ws-at-mega, .ws-at-super, .ws-at-ultra, .ws-when-landed {
  opacity: 0; visibility: hidden; transition: opacity 0.4s ease, visibility 0s linear 0.4s;
}
.ws-when-rolling { opacity: 1; transition: opacity 0.4s ease; }
.ws-at-big *, .ws-at-mega *, .ws-at-super *, .ws-at-ultra *, .ws-when-landed * { animation-play-state: paused; }
[data-ws-tier="big"] .ws-at-big,
[data-ws-tier="mega"] :is(.ws-at-big, .ws-at-mega),
[data-ws-tier="super"] :is(.ws-at-big, .ws-at-mega, .ws-at-super),
[data-ws-tier="ultra"] :is(.ws-at-big, .ws-at-mega, .ws-at-super, .ws-at-ultra),
[data-ws-phase="landed"] .ws-when-landed { opacity: 1; visibility: visible; transition: opacity 0.3s ease, visibility 0s; }
[data-ws-tier="big"] .ws-at-big *,
[data-ws-tier="mega"] :is(.ws-at-big, .ws-at-mega) *,
[data-ws-tier="super"] :is(.ws-at-big, .ws-at-mega, .ws-at-super) *,
[data-ws-tier="ultra"] :is(.ws-at-big, .ws-at-mega, .ws-at-super, .ws-at-ultra) *,
[data-ws-phase="landed"] .ws-when-landed * { animation-play-state: running; }
[data-ws-phase="landed"] .ws-when-rolling { opacity: 0; }
.ws-stage .wx-strike { animation: wx-strike 4.4s linear var(--wx-d, 0s) infinite both; }
.reduced-motion .ws-stage * { animation: none !important; transition: none !important; }
.reduced-motion .ws-stage :is(.wx-rise, .wx-fall, .wx-strike, .wx-glint) { opacity: 0 !important; }
.performance-mode .ws-stage :is(.ws-heavy, .wx-heavy) { display: none !important; }

/* ── JACKPOT SCENES (ui/JackpotScene.js) — the machine's own world behind
   the MINI / MINOR / MAJOR / GRAND jackpot. The tier is fixed; the phase
   builds: charge → burst (the capsule cracks) → reveal → landed. ── */
.jps-stage { position: absolute; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; contain: strict; }
.jps-front-stage { background: transparent !important; z-index: 30; }
.jps-layer, .jps-fill { position: absolute; inset: 0; }
.jps-stage > .jps-layer { animation: ws-in 0.5s ease-out both; }
[data-jps-tier="mini"] :is(.jps-at-minor, .jps-at-major, .jps-at-grand),
[data-jps-tier="minor"] :is(.jps-at-major, .jps-at-grand),
[data-jps-tier="major"] .jps-at-grand { display: none !important; }
.jps-after-burst, .jps-when-landed { opacity: 0; visibility: hidden; transition: opacity 0.35s ease, visibility 0s linear 0.35s; }
.jps-after-burst *, .jps-when-landed * { animation-play-state: paused; }
:is([data-jps-phase="burst"], [data-jps-phase="reveal"], [data-jps-phase="landed"]) .jps-after-burst,
[data-jps-phase="landed"] .jps-when-landed { opacity: 1; visibility: visible; transition: opacity 0.22s ease, visibility 0s; }
:is([data-jps-phase="burst"], [data-jps-phase="reveal"], [data-jps-phase="landed"]) .jps-after-burst *,
[data-jps-phase="landed"] .jps-when-landed * { animation-play-state: running; }
.jps-when-charge { opacity: 1; transition: opacity 0.3s ease; }
.jps-stage:not([data-jps-phase="charge"]) .jps-when-charge { opacity: 0; }
.jps-stage:not([data-jps-phase="charge"]) .jps-when-charge * { animation-play-state: paused; }
.jps-stage.jps-rest * { animation-play-state: paused !important; }
.jps-stage .wx-strike { animation: wx-strike 4.4s linear var(--wx-d, 0s) infinite both; }
.reduced-motion .jps-stage * { animation: none !important; transition: none !important; }
.reduced-motion .jps-stage :is(.wx-rise, .wx-fall, .wx-strike, .wx-glint) { opacity: 0 !important; }
.performance-mode .jps-stage :is(.jps-heavy, .ws-heavy, .wx-heavy) { display: none !important; }

/* ============================================================
   BIG WIN CEREMONY (BigWinCelebration, ui/Overlays.js) — the machine's own
   world behind (WIN SCENES above), and over it: the tier word on a ribbon,
   the amount in a plaque with a ×bet readout, the BIG · MEGA · SUPER · ULTRA
   meter. Tinted per live tier through --bwx-rgb / --bwx-fill / --bwx-edge.
   Only transform / opacity move; nothing that moves is filtered.
   ============================================================ */
.bwx-root {
  position: fixed; inset: 0; z-index: 108; display: flex; align-items: center; justify-content: center;
  overflow: hidden; isolation: isolate; cursor: pointer; user-select: none; -webkit-user-select: none;
  animation: overlay-fade-in 0.35s ease-out both;
}
.bwx-plain { background: rgba(0, 0, 0, 0.82); }
.bwx-leaving { animation: bwx-root-out 0.48s ease-in forwards !important; pointer-events: none !important; }
@keyframes bwx-root-out { 0%, 35% { opacity: 1; } 100% { opacity: 0; } }

/* the soft dark pool the text sits in, over the world */
.bwx-pool {
  position: absolute; inset: 0; pointer-events: none; z-index: 5;
  background: radial-gradient(ellipse min(46vw, 560px) min(34vh, 330px) at 50% 50%, rgba(0,0,0,0.66) 0%, rgba(0,0,0,0.42) 52%, rgba(0,0,0,0) 100%);
}
.bwx-plain .bwx-pool { background: radial-gradient(ellipse at 50% 50%, rgba(var(--bwx-rgb), 0.16), rgba(0,0,0,0) 60%); }
.bwx-flash {
  position: absolute; inset: 0; pointer-events: none; z-index: 40;
  background: radial-gradient(ellipse at 50% 48%, rgba(255,255,255,0.95) 0%, rgba(var(--bwx-rgb), 0.55) 26%, rgba(var(--bwx-rgb), 0) 68%);
  animation: bwx-flash 0.5s ease-out forwards;
}
@keyframes bwx-flash { 0% { opacity: 0; } 18% { opacity: 0.9; } 100% { opacity: 0; } }

.bwx-col { position: relative; z-index: 10; display: flex; justify-content: center; width: 100%; padding: 0 16px; pointer-events: none; }
.bwx-stack { position: relative; display: flex; flex-direction: column; align-items: center; width: min(820px, 100%); }
.bwx-leaving .bwx-stack { animation: bwx-fly 0.46s cubic-bezier(0.5, 0, 0.75, 0) forwards; }
@keyframes bwx-fly {
  0% { transform: none; opacity: 1; }
  25% { transform: scale(1.05); opacity: 1; }
  100% { transform: translateY(-40vh) scale(0.22); opacity: 0; }
}

/* light behind the headline */
.bwx-halo { position: absolute; left: 50%; top: 36%; width: 0; height: 0; pointer-events: none; z-index: 0; }
.bwx-rays-mask {
  position: absolute; left: min(-46vw, -440px); top: min(-46vw, -440px); width: max(92vw, 880px); height: max(92vw, 880px); border-radius: 50%;
  -webkit-mask: radial-gradient(closest-side, #000 0%, rgba(0,0,0,0.7) 38%, rgba(0,0,0,0) 100%);
  mask: radial-gradient(closest-side, #000 0%, rgba(0,0,0,0.7) 38%, rgba(0,0,0,0) 100%);
}
.bwx-rays {
  position: absolute; inset: 0; border-radius: 50%; opacity: 0.55;
  background: repeating-conic-gradient(from 0deg, rgba(var(--bwx-rgb), 0.34) 0deg 7deg, rgba(var(--bwx-rgb), 0) 7deg 22.5deg);
  animation: bwx-spin 18s linear infinite;
  transition: opacity 0.4s;
}
.bwx-t-win .bwx-rays { opacity: 0.25; }
.bwx-t-ultra .bwx-rays, .bwx-t-super .bwx-rays { opacity: 0.7; }
@keyframes bwx-spin { to { transform: rotate(360deg); } }
.bwx-core {
  position: absolute; left: -260px; top: -150px; width: 520px; height: 300px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--bwx-rgb), 0.42), rgba(var(--bwx-rgb), 0.12) 55%, rgba(var(--bwx-rgb), 0));
  animation: bwx-breathe 1.6s ease-in-out infinite;
}
@keyframes bwx-breathe { 0%, 100% { opacity: 0.75; transform: scale(1); } 50% { opacity: 1; transform: scale(1.08); } }

/* the shockwave of a tier step: two rings out of the headline */
.bwx-wave { position: absolute; left: 50%; top: 36%; width: 0; height: 0; pointer-events: none; z-index: 1; }
.bwx-wave span {
  position: absolute; left: -120px; top: -120px; width: 240px; height: 240px; border-radius: 50%;
  border: 5px solid rgba(var(--bwx-rgb), 0.95);
  box-shadow: 0 0 26px rgba(var(--bwx-rgb), 0.8), inset 0 0 22px rgba(var(--bwx-rgb), 0.6);
  animation: bwx-wave 0.85s cubic-bezier(0.15, 0.6, 0.3, 1) forwards;
}
.bwx-wave span + span { border-width: 2px; border-color: rgba(255,255,255,0.9); animation-delay: 0.08s; animation-duration: 1s; opacity: 0; }
@keyframes bwx-wave { 0% { transform: scale(0.15); opacity: 1; } 100% { transform: scale(5.2); opacity: 0; } }

/* coins + sparks thrown out on every step */
.bwx-burst { position: absolute; left: 50%; top: 40%; width: 0; height: 0; pointer-events: none; z-index: 2; }
.bwx-burst i {
  position: absolute; left: -13px; top: -13px; width: 26px; height: 26px; border-radius: 50%; opacity: 0;
  animation: bwx-throw 1.05s cubic-bezier(0.12, 0.7, 0.35, 1) var(--dl, 0s) forwards;
}
.bwx-coin {
  background: radial-gradient(circle at 36% 30%, #fffbe6 0 12%, #fde047 24%, #f59e0b 56%, #92400e 100%);
  box-shadow: inset 0 0 0 2px rgba(120,53,15,0.55), inset 0 0 0 5px rgba(253,224,71,0.5), 0 0 12px rgba(251,191,36,0.55);
}
.bwx-spark {
  width: 14px !important; height: 14px !important; left: -7px !important; top: -7px !important;
  background: radial-gradient(closest-side, #fff 0 30%, rgba(var(--bwx-rgb), 0.9) 45%, rgba(var(--bwx-rgb), 0) 100%);
}
@keyframes bwx-throw {
  0% { opacity: 0; transform: translate(0, 0) scale(0.3) rotate(0deg); }
  10% { opacity: 1; }
  70% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--x), var(--y)) scale(1.15) rotate(var(--r)); }
}

/* the machine's crest */
.bwx-crest { position: relative; z-index: 3; margin-bottom: 2px; animation: win-pop 0.7s ease-out; }
.bwx-crest-glow { position: absolute; inset: -40%; pointer-events: none; background: radial-gradient(closest-side, rgba(var(--bwx-rgb), 0.45), rgba(var(--bwx-rgb), 0)); }

/* the tier word on its ribbon */
.bwx-head { position: relative; z-index: 4; display: flex; justify-content: center; padding: 0.12em 0.9em 0.2em; font-size: clamp(2.5rem, 10.5vw, 7.4rem); }
.bwx-ribbon { position: absolute; left: -0.1em; right: -0.1em; top: 0.06em; bottom: 0.02em; width: calc(100% + 0.2em); height: calc(100% - 0.08em); overflow: visible; animation: bwx-ribbon 0.5s cubic-bezier(0.2, 1.4, 0.4, 1) both; }
@keyframes bwx-ribbon { 0% { transform: scaleX(0.1); opacity: 0; } 100% { transform: none; opacity: 1; } }
.bwx-word {
  position: relative; margin: 0; display: flex; align-items: baseline; white-space: nowrap;
  font-size: 1em; line-height: 1.02; font-weight: 900; letter-spacing: 0.04em; text-transform: uppercase;
}
.bwx-gap { display: inline-block; width: 0.32em; }
.bwx-ch { position: relative; display: inline-block; animation: bwx-drop 0.52s cubic-bezier(0.2, 1.5, 0.45, 1) calc(var(--i) * 45ms) both; }
@keyframes bwx-drop {
  0% { transform: translateY(-0.9em) scale(2.4); opacity: 0; }
  55% { opacity: 1; }
  100% { transform: none; opacity: 1; }
}
.bwx-ch-in { position: relative; display: inline-block; animation: bwx-wave-y 1.5s ease-in-out calc(0.7s + var(--i) * 0.09s) infinite; }
@keyframes bwx-wave-y { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-0.07em); } }
.bwx-ch-back {
  display: inline-block; color: var(--bwx-edge);
  -webkit-text-stroke: 0.13em var(--bwx-edge); paint-order: stroke fill;
  text-shadow: 0 0.035em 0 var(--bwx-deep), 0 0.07em 0 var(--bwx-deep), 0 0.1em 0 var(--bwx-deep), 0 0.15em 0.12em rgba(0,0,0,0.7), 0 0 0.35em rgba(var(--bwx-rgb), 0.85);
}
.bwx-ch-fill {
  position: absolute; left: 0; top: 0; display: inline-block;
  background: var(--bwx-fill); -webkit-background-clip: text; background-clip: text; color: transparent;
  -webkit-text-stroke: 0.012em rgba(255,255,255,0.55);
}

/* the amount */
.bwx-plaque {
  position: relative; z-index: 4; margin-top: 0.7rem; width: min(680px, 88vw); padding: 0.55rem 1.4rem 0.6rem; border-radius: 22px; overflow: hidden;
  background: linear-gradient(180deg, rgba(var(--bwx-rgb), 0.16) 0%, rgba(14, 10, 6, 0.9) 22%, rgba(5, 4, 3, 0.94) 100%);
  border: 2px solid rgba(var(--bwx-rgb), 0.75);
  box-shadow: 0 0 0 4px rgba(0,0,0,0.55), 0 0 0 5px rgba(var(--bwx-rgb), 0.3), 0 0 46px rgba(var(--bwx-rgb), 0.35), 0 18px 40px rgba(0,0,0,0.6),
    inset 0 2px 0 rgba(255,255,255,0.18), inset 0 -14px 34px rgba(var(--bwx-rgb), 0.14);
  animation: bwx-plaque-in 0.55s cubic-bezier(0.2, 1.3, 0.4, 1) 0.12s both;
  transition: border-color 0.3s, box-shadow 0.3s;
}
@keyframes bwx-plaque-in { 0% { transform: translateY(24px) scale(0.85); opacity: 0; } 100% { transform: none; opacity: 1; } }
.bwx-plaque.bwx-bump { animation: bwx-bump 0.42s cubic-bezier(0.2, 1.6, 0.4, 1); }
@keyframes bwx-bump { 0% { transform: scale(1); } 30% { transform: scale(1.09); } 100% { transform: scale(1); } }
.bwx-amount {
  font-size: clamp(3.3rem, 14vw, 6.8rem); line-height: 1.04; font-weight: 900; text-align: center;
  background: linear-gradient(180deg, #fffef5 0%, #fef08a 32%, #facc15 55%, #d97706 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.bwx-mult { display: flex; align-items: baseline; justify-content: center; gap: 8px; margin-top: 0.1rem; }
.bwx-mult b {
  font-weight: 900; font-size: 1.05rem; color: #fff; padding: 1px 10px; border-radius: 999px; font-variant-numeric: tabular-nums;
  background: rgba(var(--bwx-rgb), 0.28); border: 1px solid rgba(var(--bwx-rgb), 0.7);
}
.bwx-mult span { font-size: 0.68rem; font-weight: 800; letter-spacing: 0.22em; text-transform: uppercase; color: rgba(255,255,255,0.6); }
.bwx-plaque-shine { position: absolute; inset: 0; pointer-events: none; }
.bwx-plaque-shine i {
  position: absolute; top: -20%; bottom: -20%; left: 0; width: 22%; opacity: 0;
  background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.32), rgba(255,255,255,0));
  transform: translateX(-120%) skewX(-18deg);
}
.bwx-landed .bwx-plaque-shine i { animation: bwx-shine 1.6s ease-in-out 0.15s 2; }
@keyframes bwx-shine { 0% { transform: translateX(-120%) skewX(-18deg); opacity: 1; } 70%, 100% { transform: translateX(560%) skewX(-18deg); opacity: 1; } }
.bwx-landed .bwx-plaque { border-color: rgba(var(--bwx-rgb), 1); }

/* the BIG · MEGA · SUPER · ULTRA meter */
.bwx-meter { position: relative; z-index: 4; width: min(560px, 80vw); height: 46px; margin-top: 1.1rem; animation: bwx-plaque-in 0.5s ease-out 0.25s both; }
.bwx-track {
  position: absolute; left: 0; right: 0; top: 8px; height: 12px; border-radius: 999px; overflow: hidden;
  background: rgba(0,0,0,0.6); box-shadow: inset 0 1px 3px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.14);
}
.bwx-fill { position: absolute; inset: 0; overflow: hidden; border-radius: 999px; will-change: transform; }
.bwx-fill i {
  position: absolute; inset: 0; will-change: transform;
  background: linear-gradient(90deg, #fbbf24 0%, #fcd34d 22%, #c084fc 40%, #a855f7 50%, #fb923c 64%, #ef4444 75%, #f472b6 86%, #22d3ee 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.55);
}
.bwx-pip { position: absolute; top: 0; width: 0; height: 0; }
.bwx-pip i {
  position: absolute; left: -14px; top: 0; width: 28px; height: 28px; border-radius: 50%;
  background: #1c1917; border: 2px solid rgba(255,255,255,0.28); box-shadow: 0 2px 6px rgba(0,0,0,0.6);
  transition: background 0.2s, border-color 0.2s;
}
.bwx-pip:last-child i { left: -26px; }
.bwx-pip b {
  position: absolute; top: 30px; left: 0; transform: translateX(-50%); white-space: nowrap;
  font-size: 10px; font-weight: 900; letter-spacing: 0.16em; color: rgba(255,255,255,0.42); transition: color 0.2s;
}
.bwx-pip:last-child b { transform: translateX(-80%); }
.bwx-pip.on i { animation: bwx-pip 0.5s cubic-bezier(0.2, 1.8, 0.4, 1); border-color: #fff; }
.bwx-pip.on b { color: #fff; }
.bwx-pip-big.on i { background: radial-gradient(circle at 40% 35%, #fff7c2, #fbbf24 55%, #b45309); box-shadow: 0 0 16px #fbbf24; }
.bwx-pip-mega.on i { background: radial-gradient(circle at 40% 35%, #f5e1ff, #c084fc 55%, #6d28d9); box-shadow: 0 0 16px #c084fc; }
.bwx-pip-super.on i { background: radial-gradient(circle at 40% 35%, #ffedd5, #fb923c 55%, #b91c1c); box-shadow: 0 0 16px #f97316; }
.bwx-pip-ultra.on i { background: radial-gradient(circle at 40% 35%, #fff, #f472b6 45%, #22d3ee); box-shadow: 0 0 18px #f472b6; }
.bwx-pip.final i::after {
  content: ''; position: absolute; inset: -9px; border-radius: 50%; border: 2px solid #fff; opacity: 0;
  animation: bwx-ping 1.1s ease-out infinite;
}
@keyframes bwx-pip { 0% { transform: scale(0.4); } 60% { transform: scale(1.45); } 100% { transform: scale(1); } }
@keyframes bwx-ping { 0% { transform: scale(0.6); opacity: 0.9; } 100% { transform: scale(1.5); opacity: 0; } }

/* skip hint / Collect */
.bwx-foot { position: relative; z-index: 4; height: 56px; margin-top: 0.9rem; display: flex; align-items: center; justify-content: center; }
.bwx-hint { font-size: 11px; letter-spacing: 0.3em; text-transform: uppercase; color: rgba(231,229,228,0.7); }
.bwx-collect {
  pointer-events: auto; cursor: pointer; padding: 12px 46px; border-radius: 999px;
  font-size: 1rem; font-weight: 900; letter-spacing: 0.24em; text-transform: uppercase; color: #3b1a02;
  background: linear-gradient(180deg, #fffbeb 0%, #fde68a 30%, #fbbf24 62%, #d97706 100%);
  border: 2px solid rgba(255,255,255,0.7);
  box-shadow: 0 0 0 3px rgba(120,53,15,0.65), 0 0 30px rgba(var(--bwx-rgb), 0.7), 0 10px 22px rgba(0,0,0,0.55), inset 0 -3px 0 rgba(146,64,14,0.5);
  animation: bwx-collect-in 0.45s cubic-bezier(0.2, 1.6, 0.4, 1) both, bwx-collect-beat 1.1s ease-in-out 0.6s infinite;
}
.bwx-collect:active { transform: scale(0.95); }
@keyframes bwx-collect-in { 0% { transform: scale(0.3); opacity: 0; } 100% { transform: none; opacity: 1; } }
@keyframes bwx-collect-beat { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.06); } }

/* phones: tighter stack */
@media (max-width: 640px) {
  .bwx-plaque { padding: 0.45rem 0.9rem 0.5rem; border-radius: 18px; }
  .bwx-mult b { font-size: 0.92rem; }
  .bwx-meter { margin-top: 0.9rem; }
  .bwx-wave span { left: -80px; top: -80px; width: 160px; height: 160px; }
}
.performance-mode .bwx-core { animation: none; }
.reduced-motion .bwx-root, .reduced-motion .bwx-root * { animation: none !important; transition: none !important; }
.reduced-motion .bwx-burst, .reduced-motion .bwx-wave, .reduced-motion .bwx-flash { display: none; }

/* ============================================================
   JACKPOT CEREMONY (JackpotCelebration, ui/Overlays.js) — the machine's own
   world behind (JACKPOT SCENES above), and over it: the MINI · MINOR · MAJOR
   · GRAND ladder that climbs and locks, a core of light that gathers and
   bursts to reveal the machine's crest, and a marquee board —
   chasing bulbs, the tier word, JACKPOT on a banner, the amount on an LED
   panel. Tinted by the tier's colour on this machine through the same
   --bwx-* tokens as the big-win word (BwxWord, burst, wave, flash reused).
   ============================================================ */
.jpx-root {
  position: fixed; inset: 0; z-index: 110; display: flex; align-items: center; justify-content: center;
  overflow: hidden; isolation: isolate; cursor: pointer; user-select: none; -webkit-user-select: none;
  animation: overlay-fade-in 0.4s ease-out both;
}
.jpx-plain { background: rgba(0, 0, 0, 0.9); }
.jpx-leaving { animation: bwx-root-out 0.52s ease-in forwards !important; pointer-events: none !important; }
.jpx-leaving .jpx-stack { animation: bwx-fly 0.5s cubic-bezier(0.5, 0, 0.75, 0) forwards; }
.jpx-rest .jpx-stack *, .jpx-rest .jpx-stack { animation-play-state: paused !important; }
.jpx-pool {
  position: absolute; inset: 0; pointer-events: none; z-index: 5;
  background: radial-gradient(ellipse min(44vw, 520px) min(40vh, 400px) at 50% 52%, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.45) 55%, rgba(0,0,0,0) 100%);
  transition: opacity 0.5s;
}
.jpx-plain .jpx-pool { background: radial-gradient(ellipse at 50% 50%, rgba(var(--bwx-rgb), 0.2), rgba(0,0,0,0) 62%); }

.jpx-col { position: relative; z-index: 10; display: flex; justify-content: center; width: 100%; max-height: 100%; padding: 0 16px; pointer-events: none; }
.jpx-stack { position: relative; display: flex; flex-direction: column; align-items: center; width: min(760px, 100%); }

/* MINI · MINOR · MAJOR · GRAND */
.jpx-ladder { display: flex; gap: 8px; margin-bottom: 4px; animation: bwx-plaque-in 0.45s ease-out both; }
.jpx-plate {
  position: relative; min-width: 74px; padding: 6px 12px 5px; border-radius: 10px; text-align: center;
  background: linear-gradient(180deg, rgba(30,27,24,0.92), rgba(8,7,6,0.95));
  border: 1.5px solid rgba(var(--pc-rgb), 0.35); box-shadow: 0 4px 10px rgba(0,0,0,0.5);
  transition: background 0.18s, border-color 0.18s, box-shadow 0.18s, opacity 0.3s;
}
.jpx-plate b { position: relative; font-size: 12px; font-weight: 900; letter-spacing: 0.2em; color: rgba(var(--pc-rgb), 0.55); transition: color 0.18s; }
.jpx-plate i { position: absolute; inset: 0; border-radius: inherit; opacity: 0; background: radial-gradient(ellipse at 50% 0%, rgba(255,255,255,0.5), rgba(255,255,255,0) 60%); transition: opacity 0.18s; }
.jpx-plate.on {
  background: linear-gradient(180deg, var(--pc) 0%, var(--pc-deep) 100%); border-color: #fff;
  box-shadow: 0 0 18px rgba(var(--pc-rgb), 0.85), 0 4px 10px rgba(0,0,0,0.5);
  animation: jpx-plate-on 0.32s cubic-bezier(0.2, 1.7, 0.4, 1);
}
.jpx-plate.on b { color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,0.6); }
.jpx-plate.on i { opacity: 1; }
/* once the ladder locks, the ones below it fade back and the tier stands out */
.jpx-locked .jpx-plate.on:not(.won) { opacity: 0.45; box-shadow: none; }
.jpx-plate.past { opacity: 0.35; }
.jpx-plate.won.lock { transform: scale(1.18); z-index: 1; animation: jpx-plate-lock 0.5s cubic-bezier(0.2, 1.8, 0.4, 1) both; }
.jpx-plate.won.lock::after {
  content: ''; position: absolute; inset: -6px; border-radius: 14px; border: 2px solid rgba(var(--pc-rgb), 1); opacity: 0;
  animation: bwx-ping 1.1s ease-out 0.2s infinite;
}
@keyframes jpx-plate-on { 0% { transform: scale(0.8); } 60% { transform: scale(1.12); } 100% { transform: scale(1); } }
@keyframes jpx-plate-lock { 0% { transform: scale(1); } 45% { transform: scale(1.42); } 100% { transform: scale(1.18); } }

/* the reveal: the gathering core, then the crest, with its light behind */
.jpx-cap { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 172px; margin-top: 10px; z-index: 6; }
.jpx-orb { position: absolute; left: 50%; top: 50%; width: 0; height: 0; }
.jpx-orb-core {
  position: absolute; left: -46px; top: -46px; width: 92px; height: 92px; border-radius: 50%;
  background: radial-gradient(circle, #fff 0 14%, rgba(var(--bwx-rgb), 0.95) 32%, rgba(var(--bwx-rgb), 0.35) 58%, rgba(var(--bwx-rgb), 0) 72%);
  animation: jpx-orb-grow 1.7s cubic-bezier(0.5, 0, 0.9, 0.6) both, jpx-orb-throb 0.26s ease-in-out infinite alternate;
}
.jpx-t-mini .jpx-orb-core { animation-duration: 0.8s, 0.26s; }
.jpx-t-minor .jpx-orb-core { animation-duration: 1.05s, 0.26s; }
.jpx-t-major .jpx-orb-core { animation-duration: 1.35s, 0.26s; }
@keyframes jpx-orb-grow { 0% { transform: scale(0.2); opacity: 0.3; } 100% { transform: scale(1.5); opacity: 1; } }
@keyframes jpx-orb-throb { from { margin-top: 0; } to { margin-top: 0; opacity: 0.82; } }
/* rings of light drawn IN to the core */
.jpx-orb-ring {
  position: absolute; left: -90px; top: -90px; width: 180px; height: 180px; border-radius: 50%;
  border: 3px solid rgba(var(--bwx-rgb), 0.9); box-shadow: 0 0 18px rgba(var(--bwx-rgb), 0.7), inset 0 0 14px rgba(var(--bwx-rgb), 0.5);
  opacity: 0; animation: jpx-orb-in 0.62s cubic-bezier(0.55, 0, 0.9, 0.5) infinite;
}
.jpx-orb-ring:nth-child(2) { animation-delay: 0.2s; border-color: rgba(255,255,255,0.8); }
.jpx-orb-ring:nth-child(3) { animation-delay: 0.41s; }
@keyframes jpx-orb-in { 0% { transform: scale(2.4); opacity: 0; } 30% { opacity: 1; } 100% { transform: scale(0.15); opacity: 0.2; } }
/* the crest slams in out of the burst */
.jpx-crest { position: relative; line-height: 0; animation: jpx-crest-in 0.5s cubic-bezier(0.2, 1.5, 0.4, 1) both; }
@keyframes jpx-crest-in { 0% { transform: scale(2.6) rotate(-8deg); opacity: 0; } 35% { opacity: 1; } 100% { transform: none; opacity: 1; } }
.jpx-crest-glow { position: absolute; inset: -45%; pointer-events: none; background: radial-gradient(closest-side, rgba(var(--bwx-rgb), 0.6), rgba(var(--bwx-rgb), 0.18) 55%, rgba(var(--bwx-rgb), 0)); }
.jpx-crest-art { position: relative; display: inline-block; line-height: 0; animation: jpx-float 2.4s ease-in-out 0.6s infinite; }
@keyframes jpx-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
.jpx-crest-star { position: relative; display: inline-block; font-size: 110px; line-height: 1; color: #fde68a; text-shadow: 0 0 30px rgba(var(--bwx-rgb), 0.9); }
.jpx-halo { position: absolute; left: 50%; top: 50%; width: 0; height: 0; pointer-events: none; z-index: -1; }
.jpx-rays-mask {
  position: absolute; left: min(-44vw, -420px); top: min(-44vw, -420px); width: max(88vw, 840px); height: max(88vw, 840px); border-radius: 50%;
  -webkit-mask: radial-gradient(closest-side, #000 0%, rgba(0,0,0,0.7) 36%, rgba(0,0,0,0) 100%);
  mask: radial-gradient(closest-side, #000 0%, rgba(0,0,0,0.7) 36%, rgba(0,0,0,0) 100%);
  opacity: 0; transition: opacity 0.4s;
}
.jpx-ph-burst .jpx-rays-mask, .jpx-ph-reveal .jpx-rays-mask, .jpx-ph-landed .jpx-rays-mask { opacity: 1; }
.jpx-core {
  position: absolute; left: -220px; top: -190px; width: 440px; height: 380px; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--bwx-rgb), 0.5), rgba(var(--bwx-rgb), 0.14) 55%, rgba(var(--bwx-rgb), 0));
  animation: bwx-breathe 1.4s ease-in-out infinite;
}
.jpx-ph-charge .jpx-core { animation-duration: 0.7s; opacity: 0.7; }
.jpx-cap .bwx-wave, .jpx-cap .bwx-burst { top: 50%; }
.jpx-cap .bwx-burst { z-index: 8; }

/* the marquee */
.jpx-sign { position: relative; z-index: 7; margin-top: 8px; width: min(640px, 90vw); border-radius: 26px; transition: opacity 0.3s; }
.jpx-sign:not(.on) { opacity: 0.72; }
.jpx-sign.jpx-bump { animation: bwx-bump 0.42s cubic-bezier(0.2, 1.6, 0.4, 1); }
.jpx-sign-face {
  position: relative; display: flex; flex-direction: column; align-items: center; padding: 0.3rem 1.1rem 1rem; border-radius: 22px;
  background: linear-gradient(180deg, rgba(var(--bwx-rgb), 0.22) 0%, rgba(16, 10, 8, 0.94) 18%, rgba(6, 4, 4, 0.96) 100%);
  border: 3px solid rgba(var(--bwx-rgb), 0.85);
  box-shadow: 0 0 0 5px rgba(0,0,0,0.6), 0 0 0 7px rgba(var(--jpx-bulb-rgb), 0.45), 0 0 60px rgba(var(--bwx-rgb), 0.4), 0 24px 50px rgba(0,0,0,0.7),
    inset 0 2px 0 rgba(255,255,255,0.2), inset 0 -18px 40px rgba(var(--bwx-rgb), 0.16);
}
.jpx-sign:not(.on) .jpx-sign-face { border-color: rgba(var(--bwx-rgb), 0.35); box-shadow: 0 0 0 5px rgba(0,0,0,0.6), 0 0 0 7px rgba(255,255,255,0.08), 0 24px 50px rgba(0,0,0,0.7); }
/* chasing bulbs: two offset rows that blink in turn */
.jpx-bulbs {
  --jb: radial-gradient(circle, #fff 0 1.8px, var(--jpx-bulb) 2.8px, rgba(var(--jpx-bulb-rgb), 0.38) 4.6px, rgba(var(--jpx-bulb-rgb), 0) 7px);
  position: absolute; inset: -13px; border-radius: 34px; pointer-events: none; z-index: 1;
  background: var(--jb) 0 0 / 20px 20px repeat-x, var(--jb) 0 100% / 20px 20px repeat-x, var(--jb) 0 0 / 20px 20px repeat-y, var(--jb) 100% 0 / 20px 20px repeat-y;
  opacity: 0.12;
}
.jpx-bulbs-b { background-position: 10px 0, 10px 100%, 0 10px, 100% 10px; }
.jpx-sign.on .jpx-bulbs-a { animation: jpx-chase 0.56s steps(1) infinite; }
.jpx-sign.on .jpx-bulbs-b { animation: jpx-chase 0.56s steps(1) 0.28s infinite; }
.jpx-ph-landed .jpx-sign .jpx-bulbs-a { animation-duration: 0.34s; }
.jpx-ph-landed .jpx-sign .jpx-bulbs-b { animation-duration: 0.34s; animation-delay: 0.17s; }
@keyframes jpx-chase { 0% { opacity: 1; } 50% { opacity: 0.18; } }

.jpx-head { position: relative; display: flex; justify-content: center; font-size: clamp(3rem, 13vw, 7.2rem); margin-top: -0.05em; }
.jpx-head-dark {
  font-weight: 900; line-height: 1.02; color: rgba(255,255,255,0.08); -webkit-text-stroke: 2px rgba(var(--bwx-rgb), 0.3);
}
.jpx-ph-charge .jpx-head-dark { animation: jpx-dark 0.5s ease-in-out infinite alternate; }
@keyframes jpx-dark { from { opacity: 0.5; } to { opacity: 1; } }
.jpx-banner { position: relative; width: min(330px, 70%); height: 46px; margin-top: -0.35rem; display: flex; align-items: center; justify-content: center; }
.jpx-banner svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.jpx-banner span {
  position: relative; font-size: 1.35rem; font-weight: 900; letter-spacing: 0.42em; padding-left: 0.42em; color: #fff;
  text-shadow: 0 2px 0 var(--bwx-deep), 0 0 14px rgba(var(--bwx-rgb), 0.9);
}
.jpx-sign.on .jpx-banner { animation: bwx-ribbon 0.5s cubic-bezier(0.2, 1.4, 0.4, 1) 0.18s both; }
.jpx-sign:not(.on) .jpx-banner span { color: rgba(255,255,255,0.25); text-shadow: none; }
.jpx-led {
  position: relative; width: 100%; margin-top: 0.55rem; padding: 0.35rem 0.9rem 0.3rem; border-radius: 14px; overflow: hidden;
  background: radial-gradient(ellipse at 50% 120%, rgba(var(--bwx-rgb), 0.22), rgba(0,0,0,0) 70%), repeating-linear-gradient(0deg, rgba(255,255,255,0.025) 0 2px, rgba(0,0,0,0) 2px 4px), #050403;
  box-shadow: inset 0 3px 10px rgba(0,0,0,0.9), inset 0 0 0 1px rgba(255,255,255,0.08), 0 1px 0 rgba(255,255,255,0.12);
}
.jpx-amount {
  font-size: clamp(3rem, 12.5vw, 6.2rem); line-height: 1.06; font-weight: 900; text-align: center;
  background: linear-gradient(180deg, #fffef5 0%, #fef08a 32%, #facc15 56%, #d97706 100%);
  -webkit-background-clip: text; background-clip: text; color: transparent;
}
.jpx-amount-idle { opacity: 0.18; }
.jpx-led-shine { position: absolute; inset: 0; pointer-events: none; }
.jpx-led-shine i {
  position: absolute; top: -20%; bottom: -20%; left: 0; width: 20%; opacity: 0;
  background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,0.3), rgba(255,255,255,0));
  transform: translateX(-120%) skewX(-18deg);
}
.jpx-ph-landed .jpx-led-shine i { animation: bwx-shine 1.6s ease-in-out 0.12s 2; }

.jpx-foot { position: relative; z-index: 7; height: 60px; margin-top: 1.1rem; display: flex; align-items: center; justify-content: center; }
.jpx-collect { padding: 13px 54px; font-size: 1.1rem; }

@media (max-width: 640px) {
  .jpx-plate { min-width: 0; padding: 5px 9px 4px; }
  .jpx-plate b { font-size: 10px; letter-spacing: 0.14em; }
  .jpx-ladder { gap: 6px; }
  .jpx-sign-face { padding: 0.2rem 0.7rem 0.8rem; }
  .jpx-banner { height: 38px; }
  .jpx-banner span { font-size: 1.05rem; }
  .jpx-cap .bwx-wave span { left: -80px; top: -80px; width: 160px; height: 160px; }
  .jpx-cap { height: 140px; }
  .jpx-crest-star { font-size: 86px; }
}
@media (max-height: 760px) and (min-width: 641px) {
  .jpx-head { font-size: clamp(2.6rem, 10vh, 5.6rem); }
  .jpx-amount { font-size: clamp(2.6rem, 9vh, 5rem); }
}
.performance-mode .jpx-core { animation: none; }
.reduced-motion .jpx-root, .reduced-motion .jpx-root * { animation: none !important; transition: none !important; }
.reduced-motion .jpx-root :is(.bwx-burst, .bwx-wave, .bwx-flash) { display: none; }

/* ── SVG TEXT CENTRING ON SAFARI — Chrome and Firefox inherit
   dominant-baseline (CSS Inline 3), WebKit does not (SVG 1.1): a
   dominantBaseline="central" set on an <svg> or <g> reaches the <text>
   inside it everywhere but iOS / macOS Safari, where every such label sat on
   its alphabetic baseline — visibly off-centre on every wheel. Inherit it
   down explicitly (a no-op where it already inherits). ── */
[dominant-baseline] :is(g, text, tspan, textPath, a, svg, switch):not([dominant-baseline]) { dominant-baseline: inherit; }

/* ============================================================
   PACK CEREMONY UPGRADE — every stage of a card pack, more alive
   (ui/CardsUI.js "THE UPGRADE KIT" + PackOpeningOverlay; the pouch art is
   PackVisual in ui/CardArt.js). ADDITIVE ONLY: nothing above is edited — the
   two rules here that restyle an existing class (.card-land.pku-deal and the
   .flip-punch flip) win on order / specificity and keep every existing
   reduced-motion guard in force.
     sealed    floats over its own shadow, glints, a tier aura, a tap-me hand,
               an idle nudge; Legendary/Wild wear a drifting holo foil
     ripping   the camera leans in (scale with --tense), the seam glows and
               spreads, light shafts pour out of it and lengthen, a ray wheel
               wakes behind the pouch, foil flecks fly off every tap
     hot       (past half way) energy cracks flicker, sparks stream up
     brink     (last two taps) heartbeat, the ray wheel whips round, energy
               rings are drawn inward, the room is at its darkest
     tear      the body splits and the halves fly apart, the cards' own light
               bursts out, three cards rise and fan, foil shreds fly
     reveal    cards deal in on a fan, flip with a lift + wind-up, every flip
               fires a rarity ray-star and sparks, NEW! is a slammed stamp,
               the hero card shimmers under a letterboxed title card
     done      the hand fans out per row, the haul is named and stamped in,
               counters roll up like an odometer, the party scales with the
               best pull (confetti count, rays, a legendary coin shower)
   Rules kept: only transform / opacity move; SVG is static; no filter,
   clip-path or mask on anything that moves; .pku-x marks the extras that
   performance mode drops; reduced motion stills or skips all of it.
   ============================================================ */
.pku-root { overflow-x: hidden; }
/* .pku-dim (below) does the old button-sized tension vignette's job for the
   whole room; that vignette's opaque edge drew a visible dark box on desktop. */
.pku-root .pack-rip-tense, .pku-root .pack-inhale { display: none; }

/* --- the room holds its breath (opacity set inline from the rip) --- */
.pku-dim {
  position: fixed; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse 58% 52% at 50% 44%, rgba(0,0,0,0) 0%, rgba(0,0,0,0.5) 58%, rgba(0,0,0,0.92) 100%);
  transition: opacity 0.28s ease-out;
}
.pku-dim.inhale { animation: pku-inhale 0.7s cubic-bezier(0.7,0,0.3,1) forwards; }
@keyframes pku-inhale { 0% { opacity: 0.72; } 30% { opacity: 0.96; } 42% { opacity: 0; } 100% { opacity: 0; } }

/* --- the foil tag under the title --- */
.pku-tagline {
  display: inline-flex; align-items: center; gap: 7px; margin-bottom: 20px;
  padding: 3px 12px 3px 9px; border-radius: 9999px; white-space: nowrap;
  font-size: 10px; font-weight: 900; letter-spacing: 0.15em; text-transform: uppercase; color: #f5f5f4;
  background: linear-gradient(180deg, rgba(var(--pk-glow),0.3), rgba(12,10,9,0.72));
  border: 1px solid rgba(var(--pk-glow),0.6);
  box-shadow: 0 0 14px rgba(var(--pk-glow),0.25), inset 0 1px 0 rgba(255,255,255,0.2);
  transition: opacity 0.2s ease-out;
}
.pku-tagline.gone { opacity: 0; }
.pku-tag-gems { color: var(--pk-c1); letter-spacing: 0; display: inline-flex; gap: 1px; font-size: 11px; }
.pku-tag-gems i { font-style: normal; }

/* --- SEALED ------------------------------------------------------------- */
.pku-shadow {
  position: absolute; left: 20%; right: 20%; bottom: -10%; height: 9%; pointer-events: none;
  border-radius: 50%; background: radial-gradient(closest-side, rgba(0,0,0,0.6), rgba(0,0,0,0));
}
/* in step with lobby-float (3s, -12px at the top): the shadow tightens as the pouch rises */
.pku-shadow.float { animation: pku-shadow 3s ease-in-out infinite; will-change: transform, opacity; }
@keyframes pku-shadow { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(0.78); opacity: 0.6; } }

.pku-aura {
  position: absolute; inset: -36%; pointer-events: none;
  opacity: calc(0.4 + var(--tense,0) * 0.6); transform: scale(calc(0.8 + var(--tense,0) * 0.5));
  transition: opacity 0.3s ease-out, transform 0.22s ease-out;
}
.pku-aura.gone { opacity: 0; }
.pku-aura i {
  position: absolute; inset: 0; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,255,255,0.22), rgba(var(--pku-rgb),0.42) 30%, rgba(var(--pku-rgb),0.13) 60%, rgba(var(--pku-rgb),0) 80%);
  animation: pku-aura 2.8s ease-in-out infinite; will-change: transform, opacity;
}
.pku-aura.brink i { background: radial-gradient(closest-side, rgba(255,255,255,0.62), rgba(var(--pku-rgb),0.62) 34%, rgba(var(--pku-rgb),0.2) 62%, rgba(var(--pku-rgb),0) 82%); }
@keyframes pku-aura { 0%,100% { transform: scale(0.9); opacity: 0.55; } 50% { transform: scale(1.07); opacity: 1; } }

.pku-lean { position: absolute; inset: 0; transition: transform 0.22s cubic-bezier(0.2,1.4,0.4,1); }
.pku-beat { position: absolute; inset: 0; }
/* an idle "pick me" wiggle every few seconds while it's sealed */
.pku-nudge { animation: pku-nudge 3.6s ease-in-out 1.6s infinite; }
@keyframes pku-nudge {
  0%,74%,100% { transform: rotate(0) scale(1); }
  79% { transform: rotate(-4deg) scale(1.035); }
  84% { transform: rotate(3.5deg) scale(1.035); }
  89% { transform: rotate(-2deg) scale(1.01); }
  94% { transform: rotate(1deg) scale(1); }
}

/* Legendary / Wild: a drifting holographic band + glitter, inset to the pouch */
.pku-holo {
  position: absolute; left: 12.5%; right: 12.5%; top: 14.5%; bottom: 5%; z-index: 2;
  border-radius: 8px; overflow: hidden; pointer-events: none; mix-blend-mode: overlay;
}
.pku-holo i {
  position: absolute; top: -30%; bottom: -30%; left: -100%; width: 100%;
  background: linear-gradient(100deg, rgba(0,0,0,0) 0%, rgba(239,68,68,0.6) 16%, rgba(250,204,21,0.6) 31%, rgba(74,222,128,0.6) 46%, rgba(56,189,248,0.6) 61%, rgba(168,85,247,0.6) 76%, rgba(0,0,0,0) 92%);
  animation: pku-holo 4.6s ease-in-out infinite alternate; will-change: transform;
}
.pku-holo.gold i {
  background: linear-gradient(100deg, rgba(0,0,0,0) 12%, rgba(255,251,235,0.5) 36%, rgba(180,83,9,0.35) 50%, rgba(255,251,235,0.5) 64%, rgba(0,0,0,0) 88%);
  animation-duration: 3.8s;
}
@keyframes pku-holo { from { transform: translateX(-5%) skewX(-12deg); } to { transform: translateX(205%) skewX(-12deg); } }
.pku-holo b {
  position: absolute; width: 6px; height: 6px; margin: -3px; border-radius: 50%; opacity: 0;
  background: radial-gradient(circle, #fff 30%, rgba(255,255,255,0) 72%);
  animation: pku-glitter 2.2s ease-in-out infinite; will-change: transform, opacity;
}
@keyframes pku-glitter { 0%,100% { opacity: 0; transform: scale(0.3); } 50% { opacity: 1; transform: scale(1.5); } }

/* four-point glints winking on and around the pouch */
.pku-glints { position: absolute; inset: 0; pointer-events: none; z-index: 6; }
.pku-glints i, .pku-spark {
  background:
    radial-gradient(circle, #fff 0 15%, rgba(255,255,255,0) 30%),
    linear-gradient(currentColor, currentColor) 50% 50% / 2px 100% no-repeat,
    linear-gradient(currentColor, currentColor) 50% 50% / 100% 2px no-repeat;
}
.pku-glints i {
  position: absolute; width: 16px; height: 16px; margin: -8px; opacity: 0;
  animation: pku-twinkle 3.2s ease-in-out infinite; will-change: transform, opacity;
}
@keyframes pku-twinkle {
  0%,100% { opacity: 0; transform: scale(0.2) rotate(0); }
  12% { opacity: 1; transform: scale(1.15) rotate(45deg); }
  26% { opacity: 0; transform: scale(0.3) rotate(90deg); }
}

/* the tap-me hand: fades in after a beat, then keeps tapping the pouch */
.pku-tap {
  position: absolute; right: -6%; bottom: 2%; width: 26%; aspect-ratio: 24 / 28; z-index: 8;
  pointer-events: none; opacity: 0; animation: pku-fade-in 0.4s ease-out 1.3s forwards;
}
@keyframes pku-fade-in { to { opacity: 1; } }
.pku-tap-hand { position: absolute; inset: 0; transform-origin: 46% 8%; animation: pku-tap 1.3s ease-in-out 1.3s infinite; will-change: transform; }
@keyframes pku-tap {
  0%,100% { transform: translate(0,0) rotate(-14deg) scale(1); }
  30%,40% { transform: translate(-9%,-11%) rotate(-14deg) scale(0.86); }
  62% { transform: translate(0,0) rotate(-14deg) scale(1); }
}
.pku-tap-ring {
  position: absolute; left: 30%; top: -6%; width: 80%; aspect-ratio: 1; border-radius: 50%;
  border: 2px solid rgba(255,255,255,0.9); opacity: 0;
  animation: pku-tap-ring 1.3s ease-out 1.3s infinite; will-change: transform, opacity;
}
@keyframes pku-tap-ring {
  0%,30% { opacity: 0; transform: translate(-50%,-50%) scale(0.2); }
  36% { opacity: 0.95; }
  75%,100% { opacity: 0; transform: translate(-50%,-50%) scale(1.3); }
}

/* --- RIPPING / STRAINING (all driven by --tense on the pouch button) ---- */
/* the seam: a hot line where the crimp meets the body, spreading as it gives */
.pku-seam {
  position: absolute; left: 10%; right: 10%; top: 11.5%; height: 6.5%; z-index: 6; pointer-events: none;
  border-radius: 50%; transform-origin: 34% 50%;
  background: radial-gradient(closest-side, #fff 0%, rgba(255,255,255,0.9) 32%, rgba(var(--pku-rgb),0.75) 58%, rgba(var(--pku-rgb),0) 100%);
  opacity: calc(0.4 + var(--tense,0) * 0.6);
  transform: scaleX(calc(0.2 + var(--tense,0) * 0.95)) scaleY(calc(0.55 + var(--tense,0) * 1.3));
  transition: transform 0.16s ease-out, opacity 0.16s ease-out;
}
.pku-seam::before {
  content: ''; position: absolute; inset: -140% -12%; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--pku-rgb),0.5), rgba(var(--pku-rgb),0));
  animation: pku-seam-pulse 0.36s ease-in-out infinite alternate; will-change: opacity;
}
@keyframes pku-seam-pulse { from { opacity: 0.45; } to { opacity: 1; } }

/* light shafts pouring out of the seam — they lengthen, widen and brighten */
.pku-shafts { position: absolute; inset: 0; pointer-events: none; z-index: 4; }
.pku-shaft {
  position: absolute; bottom: 85.5%; transform-origin: 50% 100%;
  transform: translateX(-50%) rotate(var(--a)) scale(calc(0.35 + var(--tense,0) * 0.95), calc(0.06 + var(--tense,0) * 1.05));
  opacity: calc(var(--tense,0) * 1.35 - 0.05);
  transition: transform 0.2s ease-out, opacity 0.2s ease-out;
}
.pku-shaft svg { display: block; animation: pku-shaft-flicker 0.9s ease-in-out var(--d,0s) infinite alternate; will-change: opacity; }
@keyframes pku-shaft-flicker { from { opacity: 0.6; } to { opacity: 1; } }

/* the ray wheel behind the pouch: wakes with the rip, whips at the brink */
.pku-raywheel {
  position: absolute; left: 50%; top: 44%; width: 250%; aspect-ratio: 1; margin: -125% 0 0 -125%;
  pointer-events: none; opacity: calc(var(--tense,0) * 0.95);
  transform: scale(calc(0.4 + var(--tense,0) * 0.7));
  transition: transform 0.25s ease-out, opacity 0.25s ease-out;
}
.pku-raywheel-spin { position: absolute; inset: 0; animation: pku-spin 9s linear infinite; will-change: transform; }
.pku-raywheel-spin.whip { animation-duration: 1.2s; }
@keyframes pku-spin { to { transform: rotate(360deg); } }

/* foil creases crinkling in as the pouch is worked (static; opacity rides --tense) */
.pku-creases { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; opacity: calc(var(--tense,0) * 1.15 - 0.2); }

/* energy cracks flickering across the seam once it's hot */
.pku-crackle { position: absolute; inset: 0; pointer-events: none; z-index: 7; animation: pku-crackle 0.46s steps(1) infinite; }
@keyframes pku-crackle { 0% { opacity: 0.95; } 18% { opacity: 0.1; } 30% { opacity: 0.85; } 52% { opacity: 0; } 70% { opacity: 1; } 86% { opacity: 0.25; } }

/* sparks streaming up off the seam: 3 at first, the whole tier's worth when hot */
.pku-embers { position: absolute; left: 14%; right: 14%; top: 14.5%; height: 0; pointer-events: none; z-index: 6; }
.pku-embers i {
  position: absolute; left: calc(50% + var(--x)); top: 0; width: 5px; height: 5px; margin: -2.5px;
  border-radius: 50%; box-shadow: 0 0 6px 1px rgba(255,255,255,0.75); opacity: 0;
  animation: pku-ember 1.1s ease-out infinite; will-change: transform, opacity;
}
.pku-embers:not(.hot) i:nth-child(n+4) { display: none; }
.pku-embers.brink i { animation-duration: 0.68s; }
@keyframes pku-ember {
  0% { opacity: 0; transform: translate(0,0) scale(0.6); }
  15% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--dx,0), -120px) scale(1.2); }
}

/* foil flecks knocked off by every tap (re-keyed per tap) */
.pku-flecks { position: absolute; left: 50%; top: 14.5%; width: 0; height: 0; pointer-events: none; z-index: 9; }
.pku-flecks i {
  position: absolute; width: 7px; height: 4px; margin: -2px -3.5px; border-radius: 1px;
  animation: pku-fleck 0.62s cubic-bezier(0.2,0.8,0.4,1) forwards; will-change: transform, opacity;
}
@keyframes pku-fleck {
  0% { opacity: 1; transform: translate(0,0) rotate(0) scale(1); }
  55% { opacity: 1; transform: translate(calc(var(--fx) * 0.8), var(--fy)) rotate(260deg) scale(0.9); }
  100% { opacity: 0; transform: translate(var(--fx), calc(var(--fy) + 34px)) rotate(460deg) scale(0.6); }
}

/* the brink: rings of energy drawn IN toward the pouch */
.pku-charge {
  position: absolute; left: 50%; top: 46%; width: 190%; aspect-ratio: 1; margin: -95% 0 0 -95%;
  border-radius: 50%; border: 3px dashed rgba(var(--pku-rgb),0.9); pointer-events: none; z-index: 1;
  opacity: 0; animation: pku-charge 0.5s ease-in infinite; will-change: transform, opacity;
}
.pku-charge::after {
  content: ''; position: absolute; inset: 14%; border-radius: 50%; border: 2px solid rgba(255,255,255,0.75);
}
@keyframes pku-charge { 0% { transform: scale(1.25); opacity: 0; } 30% { opacity: 0.9; } 100% { transform: scale(0.42); opacity: 0; } }

.pku-heartbeat { animation: pku-heartbeat 0.62s ease-out infinite; will-change: transform; }
@keyframes pku-heartbeat {
  0% { transform: scale(1); } 14% { transform: scale(1.07); } 28% { transform: scale(0.99); }
  42% { transform: scale(1.045); } 64%,100% { transform: scale(1); }
}

/* --- TEAR ---------------------------------------------------------------- */
/* the body splits down the middle, the halves fly apart on the 0.26s beat */
.pku-half { position: absolute; top: 0; bottom: 0; width: 50%; overflow: hidden; will-change: transform, opacity; }
.pku-half.l { left: 0; transform-origin: 20% 100%; animation: pku-half-l 0.64s cubic-bezier(0.3,0.1,0.5,1) 0.26s forwards; }
.pku-half.r { right: 0; transform-origin: 80% 100%; animation: pku-half-r 0.64s cubic-bezier(0.3,0.1,0.5,1) 0.26s forwards; }
.pku-half-in { position: absolute; top: 0; height: 100%; width: 200%; }
.pku-half.l .pku-half-in { left: 0; }
.pku-half.r .pku-half-in { right: 0; }
.pku-rag { position: absolute; top: 0; height: 100%; width: 14%; opacity: 0; animation: pku-fade-in 0.06s linear 0.26s forwards; }
.pku-half.l .pku-rag { right: 0; }
.pku-half.r .pku-rag { left: 0; }
@keyframes pku-half-l {
  0% { transform: translate(0,0) rotate(0); opacity: 1; }
  18% { transform: translate(-7%,-2%) rotate(-5deg); opacity: 1; }
  60% { opacity: 1; }
  100% { transform: translate(-105%,26%) rotate(-40deg); opacity: 0; }
}
@keyframes pku-half-r {
  0% { transform: translate(0,0) rotate(0); opacity: 1; }
  18% { transform: translate(7%,-2%) rotate(5deg); opacity: 1; }
  60% { opacity: 1; }
  100% { transform: translate(105%,26%) rotate(40deg); opacity: 0; }
}
/* the cards' own light, bursting out of the split */
.pku-core {
  position: absolute; left: 50%; top: 44%; width: 130%; aspect-ratio: 1; margin: -65% 0 0 -65%;
  border-radius: 50%; pointer-events: none; z-index: 3; opacity: 0;
  background: radial-gradient(closest-side, #fff 0 18%, rgba(var(--pku-rgb),0.85) 40%, rgba(var(--pku-rgb),0) 76%);
  animation: pku-core 0.72s ease-out 0.22s forwards; will-change: transform, opacity;
}
@keyframes pku-core { 0% { opacity: 0; transform: scale(0.2); } 22% { opacity: 1; transform: scale(1); } 100% { opacity: 0; transform: scale(1.7); } }
/* three card backs rise out of the pouch and fan, handing off to the deal */
.pku-rise { position: absolute; left: 50%; top: 46%; width: 0; height: 0; z-index: 5; pointer-events: none; }
.pku-rise i {
  position: absolute; width: 64px; height: 86px; margin: -43px 0 0 -32px; border-radius: 8px; border: 2px solid;
  box-shadow: 0 0 18px rgba(var(--pk-glow,255,255,255),0.65), inset 0 0 0 3px rgba(0,0,0,0.35);
  opacity: 0; animation: pku-rise 0.6s cubic-bezier(0.2,1.3,0.4,1) 0.3s forwards; will-change: transform, opacity;
}
@keyframes pku-rise {
  0% { opacity: 0; transform: translate(0,34px) scale(0.45) rotate(0); }
  30% { opacity: 1; }
  100% { opacity: 1; transform: translate(calc(var(--f) * 40px), calc(-74px + var(--f) * var(--f) * 10px)) rotate(calc(var(--f) * 16deg)) scale(1); }
}
/* each rising card carries the back's diamond */
.pku-rise i::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: 22px; height: 22px; margin: -11px;
  border: 2px solid rgba(255,255,255,0.75); border-radius: 3px; transform: rotate(45deg);
  background: rgba(var(--pk-glow,255,255,255),0.35);
}
/* long curling foil shreds (they borrow .pack-shrapnel's flight) */
.pack-shrapnel.pku-shred { width: 4px; height: 20px; border-radius: 2px; animation-duration: 1s; animation-fill-mode: both; }

/* --- REVEAL -------------------------------------------------------------- */
/* the deal: from the pouch's middle, fanned, into place (overrides card-land) */
.card-land.pku-deal { animation: pku-deal 0.66s cubic-bezier(0.2,0.9,0.3,1.15) backwards; }
@keyframes pku-deal {
  0% { opacity: 0; transform: translate3d(calc(var(--fan,0) * -46px), -110px, 0) rotate(calc(var(--fan,0) * -22deg)) scale(0.35); }
  55% { opacity: 1; transform: translate3d(0,6px,0) rotate(calc(var(--fan,0) * 2deg)) scale(1.05); }
  78% { transform: translate3d(0,-3px,0) rotate(calc(var(--fan,0) * -1deg)) scale(0.985); }
  100% { opacity: 1; transform: translate3d(0,0,0) rotate(0) scale(1); }
}
/* the flip: lift + wind back, whip over, overshoot, settle (the hero flip
   keeps its own slower keyframes — .flip-hero outranks this) */
.flip-inner.flipped.flip-punch { animation: pku-flip 0.64s cubic-bezier(0.35,0,0.25,1) forwards; }
@keyframes pku-flip {
  0% { transform: translateY(0) rotateY(0) translateZ(0) scale(1); }
  20% { transform: translateY(-9px) rotateY(-16deg) translateZ(34px) scale(1.06); }
  58% { transform: translateY(-12px) rotateY(118deg) translateZ(52px) scale(1.1); }
  82% { transform: translateY(-2px) rotateY(188deg) translateZ(12px) scale(1.02); }
  100% { transform: translateY(0) rotateY(180deg) translateZ(0) scale(1); }
}
/* the rarity ray-star behind every flipped card */
.pku-flipburst {
  position: absolute; left: 50%; top: 50%; width: 220%; aspect-ratio: 1; margin: -110% 0 0 -110%;
  pointer-events: none; opacity: 0; animation: pku-flipburst 0.9s cubic-bezier(0.15,0.7,0.3,1) 0.18s both;
  will-change: transform, opacity;
}
.pku-flipburst.k-common { width: 170%; margin: -85% 0 0 -85%; }
.pku-flipburst.k-epic, .pku-flipburst.k-legendary, .pku-flipburst.k-wild { width: 270%; margin: -135% 0 0 -135%; }
.pku-flipburst.k-legendary, .pku-flipburst.k-wild { animation-duration: 1.7s; }
.pku-flipburst.hero { animation-delay: 0.5s; }
@keyframes pku-flipburst { 0% { opacity: 0; transform: scale(0.3) rotate(0); } 20% { opacity: 1; } 100% { opacity: 0; transform: scale(1.2) rotate(38deg); } }
.pku-spark {
  position: absolute; left: 50%; top: 50%; width: 15px; height: 15px; margin: -7.5px; pointer-events: none; z-index: 12;
  opacity: 0; animation: pku-spark 0.75s cubic-bezier(0.15,0.8,0.3,1) both; will-change: transform, opacity;
}
@keyframes pku-spark {
  0% { opacity: 0; transform: translate(0,0) scale(0.3) rotate(0); }
  20% { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--sx), var(--sy)) scale(1.1) rotate(180deg); }
}
/* NEW! — a rubber stamp slammed onto the card's corner */
.pku-stamp {
  position: absolute; bottom: 21%; right: -12px; z-index: 16; pointer-events: none;
  padding: 4px 7px 3px; border: 2px solid #34d399; border-radius: 6px; line-height: 1;
  font-size: 11px; font-weight: 900; letter-spacing: 0.12em; text-transform: uppercase; color: #6ee7b7;
  background: #064e3b;
  box-shadow: 0 0 0 2px #064e3b, 0 5px 12px rgba(0,0,0,0.55), inset 0 0 0 1px rgba(52,211,153,0.45), 0 0 16px rgba(52,211,153,0.45);
  animation: pku-stamp 0.42s cubic-bezier(0.3,1.6,0.5,1) 0.42s both;
}
.pku-stamp.hero { animation-delay: 1.02s; }
.pku-stamp::after {
  content: ''; position: absolute; inset: -7px; border-radius: 11px; border: 2px solid rgba(52,211,153,0.75);
  opacity: 0; animation: pku-stamp-ring 0.5s ease-out 0.56s both;
}
.pku-stamp.hero::after { animation-delay: 1.16s; }
@keyframes pku-stamp { 0% { opacity: 0; transform: rotate(-12deg) scale(2.7); } 60% { opacity: 1; transform: rotate(15deg) scale(0.9); } 100% { opacity: 1; transform: rotate(12deg) scale(1); } }
@keyframes pku-stamp-ring { 0% { opacity: 0.9; transform: scale(0.8); } 100% { opacity: 0; transform: scale(1.7); } }
.pku-newpill { box-shadow: 0 0 12px rgba(255,255,255,0.18); }
/* a duplicate's star spins in and throws off little stars */
.pku-dupe { position: relative; }
.pku-dupe-star { display: inline-flex; animation: pku-dupe-star 0.7s cubic-bezier(0.2,1.5,0.4,1) 0.45s both; }
@keyframes pku-dupe-star { 0% { transform: scale(0) rotate(-180deg); } 70% { transform: scale(1.55) rotate(20deg); } 100% { transform: scale(1) rotate(0); } }
.pku-dupe-fly {
  position: absolute; right: 12px; top: 50%; width: 6px; height: 6px; margin: -3px; background: #fbbf24;
  box-shadow: 0 0 6px rgba(251,191,36,0.9); opacity: 0; animation: pku-dupe-fly 0.7s ease-out 0.55s both;
}
.pku-dupe-fly:nth-of-type(1) { --dx: -16px; --dy: -18px; }
.pku-dupe-fly:nth-of-type(2) { --dx: 16px; --dy: -18px; }
.pku-dupe-fly:nth-of-type(3) { --dx: -20px; --dy: 12px; }
.pku-dupe-fly:nth-of-type(4) { --dx: 20px; --dy: 12px; }
@keyframes pku-dupe-fly { 0% { opacity: 0; transform: translate(0,0) rotate(45deg) scale(0.4); } 25% { opacity: 1; } 100% { opacity: 0; transform: translate(var(--dx),var(--dy)) rotate(45deg) scale(1); } }
/* the hero card's shimmer, sweeping twice through its slow-mo hold */
.pku-hero-shine { position: absolute; inset: 0; border-radius: 0.75rem; overflow: hidden; pointer-events: none; z-index: 8; }
.pku-hero-shine i {
  position: absolute; top: -20%; bottom: -20%; left: 0; width: 45%;
  background: linear-gradient(90deg, rgba(255,247,214,0), rgba(255,247,214,0.8) 50%, rgba(255,247,214,0));
  transform: translateX(-120%) skewX(-18deg); animation: pku-hero-shine 0.55s ease-in-out 0.72s 2 both; will-change: transform;
}
.pku-hero-shine.wild i { background: linear-gradient(90deg, rgba(244,114,182,0), rgba(244,114,182,0.55) 28%, rgba(250,204,21,0.6) 50%, rgba(56,189,248,0.55) 72%, rgba(56,189,248,0)); }
@keyframes pku-hero-shine { 0% { transform: translateX(-120%) skewX(-18deg); } 100% { transform: translateX(300%) skewX(-18deg); } }
/* the letterbox's title card */
.pku-hero-title {
  position: absolute; left: 0; right: 0; top: 50%; text-align: center; white-space: nowrap;
  font-size: clamp(18px, 4.2vh, 34px); font-weight: 900; letter-spacing: 0.3em; text-transform: uppercase;
  color: #fde68a; text-shadow: 0 0 18px rgba(251,191,36,0.85), 0 2px 0 #78350f;
  opacity: 0; animation: pku-hero-title 1.8s ease-out both;
}
.pku-hero-title.wild { color: #fbcfe8; text-shadow: 0 0 18px rgba(244,114,182,0.9), 0 2px 0 #581c87; }
@keyframes pku-hero-title {
  0%,12% { opacity: 0; transform: translateY(-50%) scale(1.5); }
  24% { opacity: 1; transform: translateY(-50%) scale(1); }
  78% { opacity: 1; transform: translateY(-50%) scale(1.04); }
  92%,100% { opacity: 0; transform: translateY(-50%) scale(1.04); }
}
.pku-hero-rule { position: absolute; left: 12%; right: 12%; bottom: 20%; height: 1px; background: linear-gradient(90deg, rgba(251,191,36,0), #fbbf24, rgba(251,191,36,0)); opacity: 0.8; }

/* --- DONE ---------------------------------------------------------------- */
/* the fanned hand (transform set inline per card, per row) */
.pku-fanned { transform-origin: 50% 135%; transition: transform 0.6s cubic-bezier(0.2,1.5,0.4,1); }
/* the haul's name, stamped in */
.pku-haul { position: relative; display: inline-block; padding: 4px 14px; animation: pku-haul-in 0.55s cubic-bezier(0.2,1.6,0.4,1) 0.15s both; }
.pku-haul::before, .pku-haul::after {
  content: ''; position: absolute; top: 50%; width: 30px; height: 2px; margin-top: -1px;
  background: linear-gradient(90deg, rgba(0,0,0,0), var(--hc));
}
.pku-haul::before { right: 100%; }
.pku-haul::after { left: 100%; transform: scaleX(-1); }
.pku-haul-txt {
  position: relative; z-index: 1; display: inline-block; font-size: 15px; letter-spacing: 0.22em; color: var(--hc);
  text-shadow: 0 0 14px rgba(var(--hrgb),0.75), 0 2px 4px rgba(0,0,0,0.85);
}
.pku-haul[data-party="0"] .pku-haul-txt { font-size: 13px; }
.pku-haul[data-party="3"] .pku-haul-txt { font-size: 17px; }
@keyframes pku-haul-in { 0% { opacity: 0; transform: scale(2.3); } 60% { opacity: 1; transform: scale(0.94); } 100% { opacity: 1; transform: scale(1); } }
.pku-haul-rays {
  position: absolute; left: 50%; top: 50%; width: 230px; height: 230px; margin: -115px 0 0 -115px; pointer-events: none;
  opacity: 0; animation: pku-fade-in 0.8s ease-out 0.25s forwards, pku-spin 16s linear infinite; will-change: transform, opacity;
}
/* the tally chips + odometer digits */
.pku-tally { display: flex; gap: 8px; justify-content: center; }
.pku-chip {
  display: inline-flex; flex-direction: column; align-items: center; min-width: 64px; padding: 6px 10px 5px;
  border-radius: 12px; background: rgba(28,25,23,0.85); border: 1px solid rgba(255,255,255,0.12);
  box-shadow: inset 0 1px 0 rgba(255,255,255,0.08), 0 4px 12px rgba(0,0,0,0.35);
  animation: pku-chip-in 0.42s cubic-bezier(0.2,1.5,0.4,1) 0.45s both;
}
.pku-chip:nth-child(2) { animation-delay: 0.57s; }
.pku-chip:nth-child(3) { animation-delay: 0.69s; }
@keyframes pku-chip-in { 0% { opacity: 0; transform: translateY(10px) scale(0.7); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
.pku-chip b { display: inline-flex; font-size: 20px; line-height: 1; font-weight: 900; color: #f5f5f4; }
.pku-chip em {
  display: inline-flex; align-items: center; gap: 3px; margin-top: 3px; font-style: normal;
  font-size: 9px; font-weight: 800; letter-spacing: 0.18em; text-transform: uppercase; color: #a8a29e;
}
.pku-chip.new { border-color: rgba(52,211,153,0.45); }
.pku-chip.new b { color: #34d399; }
.pku-chip.stars { border-color: rgba(251,191,36,0.45); }
.pku-chip.stars b { color: #fbbf24; }
.pku-roll { display: inline-flex; }
.pku-roll-d { display: inline-block; height: 1em; line-height: 1em; overflow: hidden; vertical-align: top; }
.pku-roll-col { display: flex; flex-direction: column; animation: pku-roll 0.95s cubic-bezier(0.2,0.7,0.2,1) both; will-change: transform; }
.pku-roll-col > span { display: block; height: 1em; line-height: 1em; }
@keyframes pku-roll { from { transform: translateY(0); } }
/* a legendary haul rains gold coins (they flip as they fall) */
.pku-coin {
  position: absolute; top: 0; width: 16px; height: 16px; border-radius: 50%; opacity: 0;
  background: radial-gradient(circle at 35% 30%, #fffbeb 0 12%, #fcd34d 32%, #d97706 76%, #92400e 100%);
  box-shadow: inset 0 0 0 2px rgba(146,64,14,0.5);
  animation: pku-coin 2.4s linear both; will-change: transform, opacity;
}
@keyframes pku-coin { 0% { transform: translateY(-10vh) rotateY(0); opacity: 0; } 10% { opacity: 1; } 100% { transform: translateY(108vh) rotateY(900deg); opacity: 0.9; } }

/* --- performance mode (phones): drop the extras, keep the story ------- */
.performance-mode .pku-x, .performance-mode .pku-glints, .performance-mode .pku-holo b,
.performance-mode .pku-embers i:nth-child(n+5) { display: none !important; }
.performance-mode .pku-seam::before, .performance-mode .pku-shaft svg { animation: none !important; }

/* --- reduced motion: nothing loops, one-shots land at once --------------- */
.reduced-motion .pku-aura i, .reduced-motion .pku-raywheel-spin, .reduced-motion .pku-nudge,
.reduced-motion .pku-heartbeat, .reduced-motion .pku-holo i, .reduced-motion .pku-holo b,
.reduced-motion .pku-tap-hand, .reduced-motion .pku-seam::before, .reduced-motion .pku-shaft svg,
.reduced-motion .pku-crackle, .reduced-motion .pku-haul-rays, .reduced-motion .pku-shadow.float,
.reduced-motion .pku-dim.inhale { animation: none !important; }
.reduced-motion .pku-embers, .reduced-motion .pku-flecks, .reduced-motion .pku-spark, .reduced-motion .pku-coin,
.reduced-motion .pku-glints, .reduced-motion .pku-charge, .reduced-motion .pku-tap-ring,
.reduced-motion .pku-hero-shine, .reduced-motion .pku-holo b { display: none !important; }
.reduced-motion .pku-tap, .reduced-motion .pku-haul-rays { animation: none !important; opacity: 1; }
.reduced-motion .pku-half, .reduced-motion .pku-core, .reduced-motion .pku-rise i, .reduced-motion .pku-flipburst,
.reduced-motion .pku-stamp, .reduced-motion .pku-stamp::after, .reduced-motion .pku-dupe-star, .reduced-motion .pku-dupe-fly,
.reduced-motion .pku-hero-title, .reduced-motion .pku-haul, .reduced-motion .pku-chip, .reduced-motion .pku-roll-col,
.reduced-motion .pack-shrapnel.pku-shred { animation-duration: 0.01s !important; animation-delay: 0s !important; animation-iteration-count: 1 !important; }
.reduced-motion .pku-lean, .reduced-motion .pku-fanned, .reduced-motion .pku-shaft, .reduced-motion .pku-seam,
.reduced-motion .pku-aura, .reduced-motion .pku-raywheel, .reduced-motion .pku-dim, .reduced-motion .pku-tagline { transition: none !important; }
