/* ============================================================
   Slushy · Casino Overlay Demo
   Faithful clone of the Slushy.com feed UI with transparent
   casino games layered on top. Their type (Acumin Pro Wide),
   chartreuse green + hot pink accents, black nav bar.
   ============================================================ */

/* ---------- TYPE ----------
   All five faces now come off our own Adobe Fonts kit (`lez7jmz`, linked in
   index.html), which is face-for-face identical to theirs (`waa4fqp`):

     body / UI    acumin-pro-wide          400 and 800
     display      rift 700                 (the gems sheet, in the app)
     kickers      normalidad-ultraextended 900   ("MY GOALS", "STARTING IN")
     numerals     monotalic-wide 700             (the crash multiplier)

   History, because it explains two rules below. The demo was on Nunito — a
   rounded Google face with nothing in common with theirs — which is why Ryan
   read our type as "all too light". It then ran on two Acumin .woff2 files
   lifted off slushygames.net, which matched but were not licensed to us. Both
   are gone; the kit is the licensed route, and Adobe's terms forbid
   self-hosting so there is no @font-face here at all.

   **Weights are 400 and 800, not 400 and 900.** 800 is what Adobe ships
   Acumin Pro Wide Black as, and what their kit requests. CSS font matching
   walks *up* from the requested weight, so every 600/700/800/900 in this
   stylesheet still lands on Black — but writing 800 means a diff against
   their site matches line for line.

   The kickers and the crash multiplier used to be faked with Acumin Black
   tracked out, because we had no licence for Normalidad or Monotalic. We do
   now, so --display and --numeral are the real faces and the fake tracking
   is gone. See docs/slushy-font-licensing.md. */

:root {
  --bg: #0a0a0c;
  --text: #ffffff;
  --text-dim: rgba(255, 255, 255, 0.7);

  /* Measured off slushygames.net, 2 Aug 2026 — docs/slushygames-net-recon.md.
     These are their literal Tailwind -700 values. The old #8ce32e/#ff2e93 were
     screenshot samples taken through a dark overlay, so they read muddy next
     to the real thing. */
  --sl-base: #1F2123;       /* page + nav background */
  --sl-green: #73FF00;      /* multipliers, player counts, money, progress */
  --sl-pink: #FF5380;       /* LIVE NOW, active nav, CTA gradient start */
  --sl-magenta: #E552C5;    /* CTA gradient end */
  --gem: #e879f9;
  --green: #73FF00;
  --green-deep: #57c400;
  --red: #ff3b57;
  --gold: #ffd34d;

  --chip-bg: rgba(35, 35, 38, 0.5);    /* their translucent button grey */
  --chip-ring: 1px solid rgba(255, 255, 255, 0.28);
  --font: 'acumin-pro-wide', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  /* Kickers/labels — the real normalidad-ultraextended now. This used to be
     Acumin tracked out to fake an ultra-extended face; the tracking that
     compensated for that is gone from .goal-kicker with it. Their own stack
     carries no fallback beyond sans-serif, so neither does ours. */
  --display: 'normalidad-ultraextended', -apple-system, BlinkMacSystemFont, sans-serif;
  /* The crash multiplier readout. Monospaced by design — the digits must not
     jitter as the number climbs, which is the whole reason they picked it. */
  --numeral: 'monotalic-wide', ui-monospace, SFMono-Regular, Menlo, monospace;

  --nav-h: 62px;
  --phone-w: 393px;
  --phone-h: 852px;

  /* ---------- HUD STACK ----------
     Everything in the casino overlay is absolutely positioned off the bottom
     of `.game` (which is already inset by --nav-h, so these clear the app's
     nav and its safe-area inset on their own). They used to be loose magic
     numbers, and adding the tab strip meant re-deriving five of them by hand
     — which is exactly how the Plinko-pegs-over-the-creator-name and
     blackjack-behind-the-room-goal bugs happened. One ladder now, bottom-up,
     each value the one below it plus that element's height plus its gap:

       tabs     0    → 44   (6 + 32 pill + 6)
       bet bar  48   → 112  (9 + 46 CTA + 9, + 4 gap)
       dots     128  → 140
       room goal 150 → 211  (10 kicker + 7 + 44 row)
       presence 220 → 242
       games must not draw below --hud-clear.

     Net change from before the tab strip: +14px of furniture, because
     deleting the personal-goal block gave most of the 44 back. */
  --hud-tabs: 44px;
  --hud-betbar: 48px;
  --hud-dots: 128px;
  --hud-goal: 150px;
  --hud-live: 220px;
  --hud-clear: 252px;
  /* The Plinko bucket baseline, republished by plinko's layout() each time it
     sizes. Dice sits on the same line so the two games line up. The fallback
     matches the room-goal-anchored case, which is the common one. */
  --hud-boardfoot: 225px;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html, body {
  height: 100%;
  background: #060608;
  font-family: var(--font);
  color: var(--text);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
  touch-action: manipulation;   /* no double-tap zoom (rapid DROP taps) */
}
button { touch-action: manipulation; }

/* ---- desktop creator nav ----
   Measured off slushygames.net, 3 Aug 2026. Every number here is theirs:
   44x44, rgba(255,255,255,0.1) on a 2px rgba(255,255,255,0.2) ring, blurred
   behind, 12px apart, 15px clear of the card's right edge, centred on the
   card's height, and `transform .15s cubic-bezier(.4,0,.2,1)` with a 1.1 hover.

   Hidden by default — it only appears in the media query below, where there is
   actually a phone frame to sit beside. On a handset the swipe is the
   affordance and there is no margin to put these in. */
.girl-rail { display: none; }
.girl-arrow {
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 9999px;
  border: 2px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.1);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
  color: #fff;
  cursor: pointer;
  padding: 0;
  transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.girl-arrow svg { width: 18px; height: 18px; display: block; }
.girl-arrow:hover { transform: scale(1.1); }
.girl-arrow:active { transform: scale(0.95); }
@media (prefers-reduced-motion: reduce) {
  .girl-arrow { transition: none; }
  .girl-arrow:hover, .girl-arrow:active { transform: none; }
}

.stage {
  position: relative;
  height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  background:
    radial-gradient(1200px 700px at 20% -10%, rgba(255, 46, 147, 0.10), transparent 60%),
    radial-gradient(1000px 600px at 90% 110%, rgba(140, 227, 46, 0.07), transparent 60%),
    #060608;
}

/* mobile: fill the real screen edge-to-edge, no frame */
.phone {
  position: relative;
  width: 100vw;
  height: 100dvh;
  background: var(--bg);
  overflow: hidden;
  isolation: isolate;
}

/* desktop: centered phone frame */
@media (min-width: 500px) and (min-height: 700px) {
  .phone {
    width: min(var(--phone-w), 100vw);
    height: min(var(--phone-h), calc(100dvh - 48px));
    border-radius: 44px;
    border: 1px solid rgba(255, 255, 255, 0.14);
    box-shadow:
      0 0 0 10px #17171c,
      0 0 0 11px rgba(255, 255, 255, 0.08),
      0 40px 90px rgba(0, 0, 0, 0.75),
      0 0 120px rgba(255, 46, 147, 0.10);
  }

  /* Beside the frame, centred on it. The 10px bezel in the box-shadow above is
     part of the phone's visual edge, so the 15px they leave is measured from
     that rather than from the element box — hence 10 + 15. */
  .girl-rail {
    display: grid;
    gap: 12px;
    position: absolute;
    top: 50%;
    left: calc(50% + min(var(--phone-w), 100vw) / 2 + 25px);
    transform: translateY(-50%);
    z-index: 5;
  }
}

/* ============================================================
   TOP CHROME — For You ▾ + icon cluster
   ============================================================ */
.topbar {
  position: absolute;
  top: 0; left: 0; right: 0;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(env(safe-area-inset-top, 0px) + 12px) 12px 8px;
  background: linear-gradient(rgba(0, 0, 0, 0.45), transparent);
  pointer-events: none;
}
.topbar > * { pointer-events: auto; }

.top-left {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.brandmark {
  height: 34px;
  width: auto;
  display: block;
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.5));
}

/* The brandmark doubles as the feed picker trigger — their "slushy ▾" pill. */
.brandpick {
  /* Bare lockup, no pill (Luke, 2 Aug). Background, border and blur removed;
     it's just the logo, tappable, with a comfortable hit area. */
  display: flex;
  align-items: center;
  height: 40px;
  padding: 0;
  border: 0;
  background: none;
  color: #fff;
  cursor: pointer;
}

.top-icons { display: flex; gap: 7px; }
.tico {
  width: 32px; height: 32px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: var(--chip-ring);
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: #fff;
  cursor: pointer;
}
.tico svg { width: 16px; height: 16px; }

/* ============================================================
   FEED
   ============================================================ */
.feed {
  position: absolute;
  inset: 0 0 var(--nav-h) 0;
  z-index: 10;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  overscroll-behavior: contain;
  scrollbar-width: none;
  /* JS owns BOTH swipe axes (one gesture arbiter, TikTok-style) — leaving
     pan-y native made the browser race our direction lock and steal
     diagonal swipes as feed scrolls. Wheel/trackpad still scroll natively. */
  touch-action: none;
  background: #000;
}
/* snap suspended while a finger drags the feed; restored after settle */
.feed.nosnap { scroll-snap-type: none; }
.feed::-webkit-scrollbar { display: none; }

.slide {
  position: relative;
  height: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  overflow: hidden;
}

/* fallback scene while the stream buffers */
.scene {
  position: absolute;
  inset: -6%;
  animation: scenePan 22s ease-in-out infinite alternate;
}
@keyframes scenePan {
  0% { transform: scale(1.05) translate(0, 0); }
  100% { transform: scale(1.14) translate(-2%, -1.5%); }
}
.scene .blob {
  position: absolute;
  border-radius: 50%;
  filter: blur(70px);
  opacity: 0.85;
  animation: blobDrift 14s ease-in-out infinite alternate;
}
.scene .blob.b2 { animation-duration: 18s; animation-delay: -6s; }
@keyframes blobDrift {
  0% { transform: translate(0, 0) scale(1); }
  100% { transform: translate(9%, -7%) scale(1.25); }
}

.slide video.stream {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity 400ms ease;
}
.slide video.stream.ready { opacity: 1; }

/* Readability scrims — both of them measured off slushygames.net's live page
   (2 Aug 2026), not approximated:

     top     h-[20%]  bg-gradient-to-b from-gray-700/80 to-transparent
     bottom  h-[55%]  bg-gradient-to-t from-gray-700 via-gray-700/80 to-transparent

   Ryan asked for "a gradient from the bottom up as well" — we only had a weak
   50%-black bottom scrim over the last 30%. Theirs is the brand base at FULL
   opacity where it meets the nav, holds 80% through the middle, and only
   clears at 55% up. That solid foot is what makes their goal rows, CTA and
   bet bar readable over any frame; ours were fighting the video, which is the
   other half of "you can read their progress bar, you can't read ours".

   The end colour is an explicit alpha-0 of the base, never `transparent` —
   `transparent` is rgba(0,0,0,0) and interpolating to it smears grey through
   the ramp (same trap as the creators sticky header). */
.slide::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    /* Eased tails, not straight ramps.

       Both scrims used to run to zero in one linear step — 0.8 to 0 over 20%
       at the top, 0.8 to 0 over the last 33% at the bottom. A straight ramp
       holds a constant rate of change and then stops dead at the end stop, and
       that discontinuity is what the eye picks up: a Mach band across the
       full width where the gradient dies. Over a dark frame, 8-bit alpha
       quantisation makes the same terminator visible as a step.

       The extra stops follow an ease-out, so the alpha approaches zero
       asymptotically and there is no point where the slope jumps. The density
       through the useful range is unchanged — the measured spec has the top
       at 0.40 by 10% and these hit 0.40 at 10% too, and the bottom's 0.8 at
       22% is kept exactly. Only the tail moves, which is the part that was
       drawing the line. */
    linear-gradient(
      to bottom,
      rgba(31, 33, 35, 0.8) 0%,
      rgba(31, 33, 35, 0.62) 4%,
      rgba(31, 33, 35, 0.47) 8%,
      rgba(31, 33, 35, 0.32) 12%,
      rgba(31, 33, 35, 0.19) 16%,
      rgba(31, 33, 35, 0.1) 20%,
      rgba(31, 33, 35, 0.05) 24%,
      rgba(31, 33, 35, 0.02) 28%,
      rgba(31, 33, 35, 0) 33%
    ),
    linear-gradient(
      to top,
      rgba(31, 33, 35, 1) 0%,
      rgba(31, 33, 35, 0.8) 22%,
      rgba(31, 33, 35, 0.6) 30%,
      rgba(31, 33, 35, 0.45) 36%,
      rgba(31, 33, 35, 0.3) 42%,
      rgba(31, 33, 35, 0.18) 47%,
      rgba(31, 33, 35, 0.09) 51%,
      rgba(31, 33, 35, 0.035) 55%,
      rgba(31, 33, 35, 0) 60%
    );
}

/* --- creator header (under topbar, like Slushy) --- */
.creator-head {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 64px);
  left: 14px;
  right: 14px;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 10px;
}
.avatar {
  width: 46px; height: 46px;
  border-radius: 14px;
  display: grid;
  place-items: center;
  font-weight: 900;
  font-size: 18px;
  color: #fff;
  border: 2px solid transparent;
  background-clip: padding-box;
  position: relative;
  flex-shrink: 0;
}
.avatar::before {
  content: '';
  position: absolute;
  inset: -3px;
  border-radius: 16px;
  padding: 2px;
  background: linear-gradient(135deg, var(--sl-pink), #a855f7);
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.avatar img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 12px;
  z-index: 1;
}
.creator-id { min-width: 0; flex: 1; }
/* Measured off slushygames.net's own feed card, 2 Aug: the name is
   12px/900 UPPERCASE white and the line under it is 8px/900 uppercase —
   "LIVE NOW" in pink, then the count in green and its noun in white-60.
   Ours was 16.5px name over a 13.5px stats line, which is why Luke read it
   as "basic as fuck" next to theirs: nearly half again as big, mixed case,
   and no hierarchy between the number and the word after it. */
.creator-name {
  font-weight: 900;
  font-size: 12px;
  line-height: 15px;
  text-transform: uppercase;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8);
}
/* 10px, and this is a DELIBERATE DIVERGENCE from slushygames.net — the one
   place we knowingly do not match them.

   Theirs is 8px/900/10px-line-height, and ours was too. Re-measured 3 Aug on
   their own feed card ("Live now" #FF5380, "2.1K" #73FF00, "Players"
   white/60): all three are 8px/900. So the type spec was right and there was
   no bug here.

   Luke, 3 Aug, reading the live build on his phone: "on the home page where
   it says live, and views, and the no of views, that text is too small". At
   8px on a real handset it is, and a spec being faithfully copied does not
   make it readable.

   10px rather than 9 or 11 because it is a step on THEIR scale
   (type-spec §5.3: 8 / 10 / 12 / 13 / 14 / 16) and it is the size they
   themselves use for the same kicker one level up — `MY GOALS` inside the
   Goals tab is 10px/900 uppercase (§4). Line-height follows §5.5's 1.25×.
   If parity is ever preferred over legibility here, the value to restore is
   8px / 10px. */
.creator-stats {
  display: flex;
  align-items: center;
  /* 8px, not 5. Luke, 3 Aug: "needs to be a bigger space between live and
     the number of likes" — at 10px the words had grown but the gap had not,
     so LIVE and the count read as one run of text. */
  gap: 8px;
  margin-top: 2px;
  font-size: 10px;
  line-height: 12.5px;
  font-weight: 900;
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}
/* The count and the word "likes" are ONE phrase and have to read as one.
   They used to be two flex children of .creator-stats, so the 8px gap that
   separates LIVE from the count also separated the count from its own noun —
   the number sat exactly midway between "LIVE" and "LIKES" and attached to
   neither. Luke, 3 Aug: "The number of likes sits in the middle of the two
   words, and it shouldn't." Grouping them makes the 8px a gap between the two
   pieces of information, and the space inside the phrase just a space. */
.creator-stats .likes-group {
  display: inline-flex;
  align-items: baseline;
  gap: 3px;
}
.creator-stats .likes { color: #fff; }
.creator-stats .dim { color: rgba(255, 255, 255, 0.6); font-weight: 900; }

/* A view count is wrong on a live room — she's streaming, not posting — so
   this replaced it. It USED to shimmer; Luke, 2 Aug: "there's a shimmer over
   it, we need to get rid of that". Theirs is flat #FF5380 at 8px/900
   uppercase and nothing else, so that is what this is now. The keyframes and
   the reduced-motion override went with it. */
.live-tag {
  font-weight: 900;
  color: var(--sl-pink);
}
/* A <button> centres its text by default ONLY horizontally; vertically it
   relies on the line box, so a 13.5px label in a 32px pill sits wherever the
   font's baseline puts it — which is high, because the em box reserves
   descender space uppercase and lowercase-without-descenders never use. Luke
   spotted it: "the word Follow, I don't even think that's central inside the
   pill." inline-flex + centre both axes fixes it by construction, and
   line-height:1 stops the label setting a taller box than the pill.
   13.5px was also off their scale — 12px is the step they use here. */
.follow {
  height: 32px;
  padding: 0 16px;
  border-radius: 999px;
  border: var(--chip-ring);
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  color: #fff;
  font-family: var(--font);
  font-weight: 900;
  font-size: 12px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: all 200ms ease;
}
.follow.on { background: var(--sl-pink); border-color: var(--sl-pink); }

/* The side rail, caption/hashtags, "I'm online" line and the heart
   burst were all removed on 2 Aug (Luke): the caption rendered behind the
   room goal line, and the room goal matters more. Only the creator header
   remains. */

@keyframes heartFloat {
  0% { transform: translate(0, 0) scale(0.4); opacity: 0; }
  12% { opacity: 1; transform: translate(var(--hx1, -8px), -40px) scale(1); }
  100% { transform: translate(var(--hx2, 10px), -300px) scale(0.9); opacity: 0; }
}

/* ============================================================
   BOTTOM NAV — Slushy black bar
   ============================================================ */
.bottombar {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: calc(var(--nav-h) + env(safe-area-inset-bottom, 0px));
  padding-bottom: env(safe-area-inset-bottom, 0px);
  z-index: 50;
  display: flex;
  align-items: center;
  background: #000;
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.nav-btn {
  position: relative;
  flex: 1;
  height: 100%;
  display: grid;
  place-items: center;
  border: none;
  background: none;
  color: rgba(255, 255, 255, 0.85);
  cursor: pointer;
}
.nav-btn svg { width: 26px; height: 26px; }
.nav-btn.active { color: var(--sl-pink); }
.nav-btn.haspip::after {
  content: '';
  position: absolute;
  top: 12px;
  right: 26%;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: #ff3b30;
}
/* casino-on shows as a small green pip (their pip language), icon stays white */
.nav-btn.casino-on::after {
  content: '';
  position: absolute;
  top: 12px;
  right: 26%;
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--sl-green);
  box-shadow: 0 0 6px rgba(140, 227, 46, 0.8);
}
.apptile .tile {
  width: 32px; height: 32px;
  border-radius: 9px;
  display: grid;
  place-items: center;
  background: linear-gradient(135deg, #b9ecc9 0%, #d8c9f4 48%, #f2c4e0 100%);
}
.apptile .tile svg { width: 20px; height: 20px; }

/* ============================================================
   CASINO OVERLAY
   ============================================================ */
.game {
  position: absolute;
  inset: 0 0 var(--nav-h) 0;
  z-index: 30;
  pointer-events: none;
  transition: opacity 250ms ease, transform 250ms ease;
}
.game.hidden { opacity: 0; transform: translateY(14px); }
.game.hidden .betbar { pointer-events: none !important; }

.wallets {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 118px);
  right: 12px;
  z-index: 5;           /* above the game panels (chicken road etc.) */
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: flex-end;
  pointer-events: auto;
}
.wchip {
  display: flex;
  align-items: center;
  gap: 5px;
  height: 30px;
  padding: 0 11px;
  border-radius: 999px;
  border: 1px solid rgba(232, 121, 249, 0.35);
  background: rgba(20, 8, 24, 0.6);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
  color: #fbd3ff;
  font-family: var(--font);
  font-weight: 800;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  /* `line-height: 1` so the pill centres the INK, not the em box.
     Luke, 3 Aug: "the usd amount you have in your balance does not look
     central inside the pill." `align-items: center` centres the label's line
     box, and a 12px em box reserves descender space that "$2,500" — digits and
     a dollar sign, all cap-height — never uses, so the glyphs sat high by
     about a pixel. Same bug as the header chip's `+` glyph. */
  line-height: 1;
}
/* The last of it: even at line-height 1 the em box is taller than the cap
   height, so the ink still sits fractionally high. Nudge it onto the optical
   centre. */
.wchip > span {
  display: block;
  transform: translateY(1px);
}
/* The cash chip is back, above the gem chip. It carries the brand green
   (--sl-green) rather than the gem magenta so the two balances are told apart
   at a glance without a second glyph: money is green, gems are magenta. */
.wchip.cash {
  border-color: rgba(115, 255, 0, 0.35);
  background: rgba(8, 18, 2, 0.6);
  color: var(--sl-green);
}
.gem { width: 14px; height: 14px; filter: drop-shadow(0 0 6px rgba(232, 121, 249, 0.7)); }
.wchip.bump, .wallet.bump { animation: bump 300ms ease; }
/* Same motion, green glow — a magenta flash on the cash chip read as a gem
   event, which is exactly the confusion the two chips exist to remove. */
.wchip.cash.bump { animation: bumpCash 300ms ease; }
@keyframes bump {
  0% { transform: scale(1); }
  40% { transform: scale(1.12); box-shadow: 0 0 20px rgba(232, 121, 249, 0.6); }
  100% { transform: scale(1); }
}
@keyframes bumpCash {
  0% { transform: scale(1); }
  40% { transform: scale(1.12); box-shadow: 0 0 20px rgba(115, 255, 0, 0.5); }
  100% { transform: scale(1); }
}

/* sliding game panels */
.panel {
  position: absolute;
  inset: 0;
  pointer-events: none;
  transition: transform 320ms cubic-bezier(0.22, 1, 0.36, 1), opacity 320ms ease;
  opacity: 0;
  transform: translateX(100%);
}
.panel.active { opacity: 1; transform: translateX(0); }
.panel.left { transform: translateX(-100%); }
.panel.noanim { transition: none; }

.game-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

/* ---------- crash hud ---------- */
.game-hud {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 122px);
  left: 0; right: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
}
/* last 5 chips, sized to fit from the far left with a gap before the $ pill */
.history {
  display: flex;
  gap: 3px;
  justify-content: flex-start;
  align-self: flex-start;
  margin-left: 14px;
  width: calc(100% - 148px);
  overflow: hidden;
  flex-wrap: nowrap;
}
.history .chip { flex-shrink: 0; }
.chip {
  font-size: 10px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  padding: 3px 6px;
  border-radius: 999px;
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  animation: riseIn 300ms ease-out;
}
@keyframes riseIn {
  from { transform: translateY(10px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
.chip.lo { color: #ff8fbf; }
.chip.hi { color: var(--green); }
.chip.epic { color: var(--gold); }

.mult-wrap { display: flex; flex-direction: column; align-items: center; gap: 0; opacity: 0.72; }
.mult {
  /* Their face for this one readout: monotalic-wide 700. It is monospaced, so
     the digits hold their columns as the number climbs — the -1px tracking
     below was Acumin compensation and is gone with it. */
  font-family: var(--numeral);
  font-size: 58px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--green);
  text-shadow:
    0 0 18px rgba(140, 227, 46, 0.6),
    0 0 60px rgba(140, 227, 46, 0.3),
    0 2px 4px rgba(0, 0, 0, 0.6);
  transition: color 200ms ease;
}
.mult.waiting { color: rgba(255, 255, 255, 0.92); text-shadow: 0 2px 12px rgba(0,0,0,0.7); }
.mult.crashed {
  color: var(--red);
  text-shadow:
    0 0 18px rgba(255, 59, 87, 0.75),
    0 0 60px rgba(255, 59, 87, 0.4),
    0 2px 4px rgba(0, 0, 0, 0.6);
}
.mult.pop { animation: multPop 350ms ease; }
@keyframes multPop {
  0% { transform: scale(1); }
  40% { transform: scale(1.18); }
  100% { transform: scale(1); }
}
.mult-sub {
  font-size: 12px;
  font-weight: 900;
  letter-spacing: 3px;
  color: var(--text-dim);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.8);
}
.mult-sub.cashed { color: var(--gold); }

.milestone {
  position: absolute;
  top: 36%;
  left: 50%;
  transform: translateX(-50%);
  font-size: 30px;
  font-weight: 900;
  color: var(--gold);
  text-shadow: 0 0 24px rgba(255, 211, 77, 0.8);
  opacity: 0;
  pointer-events: none;
}
.milestone.pop { animation: milestonePop 900ms ease-out; }
@keyframes milestonePop {
  0% { opacity: 0; transform: translateX(-50%) translateY(10px) scale(0.6); }
  25% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1.15); }
  60% { opacity: 1; transform: translateX(-50%) translateY(-14px) scale(1); }
  100% { opacity: 0; transform: translateX(-50%) translateY(-30px) scale(0.95); }
}

/* round result popup — shows even when you get wiped out */
.crash-result {
  position: absolute;
  top: 40%;
  left: 50%;
  transform: translate(-50%, 0) scale(0.8);
  padding: 14px 26px;
  border-radius: 20px;
  text-align: center;
  font-weight: 900;
  opacity: 0;
  pointer-events: none;
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
}
.crash-result .big { display: block; font-size: 26px; letter-spacing: -0.5px; }
.crash-result .small { display: block; font-size: 13px; letter-spacing: 1.5px; opacity: 0.85; margin-top: 2px; white-space: nowrap; }
.crash-result .small.profit { opacity: 1; margin-top: 1px; }
.crash-result.win {
  background: rgba(30, 24, 2, 0.72);
  border: 1.5px solid rgba(255, 211, 77, 0.6);
  color: var(--gold);
  box-shadow: 0 0 44px rgba(255, 211, 77, 0.35);
}
.crash-result.lose {
  background: rgba(30, 4, 8, 0.72);
  border: 1.5px solid rgba(255, 59, 87, 0.55);
  color: #ff8fa3;
  box-shadow: 0 0 44px rgba(255, 59, 87, 0.25);
}
.crash-result.show { animation: resultPop 2.4s ease forwards; }
@keyframes resultPop {
  0% { opacity: 0; transform: translate(-50%, 12px) scale(0.8); }
  10% { opacity: 1; transform: translate(-50%, 0) scale(1.06); }
  16% { transform: translate(-50%, 0) scale(1); }
  82% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -14px) scale(0.96); }
}

.crash-flash {
  position: absolute;
  inset: 0;
  background: radial-gradient(circle at 50% 45%, rgba(255, 59, 87, 0.35), transparent 70%);
  opacity: 0;
  pointer-events: none;
}
.crash-flash.on { animation: flashOut 600ms ease-out; }
@keyframes flashOut { 0% { opacity: 1; } 100% { opacity: 0; } }
.phone.shake { animation: shake 400ms ease-in-out; }
@keyframes shake {
  0%, 100% { transform: translate(0, 0); }
  20% { transform: translate(-5px, 2px); }
  40% { transform: translate(5px, -2px); }
  60% { transform: translate(-4px, -1px); }
  80% { transform: translate(3px, 1px); }
}

/* ---------- blackjack ---------- */
/* Luke, 2 Aug: on a notched phone with Safari's chrome, the YOU total was
   sitting behind the ROOM GOAL readout. The old `bottom: 252px` was clearance
   for the caption block that has since been deleted, and the hand had a fixed
   106px card, so on a short viewport the box became shorter than its own
   content and the bottom row spilled over the goal rows.

   Everything here now scales with the space available (`vh` inside `clamp`),
   so it compresses instead of overflowing. Values hold their old size on a
   tall screen and shrink on a short one. */
.bj {
  position: absolute;
  /* The creator header ends at safe + 110px (top 64 + 46 tall), so the dealer
     row has to start below that — at 108 it was sitting on her likes. */
  top: calc(env(safe-area-inset-top, 0px) + 124px);
  left: 0; right: 0;
  /* The presence row's top edge sits 229px above the game overlay's bottom, so
     this must stay ABOVE that. A SMALLER value extends the box DOWNWARD and
     puts the YOU total straight through the room-goal readout — 214px did
     exactly that. Short screens are handled by the clamps below shrinking the
     cards, never by changing this number.

     It reads --hud-clear now rather than carrying its own 244: the tab strip
     moved the whole stack and hand-editing five of these is how the last two
     overlap bugs happened. */
  bottom: var(--hud-clear);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: center;
  padding: 4px 16px;
}
.bj-row { display: flex; flex-direction: column; align-items: center; gap: clamp(3px, 0.9vh, 8px); }
.bj-label {
  font-size: 11px;
  font-weight: 900;
  letter-spacing: 2.5px;
  color: var(--text-dim);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.9);
  display: flex;
  align-items: center;
  gap: 8px;
}
.bj-label:empty { display: none; }
.bj-total {
  font-size: 12px;
  font-weight: 900;
  padding: 2px 8px;
  border-radius: 999px;
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  font-variant-numeric: tabular-nums;
  color: #fff;
}
.bj-total:empty { display: none; }

.bj-hand {
  display: flex;
  min-height: clamp(66px, 12vh, 108px);
  align-items: center;
}

/* transparent cards — stream fully visible through them */
.card {
  width: clamp(52px, 8.6vh, 74px);
  height: clamp(74px, 12.3vh, 106px);
  margin-left: clamp(-22px, -2.6vh, -15px);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.05);
  border: 1.5px solid rgba(255, 255, 255, 0.55);
  box-shadow:
    0 8px 24px rgba(0, 0, 0, 0.35),
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    0 0 18px rgba(255, 255, 255, 0.12);
  position: relative;
  display: grid;
  place-items: center;
}
.card:first-child { margin-left: 0; }
/* deal animation ONLY on newly-added cards (no reflash of the hand) */
.card.enter { animation: dealIn 380ms cubic-bezier(0.22, 1, 0.36, 1); }
@keyframes dealIn {
  from { transform: translate(60px, -70px) rotate(10deg); opacity: 0; }
  to { transform: translate(0, 0) rotate(0); opacity: 1; }
}
.card .pip {
  font-size: 30px;
  font-weight: 800;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85), 0 0 14px rgba(0, 0, 0, 0.5);
}
.card .corner {
  position: absolute;
  top: 7px; left: 8px;
  font-size: 13px;
  font-weight: 900;
  line-height: 1.05;
  text-align: center;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
}
.card .corner.br { top: auto; left: auto; bottom: 7px; right: 8px; transform: rotate(180deg); }
.card.red-suit { color: #ff8aa8; }
.card.black-suit { color: #fff; }

.card.facedown {
  background: rgba(255, 46, 147, 0.10);
  border-color: rgba(255, 122, 181, 0.5);
}
.card.facedown .back-star { width: 30px; height: 30px; fill: rgba(255, 255, 255, 0.55); }
.card.flip { animation: flipIn 420ms ease; }
@keyframes flipIn {
  0% { transform: rotateY(90deg); }
  100% { transform: rotateY(0); }
}

/* transient message pill — hidden entirely when empty (no idle text) */
.bj-msg {
  font-size: 15px;
  font-weight: 900;
  letter-spacing: 2.5px;
  color: var(--text-dim);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.9);
  padding: 8px 18px;
  border-radius: 999px;
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  transition: all 200ms ease;
}
.bj-msg:empty { display: none; }
.bj-msg.win { color: var(--green); text-shadow: 0 0 18px rgba(140, 227, 46, 0.5); }
.bj-msg.lose { color: var(--red); }
.bj-msg.push { color: var(--gold); }

/* ---------- plinko ---------- */
.plinko-msg {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 120px);
  left: 0; right: 0;
  text-align: center;
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 2px;
  color: var(--text-dim);
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.9);
  pointer-events: none;
}
.plinko-msg .big {
  display: block;
  font-size: 30px;
  font-weight: 900;
  letter-spacing: -0.5px;
  color: var(--gold);
  text-shadow: 0 0 22px rgba(255, 211, 77, 0.7);
}

/* ---------- dice — one slim translucent band, girl stays visible ---------- */
.dice {
  position: absolute;
  /* Symmetric. Luke, 2 Aug: dice sat left of centre — the 62px on the right
     was clearance for the side-rail buttons, deleted earlier. */
  left: 16px;
  right: 16px;
  top: auto;
  /* Level with the bottom of the Plinko buckets — Luke, 3 Aug. The variable is
     republished by plinko's layout(), so when the presence row appears and
     pushes the board up, dice follows it rather than sitting under it. */
  bottom: var(--hud-boardfoot);
  display: flex;
  flex-direction: column;
  gap: 14px;
  pointer-events: none;
}

/* recent rolls — same spot as the crash history chips */
.dice-results {
  position: absolute;
  top: calc(env(safe-area-inset-top, 0px) + 122px);
  left: 14px;
  right: 148px;
  display: flex;
  gap: 3px;
  justify-content: flex-start;
  overflow: hidden;
  flex-wrap: nowrap;
}
.droll {
  font-size: 10px;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
  padding: 3px 6px;
  border-radius: 999px;
  background: var(--chip-bg);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  animation: riseIn 300ms ease-out;
  flex-shrink: 0;
}
.droll.win { color: #00e701; }
.droll.lose { color: #ff4d6a; }

/* slider */
.dice-slider {
  position: relative;
  pointer-events: auto;
  /* 14px of horizontal padding keeps the handle's travel clear of the phone's
     left/right screen edges, where a drag is the browser's own back/forward
     gesture rather than ours — that is what made the slider impossible to drag
     back once it hit the far left. The handle and badge are positioned in px
     off `.dice-track` because of this padding; see app.js. */
  padding: 26px 14px 16px;   /* room for the result badge above */
  touch-action: none;
}
.dice-track {
  position: relative;
  height: 9px;
  border-radius: 999px;
  overflow: hidden;
  background: rgba(15, 15, 18, 0.28);
  box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.25);
}
.dice-seg {
  position: absolute;
  top: 0; bottom: 0;
}
.dice-seg.red { background: rgba(233, 17, 60, 0.5); box-shadow: 0 0 12px rgba(233, 17, 60, 0.25); }
.dice-seg.green { background: rgba(0, 231, 1, 0.45); box-shadow: 0 0 12px rgba(0, 231, 1, 0.25); }

.dice-scale {
  position: absolute;
  left: 0; right: 0;
  bottom: -6px;
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  font-weight: 800;
  color: rgba(255, 255, 255, 0.75);
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.8);
  pointer-events: none;
}

.dice-handle {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 26px;
  height: 30px;
  margin-top: 5px;
  transform: translate(-50%, -50%);
  border-radius: 7px;
  background: rgba(90, 168, 255, 0.55);
  border: 1.5px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 3px 10px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.3);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  cursor: grab;
}
.dice-handle:active { cursor: grabbing; }
.dice-handle span {
  width: 2px;
  height: 12px;
  border-radius: 1px;
  background: rgba(255, 255, 255, 0.75);
}

/* rolled-number badge — translucent glass, pops at the result position */
.dice-badge {
  position: absolute;
  top: -8px;
  left: 50%;
  transform: translateX(-50%);
  padding: 5px 10px;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.12);
  border: 1.5px solid rgba(255, 255, 255, 0.6);
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
  font-size: 15px;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
  opacity: 0;
  pointer-events: none;
}
.dice-badge::after {
  content: '';
  position: absolute;
  left: 50%;
  bottom: -5px;
  transform: translateX(-50%);
  border: 5px solid transparent;
  border-top-color: rgba(255, 255, 255, 0.6);
  border-bottom: none;
}
.dice-badge.win { color: #00e701; text-shadow: 0 0 12px rgba(0, 231, 1, 0.6), 0 1px 4px rgba(0,0,0,0.7); }
.dice-badge.lose { color: #ff4d6a; text-shadow: 0 0 12px rgba(255, 77, 106, 0.6), 0 1px 4px rgba(0,0,0,0.7); }
.dice-badge.show { animation: badgePop 350ms cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
@keyframes badgePop {
  0% { opacity: 0; transform: translateX(-50%) translateY(8px) scale(0.7); }
  100% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
}

/* stat chips */
.dice-stats {
  display: flex;
  gap: 8px;
  pointer-events: auto;
}
/* see-through like the blackjack cards */
.dstat {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 1px;
  padding: 7px 4px;
  border-radius: 14px;
  border: 1.5px solid rgba(255, 255, 255, 0.55);
  background: rgba(255, 255, 255, 0.05);
  box-shadow:
    0 8px 24px rgba(0, 0, 0, 0.25),
    inset 0 1px 0 rgba(255, 255, 255, 0.25),
    0 0 18px rgba(255, 255, 255, 0.10);
  font-family: var(--font);
  color: #fff;
}
.dstat b, .dstat .dlabel { text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85); }
.dstat .dlabel {
  font-size: 10px;
  font-weight: 900;
  color: rgba(255, 255, 255, 0.95);
  display: flex;
  align-items: center;
  gap: 4px;
}
.dstat .dlabel svg { width: 11px; height: 11px; color: var(--sl-green); }
.dstat b {
  font-size: 14px;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
}
.dstat.toggle { cursor: pointer; }
.dstat.toggle:active { transform: scale(0.97); }

/* ---------- swipe dots + persistent game name ---------- */
.dots-row {
  position: absolute;
  left: 0; right: 0;
  bottom: var(--hud-dots);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  pointer-events: none;
  z-index: 6;
}
.dots {
  display: flex;
  gap: 7px;
}
.dot {
  width: 6px; height: 6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.3);
  transition: all 250ms ease;
}
.dot.active {
  background: var(--sl-pink);
  box-shadow: 0 0 10px rgba(255, 83, 128, 0.8);
  transform: scale(1.25);
}

/* The name stays put after the big title has faded, so "which game am I on"
   is answerable at a glance without waiting for another transition. */

/* ---------- landing game title ---------- */
.game-title {
  position: absolute;
  left: 0; right: 0;
  top: 34%;
  z-index: 7;
  display: grid;
  justify-items: center;
  gap: 6px;
  pointer-events: none;      /* never blocks an immediate bet */
  opacity: 0;
  transition: opacity 260ms ease, transform 260ms ease;
  transform: translateY(6px) scale(0.98);
}
.game-title.show {
  opacity: 1;
  transform: none;
}
.game-title-name {
  font-size: 46px;
  font-weight: 900;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  line-height: 1;
  text-shadow: 0 4px 22px rgba(0, 0, 0, 0.75);
}
.game-title-sub {
  font-size: 13px;
  font-weight: 800;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 2px 10px rgba(0, 0, 0, 0.8);
}
@media (prefers-reduced-motion: reduce) {
  .game-title { transition: opacity 120ms linear; transform: none; }
}

/* ---------- goal rows ----------
   slushygames.net's shape, measured off their live page:

     row      bg-white/10, backdrop-blur-md, 44.5px tall, px-20 py-10
     fill     absolute inset-y-0 left-0, gradient lime/10 → lime/75,
              opacity .4, 2px lime right border, width transitions 500ms
     name     10px / 900
     sub      8px  / 700 / white 60%
     count    10px, number in 900

   We shipped a thin segmented rail instead, on the argument that a fill
   behind text is less readable. Ryan, 2 Aug, looking at both: "You can read
   their progress bar. You can't read ours." The lesson isn't fill-vs-rail —
   it's the SURFACE. Their row is its own translucent card, so the type has
   something to sit on; ours was hairline text on raw video with only a
   text-shadow between it and a bright frame. Copied properly now.

   One row type, three places: over the video (room goal), and in the Goals
   tab (room goal + the four personal tiers). */
.goal-kicker {
  font-family: var(--display);
  font-size: 10px;
  line-height: 12.5px;
  font-weight: 900;
  /* No tracking. The 0.05em that used to be here was faking the width of
     normalidad-ultraextended with Acumin; the real face is now loaded, and
     their measured spec carries no letter-spacing at this size. */
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
}
/* Over the video it takes their FEED CARD treatment instead, which is a
   different spec from the one inside the Goals tab: 8px/900, 0.2px tracking,
   and brand green rather than white. Both are measured; they just use each in
   a different place, and so do we. */
.roomgoal .goal-kicker {
  font-size: 8px;
  line-height: 10px;
  letter-spacing: 0.2px;
  color: var(--sl-green);
}

.goalrow {
  position: relative;
  width: 100%;
  overflow: hidden;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.10);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
}
/* The fill sits UNDER the text at 40% opacity with a bright leading edge —
   that edge is what reads as "how far along", not the wash behind it. */
.goalrow-fill {
  position: absolute;
  inset: 0 auto 0 0;
  width: 0;
  opacity: 0.4;
  border-right: 2px solid var(--sl-green);
  background: linear-gradient(to right, rgba(115, 255, 0, 0.10), rgba(115, 255, 0, 0.75));
  transition: width 500ms ease;
}
/* A zero-width fill still paints its 2px leading edge, which put a coloured
   bar down the left of every locked tier and read as "started". No progress,
   no edge. */
.goalrow-fill.empty { border-right-width: 0; }
.goalrow-body {
  position: relative;   /* over the fill */
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 44px;
  padding: 8px 14px;
}
.goalrow-id {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.goalrow-name {
  /* 12px/900, theirs exactly (feed card). Was 11. */
  font-size: 12px;
  line-height: 15px;
  font-weight: 900;
  color: #fff;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.goalrow-sub {
  /* 8px/700 white-60, theirs exactly (live page). Was 9px. */
  font-size: 8px;
  line-height: 10px;
  font-weight: 700;
  color: rgba(255, 255, 255, 0.6);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.goalrow-count {
  /* 12px/900, the target half at white-60 and the current figure at full
     white — theirs exactly ("4.7K" white, "/ 7.5K" at .6). Was 11px/700. */
  flex: none;
  font-size: 12px;
  line-height: 15px;
  font-weight: 900;
  color: rgba(255, 255, 255, 0.6);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.goalrow-count b { font-weight: 900; color: #fff; }

/* Locked tiers — visible, so the ladder reads as a ladder, but clearly not
   live yet. Theirs does the same with a padlock on the right. */
.goalrow.locked { background: rgba(255, 255, 255, 0.05); }
.goalrow.locked .goalrow-name,
.goalrow.locked .goalrow-count b { color: rgba(255, 255, 255, 0.55); }
.goalrow.locked .goalrow-sub { color: rgba(255, 255, 255, 0.35); }
.goalrow.locked .goalrow-count { color: rgba(255, 255, 255, 0.4); }
.goalrow-lock { width: 11px; height: 11px; margin-left: 7px; vertical-align: -1px; fill: rgba(255,255,255,0.45); }

/* Your tiers are pink, the room's is green — the colour still says whose goal
   it is (Luke, 2 Aug: "it needs to stay pink"). */
.goalrow.mine .goalrow-fill {
  border-right-color: var(--sl-magenta);
  background: linear-gradient(to right, rgba(229, 82, 197, 0.10), rgba(229, 82, 197, 0.75));
}
.goalrow.done .goalrow-fill { border-right-width: 0; }

/* Over the video. `--hud-*` below owns the vertical stack. */
.roomgoal {
  position: absolute;
  left: 14px; right: 14px;
  bottom: var(--hud-goal);
  z-index: 6;
  pointer-events: none;
  display: grid;
  gap: 7px;
}

/* Checkpoint hit: the leading edge flares once. */
@keyframes rgPop {
  0%   { box-shadow: 0 0 0 rgba(115, 255, 0, 0); }
  40%  { box-shadow: -8px 0 22px rgba(115, 255, 0, 0.9); }
  100% { box-shadow: 0 0 0 rgba(115, 255, 0, 0); }
}
.goalrow.pop .goalrow-fill { animation: rgPop 420ms ease; }
@media (prefers-reduced-motion: reduce) {
  .goalrow.pop .goalrow-fill { animation: none; }
}

/* ---------- checkpoint gem burst ----------
   Replaces the deleted social toasts as the "other people are here" signal:
   when the room's bets push the fill past a tick, THAT tick throws a handful
   of small green gems and then sits solid green (.rg-tick.hit above).

   Deliberately tiny — 7px gems travelling ~25px off a 4px hairline. It must
   read as the bar reacting, not as a slot machine, and it must never cover
   the video or reach the bet bar 100px below.

   pointer-events:none on the particle as well as on .roomgoal, because this
   sits directly over the play area and may not swallow a tap under any
   circumstances. */
.rg-gem {
  position: absolute;
  top: 0;
  margin-left: -6px;
  width: var(--sz, 11px);
  height: var(--sz, 11px);
  pointer-events: none;
  opacity: 0;
  animation: rgGem 1150ms cubic-bezier(0.2, 0.7, 0.3, 1) forwards;
  filter: drop-shadow(0 0 5px currentColor);
  z-index: 2;
}
.rg-gem svg {
  display: block;
  width: 100%;
  height: 100%;
  filter: drop-shadow(0 0 3px currentColor);
}
@keyframes rgGem {
  0%   { opacity: 0; transform: translate(0, 0) scale(0.4) rotate(0deg); }
  14%  { opacity: 1; transform: translate(calc(var(--dx) * 0.55), calc(var(--dy) * 0.65)) scale(1.15) rotate(calc(var(--rot) * 0.3)); }
  40%  { opacity: 1; transform: translate(var(--dx), var(--dy)) scale(1) rotate(calc(var(--rot) * 0.7)); }
  100% { opacity: 0; transform: translate(calc(var(--dx) * 1.12), calc(var(--dy) + var(--fall))) scale(0.55) rotate(var(--rot)); }
}

@media (prefers-reduced-motion: reduce) {
  .roomgoal-rail.pop .rg-fill { animation: none; }
  /* No particles at all — the tick simply goes solid green. JS also skips
     spawning them, so this is only a belt-and-braces net. */
  .rg-gem { display: none; }
}

/* ---------- presence row ----------
   Two layouts in one block, switched by .round / .ambient — see
   GAME_PRESENCE in app.js. Crash gets the round layout (betting countdown,
   head-count, player stack); the four private games get the ambient one. Two
   rows at most: the bottom of the panel is already carrying the room goal,
   the personal goal and the bet bar, and a third row pushed straight into
   Plinko's buckets.

   NEITHER shows until JS has put a treatment class on. This is deliberate and
   it is the fix for a real bug: the markup used to default to the round
   layout, so before the first animation frame — which never arrives at all in
   a backgrounded tab, since Chrome suspends rAF there — the feed opened on
   Blackjack showing "0 PLAYING · STARTING IN 4.2". The row must not be able
   to claim a shared round before GAME_PRESENCE has been consulted, so the
   unreconciled state shows nothing rather than something false. */
.roomlive {
  position: absolute;
  left: 14px; right: 14px;
  bottom: var(--hud-live);
  z-index: 6;
  pointer-events: none;
  display: grid;
  gap: 5px;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}
.rl-row, .rl-ambient { display: none; }
.roomlive.round .rl-row { display: flex; }
/* Co-participation is only claimed once you're actually a participant: the
   avatar stack and the head-count are hidden until you've taken the round
   (`.mine`, set in app.js). The STARTING IN countdown is not gated — it's the
   betting window, not a presence claim, and it's how you know you can bet. */
.roomlive.round:not(.mine) .rl-stack,
.roomlive.round:not(.mine) .rl-counts { display: none; }
/* AMBIENT PRESENCE IS HIDDEN, NOT DELETED.
   Luke, 2 Aug: "I don't know where it says 'in this room, 46 players' at the
   top of Room Goals — we can get rid of that, because we've already got it at
   the bottom." He's right: the tab strip's players pill now carries the same
   number, and the avatar stack plus the caption was a second copy of it
   sitting across the girl.

   It stays in the DOM and keeps ticking on purpose — `tabs.roomCount()` reads
   #rlHere to label that pill, so removing the node would empty the very thing
   that replaced it. This is a display rule, nothing more.

   THE ROUND ROW IS UNAFFECTED, and must stay that way: Crash's "STARTING IN"
   countdown is not duplicated anywhere, and it is the only signal that the
   betting window is open. See handoff §3. */
.roomlive.ambient { display: none; }
.rl-row {
  align-items: center;
  gap: 8px;
  min-width: 0;
}
/* stack of the other players in the room */
.rl-stack { display: inline-flex; align-items: center; flex: none; }
.rl-av {
  width: 17px; height: 17px;
  border-radius: 50%;
  margin-right: -6px;
  border: 1.5px solid rgba(12, 12, 14, 0.9);
  display: inline-grid;
  place-items: center;
  font-size: 8px;
  font-weight: 900;
  color: rgba(255, 255, 255, 0.95);
  text-shadow: none;
}
.rl-av.me {
  border-color: var(--sl-green);
  box-shadow: 0 0 7px rgba(115, 255, 0, 0.7);
}
/* the -6px overlap is what makes it a stack; without this the last one eats
   into the head-count next to it */
.rl-av:last-child { margin-right: 0; }
.rl-more {
  margin-left: 10px;
  font-size: 10px;
  font-weight: 900;
  color: rgba(255, 255, 255, 0.72);
}
.rl-counts {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  overflow: hidden;
  white-space: nowrap;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.06em;
}
.rl-counts b { font-size: 11px; font-variant-numeric: tabular-nums; }
.rl-playing { color: var(--sl-green); }
.rl-cap { color: rgba(255, 255, 255, 0.6); }
.rl-state {
  flex: none;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.62);
}
.rl-state b {
  color: #fff;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  margin-left: 3px;
}
.roomlive.live .rl-state b { color: var(--sl-green); }
/* ---------- ambient presence (Blackjack, Chicken, Plinko, Dice) ----------
   Co-presence, not co-participation. These four games are self-paced and
   private — there is no shared round to report, so the countdown, the round
   bar and the PLAYING / CASHED OUT split are all gone and the row says only
   what is actually true: who else is in the room. Quieter than the round row
   on purpose; it is context, not a call to act. (Visibility is handled by the
   .round / .ambient rules at the top of this section.) */
.rl-ambient {
  align-items: center;
  gap: 8px;
  min-width: 0;
}
/* The round layout is row + 5px gap + 2px bar; without the bar the ambient
   row would land ~7px lower and the whole block would jump as you swipe
   between a round game and a solo one. This holds it still. */
.roomlive.ambient { padding-bottom: 7px; }
.rl-here {
  display: inline-flex;
  align-items: baseline;
  gap: 4px;
  font-size: 10px;
  font-weight: 900;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}
.rl-here b {
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: rgba(255, 255, 255, 0.92);
}

/* ============================================================
   ROOM TABS — slushygames.net's bottom strip
   ============================================================
   Their pills, measured: h-36 px-15 rounded-20, 12px font-black, active
   bg-white/10 + white text, inactive transparent + white/50, gap-10, the
   strip itself px-10 py-10 and horizontally scrollable.

   Ours are 32px rather than 36 for the same reason the bet-bar gap halved:
   the app's own 55px nav sits under this strip, where on their layout the
   nav is a left sidebar and the strip is the last thing on screen. */
.tabstrip {
  position: absolute;
  left: 0; right: 0;
  bottom: 0;
  z-index: 8;
  display: flex;
  align-items: center;
  gap: 6px;
  /* Tighter than the 10px theirs uses, and the pills are 30px not 36 — on
     their layout this strip is the last thing on screen, on ours the app's
     own 55px nav sits directly under it, so the same padding reads as twice
     the furniture. Luke: "they're smaller than ours, it doesn't cover the
     screen as much." The TYPE is untouched at their 12px/900. */
  padding: 5px 10px;
  overflow-x: auto;
  scrollbar-width: none;
  pointer-events: auto;
  /* the strip scrolls sideways on its own; the feed arbiter must not treat
     that as a creator swipe */
  touch-action: pan-x;
}
.tabstrip::-webkit-scrollbar { display: none; }
/* Same fix as .follow — a button's label is only auto-centred horizontally,
   so the text sat high in the pill. */
.tab {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  line-height: 1;
  height: 30px;
  padding: 0 12px;
  border: none;
  border-radius: 20px;
  background: transparent;
  /* white-40 inactive, theirs exactly (was .5) */
  color: rgba(255, 255, 255, 0.4);
  font-family: var(--font);
  font-size: 12px;
  font-weight: 900;
  white-space: nowrap;
  cursor: pointer;
  transition: background 160ms ease, color 160ms ease;
}
.tab.on {
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
}
.tab:active { transform: scale(0.97); }

/* ---------- tab panels ----------
   They replace the game view rather than layering over it, which is what
   makes the swap feel like a drawer instead of more clutter. The panel is
   anchored to the bet bar's old position and grows upward to a cap, so a long
   chat scrolls inside itself and never pushes into the creator's face. */
.tabpanels {
  position: absolute;
  left: 12px; right: 12px;
  bottom: var(--hud-betbar);
  max-height: 58%;
  z-index: 7;
  display: flex;
  flex-direction: column;
  pointer-events: auto;
}
.tabpanels[hidden] { display: none; }
.tabpanel {
  display: none;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
}
.tabpanel.on { display: flex; }

/* Opening a panel puts the game and its HUD away. Everything below still
   RUNS — Plinko balls keep falling and paying — it is only hidden. */
.game.tab-open .panel,
.game.tab-open .roomgoal,
.game.tab-open .roomlive,
.game.tab-open .dots-row,
.game.tab-open .betbar,
.game.tab-open .game-title { opacity: 0; pointer-events: none; }
.game.tab-open .panel { transition: opacity 200ms ease; }

/* MEASURED off their players panel, which is a different shape from what we
   had. Luke: "ours is much bigger than theirs and looks significantly
   different." It was: our rows carried their own 9px/14px padding and the
   list had none, so every row was ~40px against their 20px.

     list  rounded-20, p-20, gap-10, bg-white/10, backdrop-blur-md, max 33vh
     row   flex, gap-10, NO padding — the container's gap does the spacing
     av    20x20 circle, 8px/900, #1F2123 on the accent colour
     name  12px/900 white   ·  status 12px/900 white-40  ·  stake 12px/900 */
.tp-list {
  display: flex;
  flex-direction: column;
  gap: 10px;
  max-height: 33vh;
  padding: 20px;
  overflow-y: auto;
  overscroll-behavior: contain;
  touch-action: pan-y;
  scrollbar-width: none;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.10);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
.tp-list::-webkit-scrollbar { display: none; }

/* a player / big-win row */
.tp-row {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 20px;
  font-size: 12px;
  line-height: 15px;
  font-weight: 900;
}
.tp-av {
  flex: none;
  width: 20px; height: 20px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 8px;
  line-height: 1;
  font-weight: 900;
  color: var(--sl-base);
  font-style: normal;
}
.tp-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: #fff;
}
.tp-mult { flex: none; color: var(--sl-green); font-variant-numeric: tabular-nums; }
/* 900 and white-40, theirs. Ours was 700 at white-45, which read as a
   different weight beside the name it sits next to. */
.tp-mult.in { color: rgba(255, 255, 255, 0.4); font-weight: 900; }
.tp-stake {
  flex: none;
  color: #fff;
  font-variant-numeric: tabular-nums;
  min-width: 54px;
  text-align: right;
}
/* Their panels sit on a 33vh card, so the tab panel must not also cap them. */
#tp-players, #tp-wins { min-height: 0; }

/* ---------- goals tab ---------- */
#tp-goals { overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; scrollbar-width: none; }
#tp-goals::-webkit-scrollbar { display: none; }
.goal-ladder { display: grid; gap: 2px; }
/* The ladder is one card split into rows, like theirs — square the inner
   corners so the rows read as a stack rather than four floating pills. */
.goal-ladder .goalrow { border-radius: 0; }
.goal-ladder .goalrow:first-child { border-radius: 14px 14px 0 0; }
.goal-ladder .goalrow:last-child { border-radius: 0 0 14px 14px; }
.goal-ladder .goalrow:only-child { border-radius: 14px; }

/* ---------- stream chat ---------- */
/* Luke: "the stream chat just kinda cuts out — realistically it should fade
   out." A mask does it without a scrim element, so nothing new sits over the
   video and the fade travels with the scroll instead of being painted at a
   fixed height. 10px rhythm is theirs (`space-y-10`); ours was 7.

   The mask has to be full-bleed horizontally (`to bottom` only) or it would
   also feather the left edge — which is the other half of what Luke spotted
   below. */
.chat-log {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 10px;
  overflow-y: auto;
  overscroll-behavior: contain;
  touch-action: pan-y;
  scrollbar-width: none;
  padding-bottom: 2px;
  /* Room for the text shadow inside the clip box.

     `overflow-y: auto` forces overflow-x to compute to auto as well, so this
     box clips horizontally — and .chat-line carries a 6px text-shadow. Every
     line's shadow was being sliced off dead straight at the left edge, which
     drew a hard vertical band down the whole column. Luke reported it three
     times and it is not the video: it is this box clipping its own shadow.

     The padding gives the shadow somewhere to land and the matching negative
     margin puts the text back where it was, so nothing moves. Same trap as
     the overflow note in the handoff, from the other direction. */
  padding-left: 10px;
  margin-left: -10px;
  padding-right: 10px;
  margin-right: -10px;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 44px);
  mask-image: linear-gradient(to bottom, transparent 0, #000 44px);
}
.chat-log::-webkit-scrollbar { display: none; }
/* No card behind the log — theirs runs the messages straight over the video
   with a text shadow, and the bottom scrim already carries them. */
/* MEASURED off their stream chat, and this was the single biggest "that's a
   different font" tell in the whole build:

     message   14px / weight 400 / line-height 17.5px / white
     author    12px / weight 700 / rgba(255,255,255,0.8)

   Ours was 13px/700 and 12px/900. Both of those resolve to BLACK — our kit
   and theirs each ship exactly two faces and collapse every weight ≥600 onto
   the heavy one (verified: "Stream Chat" at 100px measures 632.70 at 400 and
   711.20 at 900 on BOTH sites, so the files are the same faces). So our chat
   was rendering entirely in Acumin Black while theirs runs the message body
   in Regular. Same typeface, completely different texture — which is exactly
   what Ryan and Luke kept seeing. */
.chat-line {
  font-size: 14px;
  font-weight: 400;
  line-height: 17.5px;
  color: #fff;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.9);
}
.chat-who {
  display: block;
  font-size: 12px;
  font-weight: 700;
  line-height: 15px;
  color: rgba(255, 255, 255, 0.8);
}
.chat-line.me .chat-who { color: var(--sl-green); }
/* tipped + system lines get a coloured edge and a tint, as theirs do */
/* Their tipped-line accent is #FB8C00, not our gold.

   The tint BLEEDS TO THE LEFT EDGE OF THE SCREEN. Luke: "there's some kind of
   gradient on there and it's not all the way to the left wall — it kind of
   starts at the first letters, and it shouldn't." Correct: the panel is inset
   12px from the wall, so a highlight drawn inside it began at the text and
   read as a floating box. The negative margin pulls it out to the wall and
   the matching padding puts the text back where it was, so only the tint
   moves. */
.chat-line.tip, .chat-line.sys {
  margin-left: -12px;
  border-left: 3px solid #FB8C00;
  background: rgba(251, 140, 0, 0.16);
  padding: 5px 9px 5px 12px;
  border-radius: 0 8px 8px 0;
}
.chat-line.tip .chat-who { color: #FB8C00; }
.chat-line.sys {
  border-left-color: var(--sl-green);
  background: rgba(115, 255, 0, 0.16);
}
.chat-line.sys .chat-who { color: var(--sl-green); }
.chat-tip {
  /* 8px/900 on a solid badge, dark #1F2123 text — theirs exactly. */
  display: inline-block;
  margin-left: 6px;
  padding: 2px 6px;
  border-radius: 999px;
  background: #FB8C00;
  color: var(--sl-base);
  font-size: 8px;
  line-height: 10px;
  font-weight: 900;
  vertical-align: 1px;
}
.chat-earn {
  flex: none;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border: none;
  border-radius: 14px;
  background: linear-gradient(135deg, var(--sl-pink), var(--sl-magenta));
  color: #fff;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 900;
  cursor: pointer;
}
.chat-earn:active { transform: scale(0.98); }
/* Their composer, measured: bg-white/10, rounded-20, px-12 py-8, and the
   field itself 12px/400 with a white-40 placeholder. */
.chat-form {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 8px 8px 12px;
  border-radius: 20px;
  background: rgba(255, 255, 255, 0.10);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
}
/* 12px REGULAR. Ours was 16px/700, which resolves to Black — Luke: "on
   theirs it isn't in bold, ours is in big bold letters". The 16px was an iOS
   guard (Safari zooms the page when a field under 16px takes focus); dropped
   because matching them was the explicit instruction and the demo's viewport
   meta already carries maximum-scale=1. If the zoom does bite on a real
   iPhone, that trade is the thing to revisit — not the weight. */
.chat-input {
  flex: 1;
  min-width: 0;
  border: none;
  background: none;
  color: #fff;
  font-family: var(--font);
  font-size: 12px;
  line-height: 15px;
  font-weight: 400;
  outline: none;
}
.chat-input::placeholder { color: rgba(255, 255, 255, 0.4); font-weight: 400; }
.chat-send {
  flex: none;
  width: 34px; height: 34px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.16);
  color: #fff;
  display: grid;
  place-items: center;
  cursor: pointer;
}
.chat-send svg { width: 16px; height: 16px; }

/* ---------- details tab ---------- */
#tp-about { overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; scrollbar-width: none; }
#tp-about::-webkit-scrollbar { display: none; }
.tp-copy {
  font-size: 12px;
  font-weight: 700;
  line-height: 1.45;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
}
.tp-steps {
  display: grid;
  gap: 6px;
  padding-left: 16px;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.4;
  color: rgba(255, 255, 255, 0.85);
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
}

/* Social toasts ("pixelpaula unlocked Shoutout!") used to sit here, pinned to
   the upper third of the video. Deleted 2 Aug — see app.js. The room's
   presence is now expressed through the room-goal bar it actually fills. */

/* ---------- bet bar — bottom of the video, just above the scrubber ---------- */
.betbar {
  position: absolute;
  left: 12px;
  right: 12px;
  /* 10px, was 20px. Luke, 2 Aug: "maybe 50% smaller gap between that and the
     navigation bar". `.game` is already inset by --nav-h (which carries the
     safe-area inset), so this gap is identical on notched and flat devices.
     The 10px is now the 4px inside --hud-betbar, which stacks on the tab
     strip below it. */
  bottom: var(--hud-betbar);
  display: flex;
  gap: 10px;
  pointer-events: auto;
  padding: 9px;
  border-radius: 22px;
  background: rgba(16, 16, 20, 0.68);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
  border: 1px solid rgba(255, 255, 255, 0.10);
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
}

/* Amount + − / + stepper, with Max underneath. The stepper is the primary
   control, and now the only one. */
.bet-amount {
  display: flex;
  flex-direction: column;
  justify-content: center;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.07);
  padding: 5px 8px;
  flex: 0 0 auto;
  min-width: 150px;
}
.bet-value {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  font-weight: 900;
  font-size: 19px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
}
/* the amount itself, held to a fixed width so the − and + never shuffle
   sideways under a thumb as the number gains or loses a digit */
.bet-num {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 4px;
  flex: 1 1 auto;
  min-width: 62px;
}
/* ---- − / + stepper ----
   The primary bet control. Deliberately the biggest touch targets in the
   bar after the CTA: this is the thing you reach for between every bet. */
/* Bigger now that Max no longer sits under them and the row has the height
   to spare (Luke, 2 Aug). */
.bet-step {
  flex: none;
  width: 34px;
  height: 34px;
  border: none;
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.12);
  color: rgba(255, 255, 255, 0.9);
  font-family: var(--font);
  font-size: 20px;
  font-weight: 900;
  line-height: 1;
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background 150ms ease, transform 120ms ease;
}
.bet-step:active { background: rgba(255, 255, 255, 0.26); transform: scale(0.9); }
.bet-step:disabled { opacity: 0.35; cursor: default; }

.actions { flex: 1; display: flex; gap: 8px; }

.act-btn {
  flex: 1;
  height: 46px;
  border: none;
  border-radius: 14px;
  font-family: var(--font);
  font-size: 13px;
  font-weight: 900;
  letter-spacing: 0.4px;
  cursor: pointer;
  color: #101800;
  background: linear-gradient(135deg, var(--sl-green), var(--green-deep));
  box-shadow: 0 0 24px rgba(140, 227, 46, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
  transition: transform 120ms ease, box-shadow 200ms ease, filter 200ms ease;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  /* 6px, not 10. Every button here is `nowrap` with `min-width: fit-content`
     (see below), so none of them can shrink below its own label and the row's
     minimum width is the SUM of the bet stepper and every button in it. That
     makes side padding the only give in the row.

     Blackjack is the tightest case — the only row with four controls, the
     stepper plus HIT / STAND / 2x. Measured in the feed iframe, the row
     overflowed its bar by 5px at 375 and 20px at 360; three buttons x 8px of
     reclaimed padding clears both. `flex: 1` still lets them spread out when
     there is room, so nothing changes on a roomy phone — this only bites in
     the squeezed case, which is exactly where it was needed. */
  padding: 0 6px;
}
/* Flex children default to min-content, so a nowrap label could push the row
   wider than the screen instead of shrinking.
   `min-width: 0` alone went too far the other way: combined with the `nowrap`
   above, a squeezed button kept its label at full width and let it paint
   outside its own box, so on Blackjack — the only row with four controls, the
   stepper plus HIT/STAND/2x — STAND's text ran under the 2x button.
   Ryan, 3 Aug: "Stand button looks a bit off too."
   `fit-content` lets them shrink to the label but never past it. */
.act-btn { min-width: fit-content; }

/* Two-line button: the words above, the money below. */
.act-btn.stack {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  line-height: 1;
}
.act-btn.stack i {
  font-style: normal;
  font-size: 9px;
  letter-spacing: 0.06em;
  opacity: 0.75;
}
/* 13px, not 14. The money line is the widest text in the Chicken row and that
   row is the tightest in the app — the bet stepper takes 158px of a 349px bar
   and never yields (`flex: 0 0 auto`), leaving ~163px for GO plus CASH OUT.
   One pixel of font size here is worth ~6px of row. */
.act-btn.stack b { font-size: 13px; font-weight: 900; }

.act-btn:active { transform: scale(0.97); }
.act-btn.gold {
  color: #1a1002;
  background: linear-gradient(135deg, #ffd34d, #ff9f2e);
  box-shadow: 0 0 28px rgba(255, 211, 77, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.4);
  animation: cashPulse 900ms ease-in-out infinite;
}
@keyframes cashPulse {
  0%, 100% { box-shadow: 0 0 18px rgba(255, 211, 77, 0.4), inset 0 1px 0 rgba(255,255,255,0.4); }
  50% { box-shadow: 0 0 34px rgba(255, 211, 77, 0.75), inset 0 1px 0 rgba(255,255,255,0.4); }
}
.act-btn.ghost { color: #fff; background: rgba(255, 255, 255, 0.12); box-shadow: none; }
/* "WAITING FOR NEXT ROUND" is 22 characters — at 13px it overflowed the
   button once the bet cluster took its width. */
.act-btn.wide { font-size: 10px; letter-spacing: 0.02em; }
.act-btn .bgem { width: 12px; height: 12px; vertical-align: -2px; margin: 0 1px; }
/* Sized for `GO →` alone. It used to read `GO → 1.20×`, which with the 60px
   floor and 13px of side padding made this the second-widest thing in the
   row; see the label comment in app.js for why the row overflows to the
   right rather than shrinking. */
.act-btn.go { flex: 0 0 auto; min-width: 52px; padding: 0 10px; }
/* The room's buy-in CTA. slushygames.net's "Play <game>" button is
   `bg-gradient-to-r from-red-700 to-pink-700` — i.e. exactly
   #FF5380 → #E552C5 — so that's what this is. Green is reserved for money and
   progress (multipliers, player counts, the room goal), which is how their
   palette uses it; a green buy-in button fights that. The old #c026d3 here was
   a pre-recon guess at their magenta. */
.act-btn.pinkish {
  color: #fff;
  background: linear-gradient(135deg, var(--sl-pink), var(--sl-magenta));
  box-shadow: 0 0 24px rgba(255, 83, 128, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.25);
}
.act-btn:disabled { filter: grayscale(0.6) brightness(0.7); cursor: default; }

/* ---------- win toast ---------- */
.win-toast {
  position: absolute;
  left: 50%;
  bottom: var(--hud-clear);   /* clears the presence row */
  transform: translate(-50%, 20px);
  padding: 10px 20px;
  border-radius: 999px;
  background: linear-gradient(135deg, rgba(255, 211, 77, 0.95), rgba(255, 159, 46, 0.95));
  color: #1a1002;
  font-weight: 900;
  font-size: 16px;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 0 40px rgba(255, 211, 77, 0.6);
  opacity: 0;
  pointer-events: none;
  display: flex;
  align-items: center;
  gap: 6px;
}
.win-toast .tpart {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;   /* never split a value across lines */
}
.win-toast.show { animation: toastUp 1.8s ease forwards; }

/* Goal unlocks: a small colour-coded pill, not the gold win slab. Luke,
   2 Aug: the old one was "way too big its annoying" — it spanned the panel
   at 16px in gold and sat over the video for nearly two seconds. */
.win-toast.goal {
  padding: 5px 12px;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  max-width: calc(100% - 32px);
  box-shadow: none;
}
.win-toast.goal .tpart { overflow: hidden; text-overflow: ellipsis; }
.win-toast.goal.room {
  background: rgba(115, 255, 0, 0.92);
  color: #0d1a00;
  box-shadow: 0 0 18px rgba(115, 255, 0, 0.45);
}
.win-toast.goal.you {
  background: rgba(229, 82, 197, 0.94);
  color: #fff;
  box-shadow: 0 0 18px rgba(229, 82, 197, 0.45);
}
@keyframes toastUp {
  0% { opacity: 0; transform: translate(-50%, 20px) scale(0.8); }
  12% { opacity: 1; transform: translate(-50%, 0) scale(1.05); }
  20% { transform: translate(-50%, 0) scale(1); }
  80% { opacity: 1; transform: translate(-50%, -6px); }
  100% { opacity: 0; transform: translate(-50%, -18px); }
}

/* when the casino overlay is open, lift the Slushy UI clear of the bet bar */
.phone.casino-open .caption { bottom: 110px; }
.phone.casino-open .rail { bottom: 110px; }
.phone.casino-open .hearts { bottom: 260px; }

@media (prefers-reduced-motion: reduce) {
  .scene, .scene .blob, .card.enter, .act-btn.gold { animation: none !important; }
  .phone.shake { animation: none; }
  .panel { transition: none; }
}

/* creator header is a link to her profile */
.creator-head { cursor: pointer; }
.creator-head .avatar,
.creator-head .creator-name { transition: opacity 0.15s; }
.creator-head:active .avatar,
.creator-head:active .creator-name { opacity: 0.65; }

/* ============================================================
   HOW TO PLAY — first-run coach card.
   Mirrors Slushy's own "HOW TO USE STORIES" sheet: blurred
   backdrop, caps display title, bordered split panel, green pill.
   ============================================================ */
/* ============================================================
   FEED PICKER — clone of Slushy's "What are we watching?" sheet
   (tap the For You chip). Sheet covers all but a strip of the feed,
   header pinned, list scrolls beneath it.
   ============================================================ */
.picker {
  position: absolute;
  inset: 0;
  z-index: 60;
  display: flex;
  flex-direction: column;
  background: rgba(24, 25, 27, 0.92);
  -webkit-backdrop-filter: blur(22px);
  backdrop-filter: blur(22px);
  animation: pickerUp 0.26s cubic-bezier(0.22, 1, 0.36, 1);
}
.picker[hidden] { display: none; }
@keyframes pickerUp {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: translateY(0); }
}
.picker-top {
  flex: none;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  /* the sheet starts below a sliver of feed, like Slushy's */
  margin-top: 92px;
}
.picker-icon {
  flex: none;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: none;
  background: rgba(255, 255, 255, 0.14);
  color: #fff;
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background 0.15s, transform 0.15s;
}
.picker-icon:active { transform: scale(0.93); }
.picker-icon svg { width: 20px; height: 20px; }
#pickerRefresh.spin svg { animation: pickerSpin 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
@keyframes pickerSpin { to { transform: rotate(360deg); } }
.picker-title {
  flex: 1;
  text-align: center;
  font-size: 16px;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.62);
}
.picker-scroll {
  flex: 1;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  padding: 8px 16px calc(28px + var(--nav-h));
  touch-action: pan-y;   /* sheet scrolls natively; arbiter ignores it */
}
.picker-scroll::-webkit-scrollbar { display: none; }
.picker-scroll.refreshing { opacity: 0.45; transition: opacity 0.18s; }
.picker-h {
  margin: 18px 0 12px;
  /* Deliberately --font, not --display. This heading is ours, not a measured
     Slushy element, and --display is now the real ultra-extended Normalidad —
     at 30px it would run far wider than the sheet. Normalidad is only correct
     where they actually use it: the kickers and the Live bets heading. */
  font-family: var(--font);
  font-size: 30px;
  font-weight: 900;
  letter-spacing: 0.5px;
  color: #fff;
}
.picker-h-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}
.picker-toggle {
  flex: none;
  display: flex;
  align-items: center;
  gap: 8px;
  height: 34px;
  padding: 0 5px 0 12px;
  border: none;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.16);
  cursor: pointer;
  transition: background 0.2s;
}
.picker-toggle[aria-pressed='true'] { background: rgba(115, 255, 0, 0.35); }
.pt-glyphs { display: inline-flex; align-items: center; gap: 3px; }
.pt-glyphs svg { width: 15px; height: 15px; fill: #fff; }
.pt-knob {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.55);
  transition: background 0.2s, transform 0.2s;
}
.picker-toggle[aria-pressed='true'] .pt-knob { background: #fff; transform: translateX(2px); }
.picker-list { display: grid; gap: 12px; }
.picker-row {
  display: flex;
  align-items: center;
  height: 78px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.045);
  color: #fff;
  overflow: hidden;
  cursor: pointer;
  text-align: left;
  transition: background 0.15s, border-color 0.15s, transform 0.12s;
}
.picker-row:active { transform: scale(0.985); }
.picker-row.on { border-color: rgba(255, 255, 255, 0.4); background: rgba(255, 255, 255, 0.09); }
.picker-thumb {
  flex: none;
  width: 62px;
  height: 100%;
  object-fit: cover;
  object-position: 50% 28%;
  display: block;
  background: #2a2c2e;
}
.picker-name {
  flex: 1;
  padding: 0 16px;
  font-size: 17px;
  font-weight: 800;
  letter-spacing: -0.01em;
}
.picker-check {
  flex: none;
  margin-right: 16px;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  background: #fff;
  display: none;
  place-items: center;
}
.picker-row.on .picker-check { display: grid; }
.picker-check svg { width: 15px; height: 15px; stroke: #111; stroke-width: 3; fill: none; }

.coach {
  position: absolute;
  inset: 0;
  z-index: 90;
  display: grid;
  place-items: center;
  padding: 24px;
  background: rgba(10, 10, 12, 0.72);
  -webkit-backdrop-filter: blur(18px);
  backdrop-filter: blur(18px);
  animation: coachIn 220ms ease both;
}
.coach[hidden] { display: none; }
@keyframes coachIn { from { opacity: 0; } to { opacity: 1; } }

.coach-inner { width: 100%; max-width: 340px; text-align: center; }

.coach-title {
  font-size: 21px;
  font-weight: 900;
  letter-spacing: 0.04em;
  margin-bottom: 22px;
}

.coach-panels {
  display: grid;
  grid-template-columns: 1fr 1fr;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: 14px;
  overflow: hidden;
}
.coach-cell {
  padding: 26px 12px 22px;
  display: grid;
  justify-items: center;
  gap: 16px;
}
.coach-cell + .coach-cell { border-left: 1px solid rgba(255, 255, 255, 0.28); }
.coach-cell svg { width: 46px; height: 46px; color: #fff; }
.coach-cell p {
  font-size: 13px;
  font-weight: 700;
  line-height: 1.35;
  color: #fff;
}

.coach-note {
  margin-top: 16px;
  font-size: 12px;
  font-weight: 700;
  color: var(--text-dim);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
}
.coach-dia {
  width: 13px;
  height: 13px;
  display: inline-block;
  background: #fff;
  clip-path: polygon(50% 4%, 92% 34%, 50% 96%, 8% 34%);
}

.coach-got {
  margin-top: 24px;
  width: 100%;
  height: 56px;
  border: 0;
  border-radius: 999px;
  background: #7CFB2D;
  color: #0c1500;
  font-family: var(--font);
  font-weight: 900;
  font-size: 17px;
  cursor: pointer;
  transition: transform 120ms ease, filter 120ms ease;
}
.coach-got:active { transform: scale(0.97); filter: brightness(0.94); }



/* White centre dot instead of a dash between the goal name and its dare. */
.pgoal-dare .mid-dot,
.roomgoal-dare .mid-dot {
  color: #fff;
  font-weight: 900;
  margin: 0 6px;
}
/* Conveyor: the label slides back and forth ONLY when it overflows, so a long
   dare can be read instead of being cut off mid-word (Luke, 2 Aug). */
.pgoal-dare { overflow: hidden; white-space: nowrap; }
.pgoal-dare.scroll {
  animation: goalConveyor 7s ease-in-out infinite alternate;
  animation-delay: 1.2s;
}
@keyframes goalConveyor {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--scroll, 0)); }
}
@media (prefers-reduced-motion: reduce) {
  .pgoal-dare.scroll { animation: none; }
}
