  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
  :root {
    --bg-1: #ffd6e7;
    --bg-2: #ffe9cc;
    --bg-3: #d6e7ff;
    --glass: rgba(255, 255, 255, 0.55);
    --glass-strong: rgba(255, 255, 255, 0.78);
    --glass-border: rgba(255, 255, 255, 0.85);
    --ink: #4a3149;
    --ink-soft: #8a708a;
    --accent: #ff6b9d;
    --accent-2: #ff9a6b;
    --shadow: 0 12px 32px rgba(120, 80, 140, 0.18);
    --shadow-soft: 0 4px 14px rgba(120, 80, 140, 0.10);
    /* HUD + tier-strip + power-up bar + gaps + body-padding. */
    /* HUD + tier strip + power-ups. The strip grew when it took on the rail
       and the caption, so the jar has to be told about it — otherwise a short
       phone runs out of height and the playfield spills off screen. */
    /* Down from 214. The HUD lost a card and the strip lost its container and
       ~18px of its own height, and every pixel saved goes straight to the jar —
       which is the one thing on screen that is supposed to dominate. */
    --vert-reserved: 166px;
    /* Decorative blob colors — overridden per theme */
    --blob-1: #ffafd1;
    --blob-2: #b9d6ff;

    /* ── ONE UI MATERIAL ──────────────────────────────────────────────────
       The interface was running three unrelated materials at once: the HUD had
       become opaque milky, the power-ups pastel colour, and the collection strip
       was still blurred glass. Glass was the odd one out in a way that mattered:
       backdrop-filter saturate(140%) pulls the world's colour INTO the panel, so
       that component alone changed character on every theme and could never sit
       with the other two.
       One surface, one hairline, one shadow — and the ROLE decides how much of
       each a component takes. Information takes the least, interaction the most. */
    --ui-top: rgba(255,254,253,0.95);
    --ui-bot: rgba(250,246,243,0.90);
    --ui-base: #fdfaf8;
    --ui-edge: rgba(255,255,255,0.75);
    --ui-hair: 0 0 0 1px rgba(90, 75, 130, 0.05);
    --ui-lift:
      inset 0 -1px 1px rgba(255,255,255,0.85),
      inset 0 1px 0 rgba(255,255,255,0.95),
      0 1px 2px rgba(72, 60, 95, 0.07),
      0 4px 12px rgba(72, 60, 95, 0.10);
    --ui-ink: #453f50;
    --ui-ink-soft: #6d6678;
    --ui-ink-quiet: #918a9c;
    /* Progression accent. Deliberately NOT var(--accent): that is redefined per
       world and lands on hot pink or red in most of them, which is exactly the
       harshness the track had. One warm, low-saturation coral everywhere. */
    --ui-progress: #d08268;
    --ui-progress-deep: #c26a4e;
  }
  html, body { height: 100%; overflow: hidden; }
  /* The theme wash lives on <html>, not <body>, and is deliberately NOT
     background-attachment: fixed. Both details are about Android.

     A root-element background is painted across the whole canvas by definition,
     so it can never come up short when the WebView is resized — which is exactly
     what happens at launch, while the system-bar insets are still settling.
     `background-attachment: fixed` instead pins the paint to the viewport rect
     the compositor layer was built with, and the strip a resize exposes at the
     bottom is left unpainted: an opaque black band, with the cards still drawn
     over it. It was also buying nothing here — html and body are both
     overflow:hidden, so this page never scrolls and there is nothing for a fixed
     background to hold still against.

     background-color under the layers is the floor: whatever else happens, the
     bottom of the stack is a theme colour and never black.

     Layers, top→bottom: a faint film grain (kills gradient banding + adds a
     tactile candy texture) → a flat white veil → the theme gradient. Grain is a
     tiny tiled SVG — static, cheap, mobile-safe.

     The gradient runs TOP TO BOTTOM, not corner to corner. Each world is now a
     two-colour scheme — sea into dusk, canopy into sun, purple night into
     pumpkin — and a diagonal reads as a swatch where a vertical reads as light
     in a place. The stops descend in lightness for the same reason.

     The veil is 5%, down from 15%. It was there so cards would pop on saturated
     worlds, but it was taking roughly a sixth of the colour off every one of the
     33 and the cards carry their own opaque fills anyway. Measured on-screen
     chroma went 0.061 → 0.11 from this and the palette together. */
  html {
    background:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.04'/%3E%3C/svg%3E") repeat,
      linear-gradient(180deg, rgba(255,255,255,0.05) 0%, rgba(255,255,255,0.05) 100%),
      linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 52%, var(--bg-3) 100%);
    background-size: 160px 160px, cover, cover;
    background-color: var(--bg-2);
  }
  body {
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", system-ui, sans-serif;
    color: var(--ink);
    /* No background here on purpose. <body> painting its own background would
       cover body::before/::after, which are the blur orbs at z-index:-1. */
    display: flex; flex-direction: column;
    align-items: center;
    /* Anchored to the bottom, not centred. Centring split the leftover height
       evenly, which on tall phones parked the power-up row 90-112px above the
       screen edge — measured 94 on a 390x844, 112 on a 412x915. The controls are
       what the thumb reaches for, so the slack belongs ABOVE the board, not under
       the buttons. */
    /* `safe` matters: plain end alignment pushes the overflow off the TOP when
       the content is taller than the screen, and overflow:hidden then eats it
       silently — measured -10px on a 360x640 and -15px on a 320x568, i.e. the
       cloud was being cut. safe falls back to start alignment exactly in that
       case, so nothing is ever lost off the top edge.
       Centred rather than bottom-anchored now: with the gaps above absorbing most
       of the slack, whatever remains is small enough that splitting it reads as
       margin on both sides instead of a hole on one. */
    justify-content: flex-end;
    justify-content: safe flex-end;
    min-height: 100dvh;
    /* Grows a little with the screen, and always clears a gesture bar. Android
       WebViews often report env() as 0 even with gesture nav, so the floor is a
       real number rather than a trust in the inset. */
    padding: 8px 8px max(clamp(14px, 2.2vh, 26px), calc(env(safe-area-inset-bottom) + 8px));
    user-select: none; -webkit-user-select: none;
    overscroll-behavior: none;
  }
  body::before, body::after {
    content: ''; position: fixed; width: 400px; height: 400px;
    border-radius: 50%; filter: blur(84px); opacity: 0.42;
    pointer-events: none; z-index: -1;
    animation: float 16s ease-in-out infinite alternate;
  }
  body::before { background: var(--blob-1); top: -150px; left: -150px; transition: background 0.5s ease; }
  body::after  { background: var(--blob-2); bottom: -150px; right: -150px; animation-delay: -8s; transition: background 0.5s ease; }
  body { transition: background 0.5s ease; }
  @keyframes float {
    from { transform: translate(0, 0) scale(1); }
    to   { transform: translate(38px, 28px) scale(1.07); }
  }
  @media (prefers-reduced-motion: reduce) {
    body::before, body::after { animation: none; }
  }

  @media (max-height: 620px) { :root { --vert-reserved: 188px; } }


  #wrap {
    /* width is constrained by EITHER viewport width OR available height (keeping 420:640 aspect).
       Whichever is smaller wins — playfield never overflows the phone. */
    width: min(420px, calc(100vw - 16px), calc((100vh - var(--vert-reserved)) * 420 / 740));
    width: min(420px, calc(100vw - 16px), calc((100dvh - var(--vert-reserved)) * 420 / 740));
    /* The gap scales with the screen so a tall phone spends its spare height on
       breathing room BETWEEN things rather than as one dead band. On a 412x883
       the board is already as wide as the phone allows, so ~158px has nowhere
       else to go — dumped at the top it was a 150px hole above the HUD, dumped
       at the bottom it was a 94px hole under the controls. */
    /* Raised a little: the height freed by shrinking the HUD-to-board gap is
       spread across every gap here rather than all landing back above the HUD. */
    display: flex; flex-direction: column; gap: clamp(6px, 2.2vh, 21px);
    align-items: stretch;
    position: relative;
  }
  /* The collection strip sits ABOVE the jar. It is a progress read-out, not a
     second playfield: under the jar it competed with the controls for the
     bottom of the screen, which is the one place a thumb is already busy. Above
     the jar it is what you glance at between drops — and the height it gives
     back goes to the jar itself.
     Done with `order` rather than by moving the markup so the DOM keeps reading
     HUD → board → progress → controls for a screen reader. */
  #hud         { order: 1; }
  #tier-strip  { order: 2; }
  #stage       { order: 3; }
  #powerup-bar { order: 4; }

  /* HUD */
  /* Three elements at three weights, not four cards at one weight. The grid gave
     every panel a comparable track and that evenness WAS the clutter: nothing
     told the eye where to land. Now SCORE takes all the slack and the two
     secondary readouts stay exactly as wide as their contents. */
  /* Three heights, not three equal boxes. Everything sitting on one baseline at
     one height is what made the strip read as a row of widgets — the score is the
     primary reading, so it is the one that rises. stretch is gone for the same
     reason: equal height forced equal presence. */
  #hud {
    display: flex; gap: 7px;
    align-items: flex-start;
    justify-content: space-between;
    padding-top: 2px;
  }
  /* Both side readouts hang from the SAME bottom edge, so their captions land on
     one line whatever the globe or the next piece measures. */
  #hud > .action-panel { flex: 0 0 auto; align-self: flex-end; margin-top: 0; }
  #hud > .score-panel  { flex: 0 1 auto; min-width: 124px; max-width: 168px; margin-top: -18px; }
  #hud > .next-panel   { flex: 0 0 auto; align-self: flex-end; margin-top: 0; }
  .panel {
    /* Shared shadow stack, held in a variable because the attract/wish states
       below add a ring on top of it and must not fork their own copy. */
    /* INFORMATION MODULE. Same family as the controls, deliberately a weaker
       member of it: no contact shadow, a shallower ambient one, and a slightly
       thinner surface. Everything up here was carrying the same lift as the
       power-ups, and equal depth is what made four different jobs read as one
       repeated component. Depth is the hierarchy signal, not size. */
    --hud-shadow:
      inset 0 1px 0 rgba(255,255,255,0.90),
      0 2px 7px rgba(72, 60, 95, 0.07),
      var(--ui-hair);
    /* Same construction as the power-up buttons: warm off-white over a solid
       accent layer, with the white held just short of opaque so roughly 5% of
       the world's own accent comes through. That is the whole of the world
       tinting — enough that the card belongs to Halloween or to Aqua, never
       enough for the world to decide the card's contrast.
       backdrop-filter is deliberately gone. blur(14px) saturate(140%) pulled the
       world's colour INTO the card, so the HUD looked different on every theme
       and could not match the bottom buttons; it also cost a live GPU pass on
       five elements every frame, on the cheap Androids this game mostly runs on. */
    /* Genuinely translucent. The old stack ended in a SOLID --ui-base, so the two
       white layers above it had nothing to be translucent over — the card was
       opaque by construction whatever their alphas said. Dropping the base lets
       the world's own gradient come through, which is the whole look.
       No backdrop-filter: a plain white veil already reads as glass over these
       soft grounds, and a live blur on five elements is a per-frame GPU cost on
       the phones this actually ships to. */
    background: linear-gradient(180deg,
      rgba(255,255,255,0.60) 0%, rgba(255,255,255,0.44) 100%);
    border: 1px solid rgba(255,255,255,0.58);
    /* 14, against the power-ups' full capsule and the board's 22 — three
       different radii for three different roles, instead of one soft-card
       radius stamped on everything. */
    border-radius: 14px;
    padding: 7px 12px 8px;
    box-shadow: var(--hud-shadow);
    text-align: center;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
  }
  /* Labels are captions, not headings. 700 at 1.2px tracking was shouting over
     the number it was supposed to introduce; the colour now carries the quietness
     instead of opacity, so the label stays crisp rather than going hazy. */
  .score-panel .label,
  .next-panel .label,
  .action-panel .label {
    font-size: 9.5px; font-weight: 500; opacity: 1;
    color: var(--ui-ink-quiet);
    letter-spacing: 0.9px; text-transform: uppercase;
  }
  /* Score/Best now inherit text-align: center from .panel — uniform row
     of tiles instead of the old scoreboard-edges asymmetry. */
  /* The live score and the record share one baseline row. Baseline alignment is
     what makes them read as one readout rather than as two stacked numbers. */
  /* Stacked, as a secondary metric should be. On one baseline with SCORE it
     had to shrink to 12px to stay subordinate, which is how it ended up reading
     as a footnote; given its own line it can be legible AND clearly second. */
  /* SCORE IS A CLOUD.
   *
   * Shape carries the hierarchy here, which is the one thing size could not do
   * without making the number silly.
   *
   * Built as a MASK, not as pseudo-element bumps. Bumps worked while the card was
   * opaque, but they paint over the card's own background, so the moment the
   * surface became translucent every overlap composited twice and the two
   * ellipses showed as lighter patches sitting on the cloud. A mask carves one
   * fill into the whole silhouette, so the alpha is identical everywhere and
   * there is nothing to double up. drop-shadow() traces the masked shape too,
   * which box-shadow never could.
   *
   * The box is padded at the top to make room for the bumps inside it — a mask
   * can only reveal what the element already covers. */
  .score-panel {
    position: relative;
    border: 0;
    box-shadow: none;
    border-radius: 0;
    background: rgba(255,255,255,0.56);
    -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 130' preserveAspectRatio='none'%3E%3Crect x='0' y='58' width='200' height='72' rx='34' fill='%23fff'/%3E%3Cellipse cx='44' cy='74' rx='36' ry='30' fill='%23fff'/%3E%3Cellipse cx='92' cy='46' rx='48' ry='42' fill='%23fff'/%3E%3Cellipse cx='150' cy='64' rx='40' ry='35' fill='%23fff'/%3E%3C/svg%3E");
    mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 130' preserveAspectRatio='none'%3E%3Crect x='0' y='58' width='200' height='72' rx='34' fill='%23fff'/%3E%3Cellipse cx='44' cy='74' rx='36' ry='30' fill='%23fff'/%3E%3Cellipse cx='92' cy='46' rx='48' ry='42' fill='%23fff'/%3E%3Cellipse cx='150' cy='64' rx='40' ry='35' fill='%23fff'/%3E%3C/svg%3E");
    -webkit-mask-size: 100% 100%;
    mask-size: 100% 100%;
    -webkit-mask-repeat: no-repeat;
    mask-repeat: no-repeat;
    filter:
      drop-shadow(0 1px 1px rgba(72, 60, 95, 0.10))
      drop-shadow(0 5px 11px rgba(72, 60, 95, 0.13));
  }
  /* Id-scoped so it outranks `.panel { padding }` inside the tall-screen media
     query, which sits later in the sheet and was flattening this to 5px — the
     label then sat ABOVE the mask's opaque area and got cut in half. The top
     padding is not spacing here, it is the room the bumps live in. */
  /* Tightened so the jar can be deeper. The cloud's top padding exists to
     make room for the mask's bumps, so it comes down last and least. */
  #hud .score-panel { padding: 21px 11px 6px; }
  /* Id-scoped for the same reason as the cloud above: `.panel { padding }` inside
     the tall-screen media query sits later in the sheet and was giving this 5px
     of bottom padding, which is exactly the amount its caption sat above WORLD's.
     #home-btn escaped it only because an id already outranks a class. */
  #hud .next-panel  { padding: 0 8px; }

  .score-line {
    margin-top: 1px;
    display: flex; flex-direction: column; align-items: center; gap: 2px;
  }
  /* The score carries its world's accent — the colour it already flashes to on
     a merge, now held all the time.
     Not the raw accent though: measured on the cloud across all 33, the raw
     values land between 1.66:1 (Faces) and 5.43:1 (Mystic), and a 27px bold
     figure needs 3:1. Pulling each accent 38% toward a deep ink keeps the hue
     unmistakable and lifts the WORST world to 3.19:1, which is the most accent
     that is readable everywhere. The plain-ink fallback covers WebViews without
     color-mix — readable rather than pretty, which is the right way round. */
  /* --score-ink is solved per world against this panel's own near-white fill:
     lightness walked down until it clears 5.4:1, chroma then as high as the
     gamut allows at that lightness, and the hue taken from the middle of that
     world's wash. It replaces color-mix(--accent 62%, ink) — --accent belongs to
     palettes that no longer exist, and that mix fell under 4.5:1 on seven worlds
     (Faces measured 3.53:1). --ui-ink is the fallback for a WebView with no
     custom property support at all. */
  .score-panel .value {
    font-size: 23px; font-weight: 600; line-height: 1;
    color: var(--score-ink, var(--ui-ink)); letter-spacing: -0.3px;
    font-variant-numeric: tabular-nums;
  }
  /* Roughly a third of SCORE's size, and quieter in colour. BEST used to be 20px
     against SCORE's 27 in a card of its own — close enough that the eye had to
     read both to find the live one. */
  .score-best {
    display: flex; align-items: center; gap: 4px;
    font-size: 12px; font-weight: 500; line-height: 1;
    color: var(--ui-ink-soft);
    font-variant-numeric: tabular-nums;
  }
  /* The real 3D crown, not a system glyph — a flat emoji beside this art would
     be the only 2D thing on the screen. */
  .best-crown {
    width: 12px; height: 12px; display: block;
    filter: drop-shadow(0 1px 1.5px rgba(120, 90, 30, 0.28));
  }
  .score-best .best-k {
    font-size: 8.5px; font-weight: 500; letter-spacing: 0.6px;
    text-transform: uppercase; color: var(--ui-ink-quiet);
  }
  .score-pop { animation: scorePop 0.32s ease-out; display: inline-block; }
  @keyframes scorePop {
    0%   { transform: scale(1); }
    35%  { transform: scale(1.18); color: var(--accent); }
    100% { transform: scale(1); }
  }
  /* Same mechanism as #home-btn: a two-row grid with the caption last. It was a
     reversed flex column, and a flex box and a grid box distribute their leftover
     space differently — which left the two captions 4-5px out of line no matter
     what their own padding said. Same layout model, same box behaviour, one line. */
  .next-panel {
    display: grid;
    grid-template-areas: 'icon' 'label';
    grid-template-rows: 38px auto;
    align-items: center;
    justify-items: center; align-content: end;
    padding: 7px 14px 8px; min-width: 78px;
  }
  .next-panel .label { grid-area: label; }
  .next-img          { grid-area: icon; }
  .next-img {
    width: 42px; height: 42px;
    background-size: contain; background-repeat: no-repeat; background-position: center;
    display: flex; align-items: center; justify-content: center;
    font-size: 34px; line-height: 1;
    /* Room to breathe. No frame around it on purpose — the card IS the frame,
       and the piece is the one thing up here allowed to stay fully colourful. */
    margin: 2px 0 0;
    filter: drop-shadow(0 2px 4px rgba(60, 50, 90, 0.16));
    transition: transform 0.25s ease-out;
  }
  .next-img.bump { animation: bump 0.4s ease-out; }
  @keyframes bump {
    0% { transform: scale(0.6) rotate(-10deg); }
    60%{ transform: scale(1.15) rotate(6deg); }
    100%{ transform: scale(1) rotate(0); }
  }

  /* Stage — styled as a glass jar.
     - Mostly translucent body so the world's bg color shows through (water in aqua, cream in bakery, pink in critters)
     - Bright top rim like a jar opening
     - Big diagonal sun-shine on top-left (::before)
     - Vertical reflection stripe on the right (::after)
     - Bottom inset shadow for jar-base depth
  */
  /* The spare height on a tall phone lands HERE, above the board, and nowhere
     else. Centring split it evenly and reopened a 70-93px hole under the
     controls; bottom-anchoring alone piled 150px above the HUD. Between the HUD
     and the board is the one place a big gap reads as air rather than as a
     hole. */
  /* Gated on height, not just clamped by it: a 360x640 has no spare height at
     all, and a vh-scaled margin there pushed the control row 23px off the bottom
     of the screen. Only phones with slack to spend get to spend it. */
  #stage { margin-top: 0; }
  @media (min-height: 745px) {
    /* More of the spare height parked here means less of it left over at the top,
       because the column is bottom-anchored: growing it from the inside pushes
       its TOP edge up rather than pushing anything down. */
    #stage { margin-top: clamp(0px, 4vh, 40px); }
    /* The height freed by tightening the HUD-to-jar gap goes BELOW the stack, not
       back above it. Bottom-anchored, shrinking a gap inside the column only
       lowers the HUD — the jar stays put. Lifting the whole column off the bottom
       edge is what actually raises the jar. */
    body { padding-bottom: max(clamp(34px, 5.2vh, 54px), calc(env(safe-area-inset-bottom) + 22px)); }
  }

  /* Sits above the canvas and below the goal banner, so the jar reddens but
     nothing the player has to read is tinted. */
  #danger-veil {
    position: absolute; inset: 0;
    z-index: 5;
    pointer-events: none;
    opacity: 0;                      /* intensity — written from the loop */
    transition: opacity 240ms ease;
  }
  /* The band is deliberately short. It covers the rim and the air above it,
     which is where the trouble is; tinting the whole jar would hide the pieces
     the player is trying to read at the exact moment they most need to. */
  #danger-veil .danger-veil-glow {
    position: absolute; inset: 0;
    background: linear-gradient(180deg,
      rgba(255, 38, 76, 0.60) 0%,
      rgba(255, 52, 92, 0.40) 8%,
      rgba(255, 92, 120, 0.17) 19%,
      rgba(255, 120, 142, 0.05) 30%,
      rgba(255, 120, 142, 0) 42%);
    /* Gated on the veil actually being up. The animation used to run
       unconditionally on this child while the PARENT sat at opacity 0, which is
       ~99% of a run: an opacity keyframe the compositor re-evaluated every
       frame, for an element nobody could see. The intensity written from the
       loop is what switches it on. */
    animation: none;
  }
  #danger-veil.is-live .danger-veil-glow {
    animation: dangerBreathe 1.25s ease-in-out infinite;
  }
  @keyframes dangerBreathe { 0%, 100% { opacity: 0.58 } 50% { opacity: 1 } }
  @media (prefers-reduced-motion: reduce) {
    #danger-veil.is-live .danger-veil-glow { animation: none; opacity: 0.85; }
  }

  #stage {
    position: relative;
    width: 100%;
    aspect-ratio: 420 / 740;
    border-radius: 38px 38px 30px 30px;
    overflow: hidden;
    background:
      /* Uniform frost — no brightening near the bottom, so there's no
         "horizon line" where the gradient changes opacity. The world bg
         glows through evenly across the entire play area. */
      linear-gradient(180deg,
        rgba(255,255,255,0.62) 0%,
        rgba(255,255,255,0.34) 14%,
        rgba(255,255,255,0.30) 50%,
        rgba(255,255,255,0.30) 100%);
    box-shadow:
      0 30px 56px rgba(40,70,110,0.28),
      0 14px 22px rgba(40,70,110,0.13),
      0 4px 8px rgba(40,70,110,0.08),
      inset 0 3px 0 rgba(255,255,255,1.0),
      inset 0 8px 14px rgba(255,255,255,0.42),
      inset -4px 0 16px rgba(255,255,255,0.30),
      inset 4px 0 16px rgba(255,255,255,0.26);
    border: 1.5px solid rgba(255,255,255,0.78);
    touch-action: none;
    cursor: pointer;
  }
  /* Glass shines render BEHIND the canvas (z-index: 0). The canvas is
     transparent except where bodies are drawn — so shines show through
     empty space (correct glass-jar look) but bodies stay crisp.
     Slow shimmer animation makes the glass feel alive. */
  #stage::before {
    content: '';
    position: absolute; inset: 0;
    background: radial-gradient(ellipse 55% 32% at 22% 12%,
      rgba(255,255,255,0.55) 0%,
      rgba(255,255,255,0.14) 48%,
      transparent 78%);
    pointer-events: none;
    z-index: 0;
    border-radius: inherit;
    animation: shineDrift 9s ease-in-out infinite alternate;
  }
  #stage::after {
    content: '';
    position: absolute;
    top: 14%; right: 5%;
    width: 8px; height: 50%;
    background: linear-gradient(180deg,
      transparent 0%,
      rgba(255,255,255,0.65) 28%,
      rgba(255,255,255,0.25) 75%,
      transparent 100%);
    border-radius: 8px;
    filter: blur(2.5px);
    pointer-events: none;
    z-index: 0;
    animation: stripeDrift 11s ease-in-out infinite alternate;
  }
  @keyframes shineDrift {
    from { opacity: 0.85; transform: translate(0, 0); }
    to   { opacity: 1.0;  transform: translate(10px, -4px); }
  }
  @keyframes stripeDrift {
    from { opacity: 0.9; transform: translateY(0); }
    to   { opacity: 1.0; transform: translateY(8px); }
  }
  #game-canvas {
    width: 100%; height: 100%; display: block; position: relative; z-index: 1;
    /* Canvas-level mask fades the bottom strip to transparent — body
       bottoms blend smoothly into the stage's frosted bg, no matter how
       big the merge or how many bodies pile up at the floor. The fade
       is part of the canvas element (CSS compositor), not painted per
       frame, so no performance hit on body drawing. */
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent 100%);
    mask-image: linear-gradient(to bottom, #000 calc(100% - 20px), transparent 100%);
  }

  /* HUD action panel — same labeled-tile pattern as the score/next/best
     panels: small caps label on top, icon below. Both home-btn and
     shop-btn share the same first-column grid cell; JS toggles which is
     visible based on context (shop on home screen, home during gameplay). */
  /* First-launch invitation. Ring + lift only — no colour fill, because the
     row already carries the world's own palette and a second colour on top of
     it reads as an error state rather than an invitation. */
  .theme-row.first-cta {
    animation: ctaPulse 1.9s ease-in-out infinite;
    position: relative;
  }
  /* Pointing hand under the play button. The disc pulses on its own, but a
     pulse only says "look here" — the hand says "press this". Sized under the
     tutorial's 48px because the row is smaller than the stage, and parked so
     the fingertip overlaps the disc's lower edge: the tap has to land ON the
     thing, not near it.
     Uses the 3D Fluent asset for the same reason the tutorial does — the
     native emoji renders flat on a lot of Android WebViews. */
  .cta-finger {
    position: absolute;
    right: 20px;           /* 14px row padding + (52px disc - 40px hand)/2 */
    bottom: 4px;
    width: 40px; height: 40px;
    pointer-events: none;
    z-index: 3;
    filter: drop-shadow(0 5px 10px rgba(0,0,0,0.26));
    animation: ctaFingerTap 1.9s ease-in-out infinite;
    will-change: transform;
  }
  .cta-finger img { width: 100%; height: 100%; display: block; }
  /* Press is quick, release is slower, then it waits — a real tap, not a hover.
     Timed so the hand arrives just before the disc reaches its widest, which
     reads as the disc reacting to the touch. */
  @keyframes ctaFingerTap {
    0%, 26%  { transform: translateY(0) scale(1); }
    44%      { transform: translateY(-12px) scale(0.88); }
    62%      { transform: translateY(0) scale(1); }
    100%     { transform: translateY(0) scale(1); }
  }

  /* The invitation lives on the play button, not on a label. A "TAP TO PLAY"
     caption in the corner collided with the NEW badge that already sits there,
     and the row does not need words: the pink button is the affordance, so it
     is the thing that should be asking. */
  .theme-row.first-cta .theme-row-play {
    animation: ctaPlay 1.9s ease-in-out infinite;
  }
  @keyframes ctaPlay {
    0%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(255,107,157,0.45); }
    50%      { transform: scale(1.09); box-shadow: 0 0 0 10px rgba(255,107,157,0); }
  }
  @keyframes ctaPulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(255,107,157,0.00); transform: scale(1); }
    50%      { box-shadow: 0 0 0 5px rgba(255,107,157,0.22); transform: scale(1.015); }
  }

  /* Attract loop on the worlds button until it has been opened once. Slow and
     small on purpose: it should read as "there is something here", not as a
     notification demanding to be cleared. */
  .action-panel.attract { animation: worldsAttract 2.6s ease-in-out infinite; }
  .action-panel.attract .action-icon { animation: worldsIcon 2.6s ease-in-out infinite; }
  /* The ring is always there now, so the attract beat swells it rather than
     fading one in from nothing. */
  @keyframes worldsAttract {
    0%, 72%, 100% { box-shadow: var(--attract-base), 0 0 0 2px rgba(161, 87, 124, 0.13); }
    86%           { box-shadow: var(--hud-shadow), 0 0 0 7px rgba(161, 87, 124, 0.20); }
  }
  /* An unlock is waiting behind this button. Louder than .attract (which only
     says "you have not opened this yet") because this one is time-limited by
     ad inventory, but still ambient — no motion large enough to pull the eye off
     the jar mid-drop. */
  .action-panel.wish-ready {
    border-color: rgba(255,255,255,0.85);
    animation: worldsWish 1.5s ease-in-out infinite;
  }
  /* Still louder than .attract, because this one is time-limited by ad inventory
     — but it earns that by deepening the badge it already has rather than by
     turning it into a different, brighter object. */
  .action-panel.wish-ready .hud-badge {
    background: rgba(161, 87, 124, 0.26);
    color: #7d4160;
    animation: worldsBadge 1.5s ease-in-out infinite;
  }
  @keyframes worldsWish {
    0%, 100% { box-shadow: var(--attract-base), 0 0 0 2px rgba(161, 87, 124, 0.26); }
    50%      { box-shadow: var(--hud-shadow), 0 0 0 8px rgba(161, 87, 124, 0.12); }
  }
  @media (prefers-reduced-motion: reduce) {
    .action-panel.wish-ready,
    .action-panel.wish-ready .hud-badge { animation: none; }
  }

  .action-panel.attract .hud-badge { animation: worldsBadge 2.6s ease-in-out infinite; }
  @keyframes worldsBadge {
    0%, 72%, 100% { transform: scale(1); }
    82%           { transform: scale(1.18); }
    92%           { transform: scale(1.06); }
  }
  @keyframes worldsIcon {
    0%, 72%, 100% { transform: rotate(0deg) scale(1); }
    80%           { transform: rotate(-7deg) scale(1.08); }
    92%           { transform: rotate(5deg)  scale(1.05); }
  }

  /* Every one of these is an attract loop — pure decoration over a message the
     static layout already carries. The hand still points at the button and the
     tile still wears its ring, so nothing is lost by holding them still. */
  @media (prefers-reduced-motion: reduce) {
    .theme-row.first-cta,
    .theme-row.first-cta .theme-row-play,
    .cta-finger,
    .action-panel.attract,
    .action-panel.attract .action-icon,
    .action-panel.attract .hud-badge { animation: none; }
  }

  /* Worlds and Shop sat in a row of four identical glass tiles next to Score,
     Next and Best. Three of those are read-outs and two are controls, and
     nothing told them apart — the eye filed the whole strip as "scoreboard"
     and stopped looking, which is a large part of why only ~20-27% of players
     ever opened the world picker and 6% opened the shop.
     So the two controls now carry a tint, an accent ring and an accent label.
     Same glass language, but they are visibly the only things in the strip
     asking to be pressed. */
  .action-panel {
    cursor: pointer;
    position: relative;
    /* SHARED WITH #shop-btn. Everything specific to the world readout is scoped
       to #home-btn below — putting it here broke Shop on the home screen, which
       has a label and an icon but no badge, so the grid areas left it with an
       empty cell and no card. */
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    gap: 3px;
    padding: 7px 11px 8px;
    min-width: 0;
    font-family: inherit;
    color: var(--ink);
    border-color: rgba(255,255,255,0.58);
    box-shadow: var(--hud-shadow);
    /* The attract/wish rings rebuild box-shadow from scratch, so they need to
       know what they are drawing on top of. */
    --attract-base: var(--hud-shadow);
    transition: transform 0.16s ease, box-shadow 0.2s ease;
  }

  /* ---- WORLDS only ---- */
  #home-btn {
    display: grid;
    grid-template-columns: auto auto;
    /* Label UNDER the pair, not over it: the globe and the figure are the thing
       being read, and the caption belongs beneath what it names. */
    grid-template-areas: 'icon badge' 'label label';
    /* Fixed value row, shared with .next-panel. Aligning only the captions left
       the globe/figure pair and the NEXT piece on centres 13px apart, so the two
       sides read as different heights however level their labels were. One row
       height on both, contents centred in it, puts the VALUES on a line too. */
    grid-template-rows: 38px auto;
    align-items: center; justify-content: center;
    gap: 0;
    padding: 0;
    /* THE SURFACE IS BACK, and the reason it had to come back is worth keeping:
       stripped of its card, this button was rendered in EXACTLY the same visual
       class as the SCORE/BEST readout sitting 42px to its right — a figure over
       a caption, no edge, no ground. Nothing in the game marks read-only text
       and tappable text apart, so players read it as a second scoreboard. The
       only affordance left was a 9px chevron.
       What it gets is not the old 162px slab: a snug glass pill sized to its
       own content, in the HUD's language rather than the powerups' saturated
       one, but carrying the three things a surface needs to say "control" —
       an edge, a lift off the background, and a top highlight. */
    padding: 9px 11px 2px;
    border-radius: 16px;
    background: linear-gradient(180deg, rgba(255,255,255,0.30), rgba(255,255,255,0.17));
    border: 1px solid rgba(255,255,255,0.55);
    backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
    --attract-base:
      inset 0 1px 0 rgba(255,255,255,0.62),
      0 1px 2px rgba(50, 40, 80, 0.10),
      0 5px 14px rgba(50, 40, 80, 0.13);
    box-shadow: var(--attract-base);
    transition: transform 0.16s cubic-bezier(0.34,1.5,0.64,1), box-shadow 0.18s ease, background 0.18s ease;
  }
  /* Nothing says "button" like answering the finger. */
  #home-btn:active {
    transform: scale(0.955);
    background: linear-gradient(180deg, rgba(255,255,255,0.42), rgba(255,255,255,0.26));
    box-shadow: inset 0 2px 4px rgba(50, 40, 80, 0.16), 0 1px 2px rgba(50, 40, 80, 0.10);
    transition-duration: 0.06s;
  }
  #home-btn .label      { grid-area: label; }
  #home-btn .action-icon {
    grid-area: icon;
    /* Smaller and given a real gap. The -3px pull was there to close the space
       to the pill; with the pill gone it dragged the globe into the figures, and
       at 31px it also out-weighed the number it belongs to. */
    width: 25px; height: 25px;
    margin-right: 3px;
  }
  #home-btn .hud-badge   { grid-area: badge; }
  /* The caption style is what made "WORLDS" read as a label about the tile
     rather than an instruction. Same size and tracking as SCORE/BEST so the
     strip stays even, but in the accent at full strength instead of 55% grey. */
  /* NOT var(--accent) — that is redefined per world, so on Aqua the label came
     out teal on a teal background and the button vanished into the scene. The
     picker's play disc already settled this: controls hold one colour across
     all 33 worlds, so they stay findable. Deeper than the tint so it reads
     against it. */
  .action-panel .label {
    /* Still one fixed colour rather than var(--accent) — the original reason
       holds: --accent is redefined per world and on Aqua a teal label on a
       teal-tinted card disappeared. It now sits directly on the world instead of
       on a card, so it carries a soft white lift to hold on the darker ones. */
    color: #a1577c;
    opacity: 1;
    letter-spacing: 0.9px;
    text-shadow: 0 1px 2px rgba(255,255,255,0.75);
  }

  /* Count badge. A number in a pill is the one badge every phone user already
     knows how to read, and "33" is a far stronger reason to tap than any icon:
     it states how much is behind the button. Total worlds, not owned ones —
     the hook is the size of the collection, not the player's progress. */
  .hud-badge {
    position: absolute;
    /* Not the top-right corner: the tile is sized by its own label, so the
       label always fills the content box edge to edge and there is no corner
       to hang in — the badge clipped the "S". The globe is a circle in a
       square box, so its bottom-right corner is empty pixels: the badge sits
       there, touching the icon, covering nothing. */
    /* No longer positioned at all: it sits in the flow beside the globe, so the
       two can never overlap however wide the label makes the card. */
    position: static;
    /* The icon carries a drop-shadow filter, which gives it its own stacking
       context, so it painted over the badge even though the badge is absolute
       and the icon is not. Lift the badge explicitly. */
    z-index: 3;
    /* This is the only container in the component now. */
    min-width: 0; height: auto;
    padding: 3px 15px 4px;
    border-radius: 999px;
    background: linear-gradient(180deg,
      rgba(255,255,255,0.60) 0%, rgba(255,255,255,0.44) 100%);
    border: 1px solid rgba(255,255,255,0.58);
    box-shadow: var(--hud-shadow);
    /* Set into the card, exactly like the counters on the power-up buttons — the
       gradient fill, white ring and coloured drop shadow it had were the same
       three signals that made those read as stickers dropped on top. */
    color: #96506f;
    font-size: 17px; font-weight: 600; line-height: 1.15;
    letter-spacing: -0.2px;
    text-align: center;
    font-variant-numeric: tabular-nums;
    pointer-events: none;
  }
  .action-panel:hover {
    transform: translateY(-2px);
    box-shadow:
      inset 0 -1px 1px rgba(255,255,255,0.85),
      inset 0 1px 0 rgba(255,255,255,0.95),
      0 2px 4px rgba(72, 60, 95, 0.08),
      0 8px 18px rgba(72, 60, 95, 0.13),
      0 0 0 1px rgba(90, 75, 130, 0.07);
  }
  .action-panel:active { transform: translateY(0); }
  .action-panel .action-icon {
    /* Match the 3D Fluent emoji language of the rest of the game (tier
       images, next-fruit preview). A flat stroke SVG sat awkwardly next
       to those vivid 3D heads — this image renders the system actions
       (shop bag, home) in the same dimensional style. */
    display: block;
    width: 26px; height: 26px;
    margin: 0;
    margin: 0;
    filter: drop-shadow(0 2px 4px rgba(60, 50, 90, 0.26));
  }

  /* Diagonal corner ribbon (shop "SALE"). The band is clipped by its OWN
     42px box rather than by overflow:hidden on the button — the button
     holds a 36px emoji that home-active scales to 1.16, and clipping the
     button would shave its edges. The box mirrors the panel's 18px corner
     so the band ends flush with the rounded edge. */
  #shop-btn { position: relative; }
  .corner-ribbon {
    position: absolute; top: 0; left: 0;
    width: 42px; height: 42px;
    overflow: hidden;
    border-top-left-radius: 18px;
    pointer-events: none;
    z-index: 3;
  }
  #shop-btn:not(.has-sale) .corner-ribbon { display: none; }
  /* The label needs no nudging: min-width:70px is only a floor, and on the
     home screen #hud becomes 1fr 1fr 1fr, so this button renders ~130px
     wide. "SHOP" sits centred around x≈100 while the band ends near x≈65 —
     roughly 18px of clearance. An earlier attempt shifted the label down and
     stole the same 6px off the icon's margin to hold the height; that left
     the bag crowding the text, which is the collision it appeared to fix. */
  .corner-ribbon i {
    position: absolute;
    top: 6px; left: -19px;
    width: 62px; padding: 1.5px 0;
    transform: rotate(-45deg);
    background: linear-gradient(135deg, #ff5f88, #ff9163);
    color: #fff; font-style: normal;
    font-size: 7.5px; font-weight: 800; letter-spacing: 0.4px;
    text-align: center;
    box-shadow: 0 1px 4px rgba(175, 40, 80, 0.42);
    /* Nudge, not a loop: two quick beats then a long rest, so the eye is
       caught once in a while instead of being pulled at constantly. This is
       a calm game — a continuously animating tag would fight that. The
       rotate() has to be repeated in every keyframe or the band would
       un-rotate as it animates. */
    animation: sale-nudge 5s ease-in-out infinite;
    will-change: transform;
  }
  @keyframes sale-nudge {
    0%, 86%, 100% { transform: rotate(-45deg) scale(1); }
    90%           { transform: rotate(-45deg) scale(1.09); }
    94%           { transform: rotate(-45deg) scale(0.97); }
    97%           { transform: rotate(-45deg) scale(1.03); }
  }
  @media (prefers-reduced-motion: reduce) {
    .corner-ribbon i { animation: none; }
  }
  #home-btn, #shop-btn { grid-column: 1; grid-row: 1; }

  /* Tier collection strip */
  /* The merge ladder. It was a flat row of eleven small icons, which reads as
     decoration rather than as progress — nothing said how far along you were or
     how far there was to go. It now sits on a rail that FILLS as tiers are
     discovered, and carries a count, so the ladder explains itself at a glance.
     Every size is fluid: the extra height a tall phone has goes into the strip,
     and a short phone gives it back to the jar. */
  #tier-strip {
    /* A little more air above than below: the larger gap separates GAMEPLAY from
       the lower zone, the smaller one holds progression and controls together
       inside it. Even spacing on both sides left the shelf floating between two
       things instead of belonging to one. */
    margin: 0 0 clamp(1px, 0.5vh, 4px);
    display: flex; flex-direction: column; align-items: stretch;
    gap: 0;
    padding: clamp(3px, 0.7vh, 6px) clamp(9px, 3vw, 14px) clamp(3px, 0.7vh, 6px);
    /* PROGRESSION SHELF — no container AND no backing of its own. There were two
       wash layers stacked down here, one behind the shelf and one behind the
       controls, which is what read as "background, glow, UI, another glow". The
       icons are colourful 3D objects and the caption carries its own lift, so
       this layer was buying nothing. One grounding for the whole zone now, and it
       lives under the controls. */
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
  }
  /* ── THE LADDER ────────────────────────────────────────────────────────
     Three layers, top to bottom: the emoji, a milestone track, a line of
     text. Each says one thing.

     The caption sits UNDER the emoji, not over them. The next emoji is
     enlarged, and anything drawn above it — a floating pill, a caption — ends
     up underneath it. That collision was the whole problem with the last pass;
     moving the text below removes it rather than negotiating with it.

     There is no NEXT pill any more. The enlarged glowing icon, the open ring
     on the track beneath it, and a caption that names it are already three
     signals for one idea; a fourth was clutter, which is the one thing the
     design was asked not to add. */

  /* Two parts, not one shouted line. "ALMOST THERE" is the state and stays
     quiet; the unlock's NAME is the only thing worth reading, so it takes the
     weight and the colour. As one uppercase 800-weight string at 1.4px tracking
     the whole thing read as a title competing with the panel it sits in. */
  .tier-progress {
    display: flex; align-items: baseline; justify-content: center; gap: 5px;
    /* The one real gap in the strip: everything above it is the collection,
       this is the line talking about it. */
    margin-top: 4px;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    line-height: 1;
    transition: color 0.35s;
  }
  /* White, with a halo it cannot do without. Measured against the wash directly
     behind this line, white runs 1.9:1 to 2.6:1 — nowhere near enough to carry
     itself on the pale worlds. The tight dark outline gives each glyph its own
     edge instead of relying on the ground being dark. */
  .tier-progress .cap-pre,
  .tier-progress .cap-name,
  .tier-progress .cap-pre.solo,
  .tier-progress.imminent .cap-pre,
  .tier-progress.imminent .cap-name,
  .tier-progress.celebrate .cap-pre,
  .tier-progress.celebrate .cap-name {
    color: #fff;
    text-shadow:
      0 1px 0 rgba(54, 36, 74, 0.50), 0 -1px 0 rgba(54, 36, 74, 0.34),
      1px 0 0 rgba(54, 36, 74, 0.34), -1px 0 0 rgba(54, 36, 74, 0.34),
      0 2px 7px rgba(54, 36, 74, 0.46);
  }
  .tier-progress .cap-pre {
    font-size: clamp(7.5px, 0.95vh, 8.8px);
    font-weight: 500; letter-spacing: 0.9px; text-transform: uppercase;
    opacity: 0.88;
  }
  .tier-progress .cap-name {
    font-size: clamp(9px, 1.15vh, 10.5px);
    font-weight: 600; letter-spacing: 0.5px; text-transform: uppercase;
  }
  /* A lone message (DROP TO BEGIN, ONE MORE MERGE!) has no name to promote, so
     it takes the name's weight itself rather than sitting there whispering. */
  .tier-progress .cap-pre.solo {
    font-size: clamp(8.5px, 1.15vh, 10.5px);
    font-weight: 600;
  }
  /* Warm only when it is actually true. If it always glows, the glow says
     nothing. */
  .tier-progress.celebrate { animation: capPop 0.5s ease-out; }
  @keyframes capPop {
    0%   { transform: scale(0.86); opacity: 0; }
    55%  { transform: scale(1.06); opacity: 1; }
    100% { transform: scale(1);    opacity: 1; }
  }

  .tier-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: clamp(1px, 0.4vw, 3px);
    padding-top: 2px;              /* headroom for the enlarged next emoji */
  }

  /* The track. A milestone under every emoji, and the connector between
     them filling as the board works toward the next one — so the row reads as
     a journey with steps rather than one long anonymous bar. */
  .tier-rail {
    position: relative; height: 7px;
    /* Pulled up under the icons it belongs to. */
    margin: 2px 0 0;
  }
  .tier-rail-track, .tier-rail-fill {
    position: absolute; left: 0; top: 50%;
    height: 3px; margin-top: -1.5px; border-radius: 999px;
  }
  /* White on every world. The wash behind this line is a different colour on all
     33, and a fixed ink was a different amount of ink on each of them. */
  .tier-rail-track {
    right: 0;
    background: rgba(255, 255, 255, 0.42);
    box-shadow: 0 1px 2px rgba(54, 36, 74, 0.14);
  }
  /* One warm coral instead of the per-world accent. The old fill ran
     accent2 -> accent, and since --accent is hot pink or red on most worlds the
     track came out as a bright red line cutting across a calm panel. */
  .tier-rail-fill {
    width: 0%;
    overflow: hidden;
    background: linear-gradient(90deg, rgba(255,255,255,0.80), #ffffff);
    box-shadow: 0 0 6px rgba(255, 255, 255, 0.55);
    transition: width 0.5s cubic-bezier(0.34,1.3,0.64,1), box-shadow 0.4s;
  }
  .tier-rail.imminent .tier-rail-fill { box-shadow: 0 0 10px rgba(255, 255, 255, 0.85); }

  /* Discovery beat: a light runs along everything earned so far. It fires only
     on the unlock itself, so the track stays still the rest of the time. */
  .tier-rail-fill::after {
    content: ''; position: absolute; inset: 0; border-radius: 999px;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,0.9), transparent);
    transform: translateX(-100%); opacity: 0;
  }
  .tier-rail.burst .tier-rail-fill::after { animation: railShine 0.75s ease-out; }
  @keyframes railShine {
    0%   { transform: translateX(-100%); opacity: 0.95; }
    100% { transform: translateX(100%);  opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) {
    .tier-rail.burst .tier-rail-fill::after { animation: none; }
    .tier-dot.pop { animation: none; }
  }

  .tier-dot {
    position: absolute; top: 50%;
    width: 6px; height: 6px; margin: -3px 0 0 -3px;
    border-radius: 50%;
    background: rgba(70, 58, 92, 0.18);
    transition: background 0.35s, transform 0.35s, box-shadow 0.35s;
  }
  .tier-dot.done {
    background: var(--ui-progress);
    box-shadow: 0 0 0 2px rgba(255,255,255,0.70);
  }
  /* Open ring = the one being worked toward. */
  .tier-dot.next {
    width: 9px; height: 9px; margin: -4.5px 0 0 -4.5px;
    background: #fff;
    box-shadow: 0 0 0 2px var(--ui-progress-deep);
  }
  .tier-dot.pop { animation: dotPop 0.55s cubic-bezier(0.34,1.5,0.64,1); }
  @keyframes dotPop {
    0%   { transform: scale(0.4); }
    55%  { transform: scale(1.9); }
    100% { transform: scale(1); }
  }

  .tier-img {
    position: relative;
    z-index: 1;
    flex: 1 1 0;
    width: auto; height: auto;
    max-width: clamp(17px, 2.5vh, 22px);
    aspect-ratio: 1;
    background-size: contain; background-repeat: no-repeat; background-position: center;
    display: flex; align-items: center; justify-content: center;
    font-size: clamp(11px, 1.9vh, 17px); line-height: 1;
    /* Softer than the found ones, but still identifiable — these are the prizes
       the player is playing FOR, and a row of unreadable grey blobs is not a
       collection. Contrast between lit and unlit does the work, not erasure. */
    /* Lifted from 0.52/30%: with the shelf's wash removed these sit directly on
       the world, and on the pale ones a row of nine locked icons had faded past
       "not yet earned" into "cannot tell what that is". They are the prizes the
       player is playing for. */
    opacity: 0.62; filter: grayscale(20%);
    transition: opacity 0.4s, filter 0.4s, transform 0.3s;
  }
  .tier-img.unlocked { opacity: 1; filter: none; transform: scale(1.12); }
  .tier-img.just-unlocked { animation: unlockPop 0.5s ease-out; }
  @keyframes unlockPop {
    0%  { transform: scale(1.1); }
    40% { transform: scale(1.5) rotate(8deg); }
    100%{ transform: scale(1.1) rotate(0); }
  }
  /* Expanding accent ring at the moment a tier is discovered — a satisfying
     "unlocked!" pulse on the ladder itself, on top of the scale pop. */
  .tier-img::after {
    content: '';
    position: absolute; inset: -5px;
    border-radius: 50%;
    border: 2px solid rgba(255,255,255,0.95);
    opacity: 0;
    pointer-events: none;
  }
  .tier-img.just-unlocked::after { animation: tierUnlockRing 0.6s ease-out; }
  @keyframes tierUnlockRing {
    0%   { opacity: 0.85; transform: scale(0.5); }
    100% { opacity: 0;    transform: scale(1.5); }
  }
  /* Next-target beacon — the lowest still-locked tier glows + bobs so the
     player always has a visible "merge to unlock THIS next" carrot. Brighter
     than the dim locked siblings so it reads as the current goal. The next
     beacon position is set in JS (updateTierBeacon) on every discovery. */
  .tier-img.next-target {
    opacity: 0.97;
    animation: tierBeacon 1.6s ease-in-out infinite;
    z-index: 2;
    /* Set apart by a gap on its left — the milestone break between what has
       been found and what is still ahead. An arrow would say the same with one
       more object on screen. */
    margin-left: clamp(5px, 1.9vw, 11px);
    margin-right: clamp(3px, 1vw, 6px);
    /* A lift off the glass, not a glow. The bright white one read as a halo
       stuck behind the emoji. */
    filter: drop-shadow(0 2px 5px rgba(60,90,120,0.22));
  }
  /* The size lives in the keyframes: an animated transform beats a declared
     one, so a scale set outside would be discarded the moment the beacon runs. */
  @keyframes tierBeacon {
    0%, 100% { transform: translateY(0)    scale(1.24); }
    50%      { transform: translateY(-3px) scale(1.36); }
  }
  /* The old beacon ring is off. It was drawn at inset:-5px around the icon, and
     once the next emoji started rendering at 1.24-1.40x the ring scaled with it
     into a big white disc. It is also the THIRD marker for one idea — the emoji
     is already enlarged and already has an open dot beneath it. Size and the
     dot carry it; the ring only added a blob. */
  .tier-img.next-target::after {
    animation: none;
    opacity: 0;
  }
  @keyframes beaconHalo {
    0%, 100% { opacity: 0;    transform: scale(0.8); }
    50%      { opacity: 0.55; transform: scale(1.25); }
  }
  @media (prefers-reduced-motion: reduce) {
    .tier-img.next-target,
    .tier-img.next-target::after,
    .tier-img.just-unlocked::after { animation: none; }
    .tier-img.next-target { opacity: 0.8; }
  }

  /* ===== HUD SHAPE LANGUAGE =====================================
     Three shapes were built and compared at phone size: discs for the side
     readouts, a single shelf holding all three, and this — no containers at all
     except the cloud. The shelf was a large rounded rectangle by another name;
     the discs cost width and cropped NEXT. What is left is one designed shape
     carrying the primary reading, and two readouts sitting directly on the
     world. Nothing here is a card.
     ============================================================== */

  /* NEXT loses its card too, which is what finally makes the two side readouts
     one pair: same treatment, same weight, sitting either side of the cloud.
     With WORLD framed and NEXT not, they read as two unrelated things. */
  .next-panel {
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0 6px;
    min-width: 70px;
  }
  .next-panel .label {
    color: #ffffff;
    font-weight: 600;
    /* Matches #home-btn .label exactly. The two captions are meant to read as one
       line across the top of the screen, so any difference in their own box
       shows up as a step. */
    padding: 1px 0 0;
    line-height: 1.2;
    text-shadow: 0 1px 3px rgba(52, 40, 72, 0.42), 0 0 7px rgba(52, 40, 72, 0.20);
  }
  .next-img {
    width: 46px; height: 46px;
    margin: 0 0 2px;
    /* Nothing behind it any more, so the shadow is what lifts it off the world. */
    filter: drop-shadow(0 3px 7px rgba(52, 40, 72, 0.34));
  }

  /* WORLD — the number is the value, not a badge, so it loses the pill too.
     Globe and figure sit as one pair with a soft lift instead of a container. */
  /* White, and bold enough to hold its own without a card behind it. The muted
     rose was tuned for a pill that no longer exists — on the world itself it
     had nowhere near enough separation, on the pale worlds worst of all.
     The lift underneath is a soft DARK halo, not a white one: white type needs
     the ground darkened behind it, and a halo (wide, no offset) does that
     without ever reading as an outline. */
  .hud-badge {
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0 1px;
    color: #ffffff;
    font-weight: 700;
    text-shadow: 0 1px 3px rgba(52, 40, 72, 0.45), 0 0 8px rgba(52, 40, 72, 0.22);
  }
  /* A chevron is the cheapest honest "this opens something" there is — no card,
     no button chrome, one glyph. Without it the world readout looked exactly like
     the NEXT readout, which is not tappable at all. */
  #home-btn .hud-badge::after {
    content: '\203A';
    display: inline-block;      /* so the idle nudge below can transform it */
    margin-left: 5px;
    font-weight: 700;
    /* Was 0.72 and doing the whole job of saying "this opens something". The
       pill carries that now, so this can simply be legible on all 33 worlds. */
    opacity: 0.95;
    text-shadow: 0 1px 2px rgba(60, 45, 90, 0.30);
  }

  /* Idle tell. Flat for the first two thirds of the cycle and then one small
     beat — a control that moves CONSTANTLY reads as an animation, one that
     rests and then nudges reads as something waiting to be pressed. The globe
     breathes and the chevron leans the way it would take you.
     ~4.4s so it is never the thing you notice while aiming a drop. */
  @keyframes worldIdleGlobe {
    0%, 64%, 100% { transform: translateY(0) scale(1); }
    74%           { transform: translateY(-1.5px) scale(1.07); }
    86%           { transform: translateY(0) scale(1); }
  }
  @keyframes worldIdleChevron {
    0%, 64%, 100% { transform: translateX(0);     opacity: 0.72; }
    76%           { transform: translateX(2.5px); opacity: 1; }
    88%           { transform: translateX(0);     opacity: 0.72; }
  }
  #home-btn .action-icon      { animation: worldIdleGlobe   4.4s ease-in-out infinite; }
  #home-btn .hud-badge::after { animation: worldIdleChevron 4.4s ease-in-out infinite; }
  @media (prefers-reduced-motion: reduce) {
    #home-btn .action-icon, #home-btn .hud-badge::after { animation: none; }
  }
  #home-btn .label {
    color: #ffffff;
    font-weight: 600;
    /* Centred under the pair, not under the figures alone. */
    justify-self: center;
    padding: 1px 0 0;
    line-height: 1.2;
    text-shadow: 0 1px 3px rgba(52, 40, 72, 0.42), 0 0 7px rgba(52, 40, 72, 0.20);
  }
  #home-btn .action-icon { filter: drop-shadow(0 2px 4px rgba(60, 50, 90, 0.28)); }

  /* Overlays */
  .overlay {
    position: absolute; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    text-align: center; padding: 24px;
    /* Neutral white-glass so the per-theme bg gradient breathes through.
       Pink-tinted wash used to clash with cool themes (Aqua/Forest/Cosmos). */
    background: rgba(255, 255, 255, 0.55);
    backdrop-filter: blur(12px) saturate(130%);
    -webkit-backdrop-filter: blur(12px) saturate(130%);
    z-index: 10;
    animation: fadeIn 0.45s ease-out;
  }
  @keyframes fadeIn {
    from { opacity: 0; transform: scale(0.96); }
    to   { opacity: 1; transform: scale(1); }
  }

  /* Offline card. No dismiss button on purpose: there is nothing useful for a
     tap to do, and a button the player can press without anything changing
     reads as broken. It clears itself the moment the connection is back. */
  #net-modal { z-index: 30; }
  .net-icon { width: 84px; height: 84px; margin-bottom: 14px; }
  .net-title {
    font-family: 'Fredoka', sans-serif;
    font-size: 26px; font-weight: 700; color: var(--ink, #3a2b4f);
    letter-spacing: -0.3px; margin-bottom: 8px;
  }
  .net-desc {
    font-size: 14px; line-height: 1.55; opacity: 0.7;
    color: var(--ink, #3a2b4f); margin-bottom: 20px;
  }
  .net-spinner { width: 34px; height: 34px; border-width: 4px; }
  .ring {
    width: 56px; height: 56px;
    border: 5px solid rgba(255,107,157,0.18);
    border-top-color: var(--accent);
    border-radius: 50%;
    animation: spin 0.85s linear infinite;
    margin-bottom: 16px;
  }
  @keyframes spin { to { transform: rotate(360deg); } }
  #loader-progress { font-size: 13px; opacity: 0.6; font-variant-numeric: tabular-nums; }

  #theme-loader { z-index: 40; }
  #theme-loader-title { font-size: 15px; font-weight: 700; letter-spacing: 0.2px; }
  #theme-loader-progress {
    font-size: 13px; opacity: 0.6; font-variant-numeric: tabular-nums; margin-top: 4px;
  }
  #theme-loader-note { font-size: 12px; opacity: 0.45; margin-top: 10px; }

  /* Title screen — title at the top, scrolling theme list in the middle
     (height-capped so the play button + No Ads pill always stay visible),
     play + shop pinned to the bottom via margin-top:auto. */
  /* No panel behind the list. Each row already carries its own world colour, so
     a frosted sheet under them added a third surface — page, panel, row — and
     the rows read as strips trapped in a box. They sit on the page directly. */
  #title-screen {
    justify-content: flex-start;
    padding: 8px 14px 12px;
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  /* CTA sits in its own footer zone — full panel width, clear gap above the
     grid, grounded instead of floating over the cards. */
  #title-screen #start-btn {
    margin-top: 14px;
    width: 100%;
    flex-shrink: 0;
    display: flex;
    justify-content: center;
  }
  .title {
    font-size: 28px; font-weight: 900; letter-spacing: -0.6px;
    background: linear-gradient(95deg, #ff6b9d 0%, #ff9a6b 50%, #6bafff 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    line-height: 1.05; margin-bottom: 10px;
  }
  /* Sub-title progress strip — soft tag under the wordmark. */
  .theme-picker-label {
    font-size: 11px; font-weight: 800;
    letter-spacing: 1.8px; text-transform: uppercase;
    opacity: 0.65;
    margin: 0 0 8px;
    transition: opacity 0.3s ease;
  }
  .theme-picker-label .progress-count { color: var(--accent); opacity: 0.95; }
  .theme-picker-label.bump { animation: labelBump 0.5s ease-out; }
  @keyframes labelBump {
    0%   { transform: scale(1); }
    50%  { transform: scale(1.08); }
    100% { transform: scale(1); }
  }

  /* Vertical theme list — bounded height + internal scroll so the bottom
     CTAs (Play, No Ads) are never clipped. Soft top/bottom mask makes the
     edge scroll feel gradient rather than abrupt. */
  #theme-picker {
    display: grid;
    /* One column. A world now carries a name, a goal and its record — that does
       not fit in half a phone's width, and a list is the right shape for reading
       rows anyway. Five rich rows beat eight tiles you cannot tell apart. */
    grid-template-columns: 1fr;
    gap: 13px;
    align-content: start;
    width: 100%;
    /* flex: 1 1 0 → picker grows to fill available vertical space but
       shrinks below max-height on short devices, keeping Tap-to-Play
       visible on iPhone SE / older Androids. */
    flex: 1 1 auto;
    width: 100%;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 2px 4px 16px;
    -webkit-overflow-scrolling: touch;
    /* A permanent track down the right edge of a world gallery is chrome
       competing with the artwork. The list moving is the scroll cue, and the
       peek row at the bottom already says there is more. */
    scrollbar-width: none;
    /* Soft top fade + a slim bottom fade — just enough to soften the cut so a
       partial "peek" row reads as scrollable, WITHOUT eating its name label. */
    mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 10px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 10px), transparent 100%);
  }
  #theme-picker::-webkit-scrollbar { width: 0; height: 0; }
  /* At the very bottom of the list there's nothing more to hint — drop the
     bottom fade so the last row stays fully crisp. */
  #theme-picker.at-bottom {
    mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 100%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 100%);
  }
  .picker-wrap {
    position: relative;
    flex: 1 1 0;
    min-height: 0;
    width: 100%;
    display: flex;
  }

  /* Tier-strip is the in-game collection preview — hide it on the home screen
     so the world grid is the clean focus. It returns the moment a game starts. */
  /* The home screen holds a static backdrop instead of the selected world's
     gradient. Thirty-three coloured rows have to sit on it, and a backdrop that
     changed with the selection meant a row was legible against some palettes and
     lost against others — Aqua's blue on a blue page, Bakery's cream on cream.
     A cool near-neutral is the one surface every pastel in the set reads on. */
  /* ── Home screen surface ──────────────────────────────────────────────────
     Three moves, in order of what the eye does:
     1. a fixed candy gradient, so worlds are judged against one constant light
     2. two blurred orbs pinned to complementary hues (the game's own idiom) —
        without them the gradient reads as a flat printed sheet
     3. a soft vignette, which is what actually makes the middle feel lit and
        lets the rows sit IN the screen rather than ON it.
     ------------------------------------------------------------------------ */
  /* Orbs dimmed hard. With colour on every card the backdrop's only job is to
     hold them apart, and two bright blooms behind translucent tiles was one
     layer of colour too many. */
  /* The orbs follow the world too, so the whole screen is one place. */
  body.home-active::before { background: var(--blob-1); opacity: 0.20; }
  body.home-active::after  { background: var(--blob-2); opacity: 0.20; }
  /* Rides on <html> for the same reason the base wash does — see the note there.
     The class is mirrored onto documentElement by goHome()/startGame(); every
     other `body.home-active` rule below still keys off <body>. */
  html.home-active {
    background:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.05'/%3E%3C/svg%3E") repeat,
      radial-gradient(120% 70% at 50% 18%, rgba(255,255,255,0.50) 0%, rgba(255,255,255,0) 62%),
      linear-gradient(180deg,
        rgba(255,255,255,0.40) 0%, rgba(255,255,255,0.20) 42%,
        rgba(255,255,255,0.16) 62%, rgba(255,255,255,0.34) 100%),
      linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 52%, var(--bg-3) 100%);
    background-size: 160px 160px, cover, cover, cover;
    background-color: var(--bg-2);
    /* Worlds cross-fade as you arrow through them rather than snapping. */
    transition: background-color 0.45s ease;
  }
  /* The backdrop is mid-tone pastel, not dark: white header text washed out on
     the peach band. Deep plum ink holds against every stop of the gradient. */
  /* White on a mid-tone pastel is the one combination that cannot win — the
     peach band sits at almost the same luminance. Deep berry ink reads on every
     stop of pink → peach → periwinkle, and still feels like this game. */
  body.home-active .title {
    font-size: 28px;
    letter-spacing: -0.7px;
    /* Berry-to-violet, kept a couple of steps brighter than the ink used for
       body text — the wordmark should feel like candy, not like a heading. The
       white halo does the separating, so the letters themselves need not go dark
       to stay readable on the peach band. */
    background: linear-gradient(96deg, #e5487f 0%, #f2735c 46%, #7b6be0 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    /* The halo is doing more work than it used to. This screen no longer sits on
       one fixed peach backdrop — it wears whichever of the 33 worlds you are
       looking at, so the wordmark has to hold against teal, green and indigo as
       well as pink. A wider, denser white glow separates the letters from any of
       them without darkening the brand colour itself. */
    filter:
      drop-shadow(0 0 3px rgba(255,255,255,1))
      drop-shadow(0 0 14px rgba(255,255,255,0.98))
      drop-shadow(0 2px 5px rgba(255,255,255,1));
    margin-bottom: 5px;
  }
  body.home-active .theme-picker-label { color: rgba(64, 48, 86, 0.86); }
  /* The unlock counter and its bar belong to the LIST, which is stowed. On the
     stage they repeat what "17 / 33" already says under the jar. */
  /* With the list stowed there is no flexible child left to take the slack, so
     the column bunched at the top and the screen ended in a dead band. Centre
     the whole thing instead. :has() is Chrome 105+; the fallback is the old
     top-aligned layout, which is merely less balanced, not broken. */
  body.home-active .theme-picker-label .progress-count { color: #8e2f63; }
  body.home-active .worlds-next { color: rgba(90, 48, 92, 0.72); }
  body.home-active .worlds-next-name { color: #8e2f63; }
  body.home-active .worlds-progress { background: rgba(255,255,255,0.50); }

  /* The outer frame added a border with nothing on the other side of it. */
  body.home-active #wrap { border: none; box-shadow: none; background: none; }

  /* BEST is a readout, not a control — it was sitting at the same weight as the
     two things you can actually press. Shop and Next get their icons back. */
  /* The header tiles were flat while everything below them had become glass —
     two design languages on one screen. Same blur, border, radius and shadow. */
  body.home-active #hud .panel {
    background:
      linear-gradient(to bottom, rgba(255,255,255,0.40) 0%, transparent 44%),
      rgba(255,255,255,0.88);
    /* Deliberately NOT backdrop-filtered. At 0.88 opacity only a tenth of the
       backdrop shows through, so a blur is invisible — but it still costs a full
       GPU pass per surface, and five rows plus three header tiles means eight of
       them on one screen. On the Xiaomi/Realme/Tecno hardware most of these
       players are on, that is a frame budget spent on nothing. */
    /* .panel's own blur is switched off here for the same reason. */
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    border: 1.5px solid rgba(255,255,255,0.78);
    border-radius: 20px;
    box-shadow:
      0 10px 22px rgba(88,48,112,0.13),
      0 2px 5px rgba(88,48,112,0.07),
      inset 0 1.5px 2px rgba(255,255,255,0.95);
  }
  body.home-active .score-best { opacity: 0.68; }
  /* Sized, not transformed. scale() does not reserve layout space, so the bag
     grew past the bottom of its own pill. (.next-img was the other half of this
     rule and is hidden on home now.) */
  body.home-active #shop-btn .action-icon { width: 30px; height: 30px; }

  body.home-active #tier-strip { display: none; }

  /* SCORE + NEXT are live-gameplay tiles — meaningless on the idle home screen.
     Hide them there, leaving a clean Shop | Best header; both return the moment
     a game starts (home-active is removed in resetGame). */
  /* Home shows Shop · Next · Best as three even tiles — only the idle "Score 0"
     is hidden (meaningless before a game). Three tiles fill the row cleanly,
     instead of two sparse ones. Next previews the selected world's piece. */
  body.home-active .score-panel { display: none; }
  /* NEXT previews the piece you are about to drop. On the idle home screen
     there is no run to drop it into, so it was a live-gameplay readout sitting
     on a menu. */
  body.home-active .next-panel { display: none; }
  /* #hud stopped being a grid in the HUD rebuild, so this button lost the 1fr
     third that used to give it its width and collapsed onto its own content —
     which put "SHOP" straight under the 42px SALE band and clipped the S.
     A floor wide enough to clear the band restores it under flex. */
  body.home-active #shop-btn { min-width: 112px; }

  /* Home doesn't run the aspect-locked playfield, so let the HUD + panel fill
     the viewport height — no big top/bottom dead-space, and it auto-adapts to
     any device aspect ratio. The grid simply shows more worlds on taller
     screens. (The game screen keeps the strict 420:640 lock.) */
  body.home-active #wrap {
    width: min(440px, calc(100vw - 16px));
    /* Fill most of the height but leave comfortable breathing room top + bottom
       (body centres the wrap, so this slack splits evenly). */
    height: calc(100dvh - 64px);
    max-height: 840px;
  }
  /* #stage is the jar. In game that frosted, heavily-shadowed panel IS the play
     field; on the home screen it was still being drawn, so the world list sat
     inside a giant glass box it had nothing to do with. Strip it back to a bare
     container here — the rows carry their own surfaces. */
  body.home-active #stage {
    aspect-ratio: auto;
    flex: 1 1 auto;
    min-height: 0;
    background: none;
    box-shadow: none;
    border: none;
    border-radius: 0;
    overflow: visible;
  }
  /* ::before is the jar's glass highlight, ::after its vertical shine — both
     only make sense on a jar, and both were still painting over the list. */
  body.home-active #stage::before,
  body.home-active #stage::after { display: none; }
  #theme-picker::-webkit-scrollbar { width: 5px; }
  #theme-picker::-webkit-scrollbar-thumb {
    background: rgba(120,80,140,0.22);
    border-radius: 3px;
  }
  #theme-picker::-webkit-scrollbar-track { background: transparent; }

  /* Row card — taller + more padded for mobile-feel. Both unlocked and
     locked rows render identically (no AD pill, no greyscale). The unlock
     mechanism lives on the Tap-to-Play button's [AD] tag instead, so the
     list itself stays clean and aspirational. */
  .theme-row {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: 14px;
    /* Symmetric side padding; vertical padding sized so the 90px artwork clears
       it. The row was pinned to 92px while holding 101px of art, and both the
       jar and the stats line were being sliced off by overflow:hidden. */
    padding: 11px 14px;
    min-height: 112px;
    /* The row IS the world's swatch: its own gradient, strong enough to tell
       worlds apart at a glance, still pale enough to read text on. */
    /* Against a fixed near-neutral page the wash can carry more of the world's
       colour than it could over a matching gradient — that contrast is the only
       thing telling one row from another now. */
    background:
      linear-gradient(to bottom, rgba(255,255,255,0.34) 0%, transparent 38%),
      linear-gradient(120deg, var(--card-wash-1, transparent) 0%, var(--card-wash-2, transparent) 100%),
      rgba(255,255,255,0.80);
    border: 2px solid transparent;
    border-radius: 20px;
    cursor: pointer;
    border-width: 1.5px;
    border-color: rgba(255,255,255,0.78);
    /* Three shadow ranges = three distances: a wide soft one for the float, a
       mid one for the body, a tight one for contact. One flat shadow is what
       made everything sit on the same plane. */
    box-shadow:
      0 18px 34px rgba(88,48,112,0.11),
      0 7px 14px rgba(88,48,112,0.09),
      0 2px 4px rgba(88,48,112,0.06),
      inset 0 1.5px 2px rgba(255,255,255,0.95);
    transition: transform 0.15s ease, border-color 0.2s ease, box-shadow 0.2s ease;
    position: relative;
    overflow: hidden;
    text-align: left;
  }
  .theme-row:active { transform: scale(0.985); }

  .theme-row-art {
    width: 90px; height: 90px;
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    font-size: 38px; line-height: 1;
  }
  .theme-row-art img {
    width: 100%; height: 100%; object-fit: contain; display: block;
    /* The art is the only real colour in the row now, so it earns a nudge of
       contrast and its own shadow — it should sit above the card, not in it. */
    filter:
      saturate(1.12) contrast(1.06)
      drop-shadow(0 4px 7px rgba(80,50,100,0.16));
  }
  .theme-row-art.is-fallback { filter: drop-shadow(0 4px 7px rgba(40,60,90,0.24)); }

  .theme-row-body { min-width: 0; overflow: hidden; display: flex; flex-direction: column; gap: 5px; align-items: flex-start; }

  /* The world's name on its own frosted chip, sized to the word rather than
     stretched across the row — the full-width gradient bar it replaced read as a
     button and shouted down everything beside it. The chip is glass tinted by
     the world it names, with the same accent as its ink, so each label belongs
     to its world without having to be loud about it. */
  .theme-row-name {
    align-self: flex-start;
    max-width: 100%;
    /* Fredoka at 600, not 700 — the heavier cut reads as a children's app, and
       this screen is meant to feel like Alto or Monument Valley: rounded, but
       quiet. Ink lifted off near-black so the word sits IN the card rather than
       being stamped onto it, and the shadow is gone entirely: a shadow made it a
       button, and there is already exactly one button per row. */
    /* Jakarta, not Fredoka. Fredoka's roundness is the game's logo voice — on a
       list of 33 items every row started looking like a wordmark. Jakarta is
       still soft but reads as a label, which is what this is.
       Fixed min-width so Aqua, Critters and Faces all sit on the same footprint:
       three chip widths in a column was the loudest inconsistency left. */
    font-family: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
    font-size: 19px; font-weight: 700;
    letter-spacing: -0.35px;
    line-height: 1.05;
    color: #574263;
    /* Sized to its word. A fixed 124px looked tidier in a mockup but on a phone
       it either truncated the long names or pushed the badge out of the row —
       and a world called "Hallowee…" is worse than an uneven column. */
    min-width: 0;
    padding: 6px 14px 7px;
    border-radius: 11px;
    text-align: center;
    background: linear-gradient(to bottom, rgba(255,255,255,0.82) 0%, rgba(255,255,255,0.60) 100%);
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.9);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }

  .theme-row-goal {
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 14.5px; font-weight: 600;
    padding-left: 4px;
    color: rgba(48, 32, 62, 0.80);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .theme-row-goal.done { color: #2f9e6b; }

  .theme-row-meta {
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 12px; font-weight: 500;
    padding-left: 4px;
    color: rgba(48, 32, 62, 0.46);
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  /* Progress variant: a hairline bar with the count riding beside it. Kept to
     4px and the world's own accent so it registers as a fill level, not as a
     third control on a row that already has a button. */
  .theme-row-meta.is-progress {
    display: flex; align-items: center; gap: 8px;
    width: 100%; padding-left: 4px; padding-top: 2px;
    overflow: visible;
  }
  .theme-row-bar {
    flex: 1 1 auto;
    min-width: 0; max-width: 132px;
    height: 4px;
    border-radius: 999px;
    background: rgba(70, 48, 88, 0.13);
    overflow: hidden;
  }
  .theme-row-bar-fill {
    height: 100%;
    border-radius: 999px;
    background: linear-gradient(90deg, var(--chip-c1, #ff85ac), var(--chip-c2, #ffa585));
    box-shadow: 0 0 5px rgba(255,140,175,0.35);
    transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1);
  }
  .theme-row-count {
    flex: 0 0 auto;
    font-size: 11.5px; font-weight: 600;
    color: rgba(48, 32, 62, 0.50);
    font-variant-numeric: tabular-nums;
  }
  /* The number you have is the part that matters; the total is context. */
  .theme-row-count b { font-weight: 800; color: rgba(48, 32, 62, 0.78); }


  /* The play affordance. A row was already tappable anywhere, but with no visible
     control the right half of every row read as dead space and nothing said what
     a tap would do. A disc in the world's own accent, and on a locked world the
     same disc wearing the ad price — one object, one message. */
  .theme-row-play {
    position: relative;
    width: 52px; height: 52px;
    flex-shrink: 0;
    border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    /* One colour for every world. A play button that changed hue per row stopped
       reading as "the button" and became more card decoration — the control the
       whole screen exists to serve has to look the same everywhere. */
    background: linear-gradient(150deg, #ff85ac 0%, #ffa585 100%);
    box-shadow:
      0 6px 13px rgba(255,120,160,0.25),
      0 2px 4px rgba(90,40,90,0.12),
      inset 0 1.5px 0 rgba(255,255,255,0.62),
      inset 0 -3px 6px rgba(190,80,120,0.11);
    transition: transform 0.15s ease;
  }
  .theme-row:active .theme-row-play { transform: scale(0.92); }
  .play-tri {
    width: 0; height: 0;
    margin-left: 4px;
    border-left: 15px solid #fff;
    border-top: 10px solid transparent;
    border-bottom: 10px solid transparent;
    filter: drop-shadow(0 1px 1px rgba(0,0,0,0.16));
  }
  /* Ad-gated worlds carry the cost on the button, tucked under it like a price
     tag — visible before the tap, without becoming a second call to action. */
  .theme-row-play.is-ad { margin-bottom: 9px; }
  .play-ad {
    position: absolute;
    bottom: -9px; left: 50%; transform: translateX(-50%);
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 8px; font-weight: 800; letter-spacing: 0.6px;
    color: #fff;
    padding: 2px 7px;
    border-radius: 999px;
    background: rgba(60, 38, 74, 0.82);
    box-shadow: 0 2px 5px rgba(60,38,74,0.30);
  }

  /* ── An unlock we owe the player ──────────────────────────────────────────
     They tapped this world, we had no ad, and we promised to come back to it.
     That promise used to live in a 1.5s toast: 220 refusals produced 31 nudges
     and 1 tap. It now lives on the row they tapped, so it is still there when
     they next open the picker — whenever that is.
     Two states, deliberately different in weight. WAITING is almost silent: it
     is not an offer yet and must not read as a broken button. READY is the
     offer, and gets the accent the picker already uses for "press this". */
  .theme-row.wish-waiting { position: relative; }
  .theme-row.wish-waiting .play-ad {
    background: rgba(60, 38, 74, 0.55);
  }
  /* A slow breath, not a spinner — "we are still holding this for you", without
     claiming anything is loading right now. */
  .theme-row.wish-waiting .theme-row-play {
    animation: wishWaiting 2.8s ease-in-out infinite;
  }
  @keyframes wishWaiting {
    0%, 100% { opacity: 0.72; }
    50%      { opacity: 1; }
  }

  .theme-row.wish-ready {
    box-shadow: 0 0 0 2px rgba(255,107,157,0.55), 0 8px 20px rgba(255,107,157,0.20);
  }
  .theme-row.wish-ready .play-ad {
    background: linear-gradient(150deg, #ff85ac 0%, #ff9f7e 100%);
    box-shadow: 0 2px 6px rgba(220,90,140,0.40);
  }
  .theme-row.wish-ready .theme-row-play {
    animation: wishReady 1.6s ease-in-out infinite;
  }
  @keyframes wishReady {
    0%, 100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(255,107,157,0.45); }
    50%      { transform: scale(1.07); box-shadow: 0 0 0 9px rgba(255,107,157,0); }
  }
  @media (prefers-reduced-motion: reduce) {
    .theme-row.wish-waiting .theme-row-play,
    .theme-row.wish-ready .theme-row-play { animation: none; }
  }

  .theme-row-head {
    display: flex; align-items: center; gap: 11px;
    min-width: 0; max-width: 100%;
    overflow: hidden;
  }
  /* A small pill beside the name. Not a corner ribbon: that corner belongs to
     the play button, and the two were slicing through each other. */
  .theme-row-tag {
    position: absolute;
    top: 9px; right: 12px;
    z-index: 2;
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 7.5px; font-weight: 800; letter-spacing: 0.35px;
    padding: 2.5px 6.5px;
    border-radius: 7px;
    white-space: nowrap;
    color: #fff;
    box-shadow: none;
  }
  /* One colour per kind, so a badge is readable at a glance instead of needing to
     be spelled out — but all six drawn from the same soft band (mid-light, low
     saturation, flat) so the column still reads as one family and none of them
     out-shouts the play button. Colour never depends on locked state: a badge
     describes the world, not whether it has been paid for. */
  .tag-done     { background: #6fbb9b; }   /* earned      — green   */
  .tag-most     { background: #ec8fae; }   /* standout    — pink    */
  .tag-new      { background: #6cc4c0; }   /* fresh       — teal    */
  .tag-popular  { background: #79a8d8; }   /* crowd       — blue    */
  .tag-premium  { background: #a691cf; }   /* elevated    — purple  */
  .tag-seasonal { background: #dfa96a; }   /* time-bound  — amber   */
  .tag-free     { background: #8fc47a; }   /* no cost     — leaf    */
  .tag-ready    { background: #e2b24e; }   /* earned      — gold    */

  /* The world you last played — a ring, nothing louder. There is no longer a
     "selected then press play" step, so selection is only a reminder. */
  /* No selected state. Selection was a leftover from when you picked a world
     and then pressed Play; a row now opens on tap, so highlighting one of them
     only said "this is where you were", which nobody asked. */

  /* Locked rows keep full colour — a greyed row is not something anyone
     reaches for. The lock chip and the "Free with ad" line carry the state. */
  .theme-row.locked .theme-row-art { opacity: 0.92; }
  /* Corner lock chip — glanceable "locked" cue, subtle frosted disc. */
  /* "Watch to unlock" CTA pill — the offer, in a consistent brand gradient
     (learnable: pink pill = free unlock) so it reads as a call-to-action. */

  /* Favorite heart — appears in each row's right slot but ONLY when the
     row is selected (so you can tap it) OR already favorited (so the fav
     state is visible). Otherwise hidden — keeps the unselected list clean. */
  @keyframes heartPop {
    0%   { transform: scale(1); }
    45%  { transform: scale(1.5); }
    100% { transform: scale(1); }
  }

  /* ── Collection progress bar ──────────────────────────────────────────
     Thin accent-gradient bar under the WORLDS · n/33 label; fills as the
     player unlocks more worlds (completionist nudge). */

  /* Collection progress bar under the WORLDS · n/33 label. */
  /* One named target under the bar. Quiet by design — it should read as a nudge
     the eye picks up on the way to Play, not another thing shouting for a tap. */
  .worlds-next {
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 12px; font-weight: 700;
    letter-spacing: 0.2px;
    color: rgba(90, 70, 105, 0.62);
    margin: 0 auto 6px;
    text-align: center;
  }
  .worlds-next-name { color: #8e2f63; font-weight: 800; }
  .worlds-pct { color: #8e2f63; font-weight: 800; }

  .worlds-progress {
    width: 67%; max-width: 230px;
    height: 8px; border-radius: 100px;
    background: rgba(120, 80, 140, 0.14);
    box-shadow: inset 0 1px 2px rgba(120, 80, 140, 0.16);
    margin: 7px auto 9px;
    position: relative;
    /* visible so the glowing head can spill a touch past the fill edge */
    overflow: visible;
    flex-shrink: 0;
  }
  .worlds-progress-fill {
    position: relative;
    height: 100%; width: 0;
    min-width: 9px;
    /* Glossy candy fill: a bright top highlight over the vivid accent gradient,
       with a tight drop + inner base-shade. No soft outer glow — that haze was
       what made the fill read as blurry/washed out. */
    background:
      linear-gradient(180deg, rgba(255,255,255,0.42) 0%, rgba(255,255,255,0) 52%),
      linear-gradient(90deg, #ff9ab8, #ffb59a);
    border-radius: 100px;
    /* Glow on the fill only — the track stays flat, so the coloured part reads
       as lit rather than the whole bar looking hazy. */
    box-shadow:
      0 0 9px rgba(255,140,175,0.55),
      0 1px 2px rgba(120,80,140,0.20),
      inset 0 -1px 2px rgba(0,0,0,0.05);
    transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
  }
  /* Glowing head at the tip of the fill — a little bead of light so the
     collection meter feels alive and rewarding as worlds fill in. */
  .worlds-progress-fill::after {
    content: '';
    position: absolute;
    right: -1px; top: 50%;
    width: 9px; height: 9px;
    transform: translateY(-50%);
    border-radius: 50%;
    background: radial-gradient(circle, #fff 0%, var(--accent-2, #ff9a6b) 58%, transparent 74%);
    box-shadow: 0 0 5px var(--accent-2, #ff9a6b);
  }
  @media (prefers-reduced-motion: reduce) {
    .worlds-progress-fill { transition: none; }
  }

  /* Narrow phones (360-400px) — the row's fixed parts eat the text column, so
     give a little back rather than letting the chip truncate to nothing. */
  @media (max-width: 400px) {
    .theme-row { gap: 11px; padding: 11px 11px; }
    .theme-row-art { width: 82px; height: 82px; }
    .theme-row-play { width: 48px; height: 48px; }
    .play-tri { border-left-width: 14px; border-top-width: 9px; border-bottom-width: 9px; }
    .theme-row-name { font-size: 18px; padding: 5px 12px 6px; }
    .theme-row-tag { top: 8px; right: 10px; }
    /* Disc and padding both shrink here, so the hand has to follow or it drifts
       off-centre: right = row padding + disc/2 - hand/2 = 11 + 24 - 18. */
    .cta-finger { width: 36px; height: 36px; right: 17px; }
  }

  /* Toast — bottom-center floating pill. Fades in/up briefly. */
  /* Anchored in JS to the button it names. Dark like the toast so it reads as
     the same voice, but it points. */
  #coach-tip {
    position: fixed;
    z-index: 210;
    max-width: 250px;
    padding: 8px 14px;
    border-radius: 14px;
    background: rgba(20, 14, 30, 0.92);
    color: #fff;
    font-family: 'Fredoka', -apple-system, "SF Pro Display", system-ui, sans-serif;
    font-size: 12.5px; font-weight: 600; line-height: 1.35;
    letter-spacing: 0.2px; text-align: center;
    box-shadow: 0 8px 22px rgba(20, 14, 30, 0.40);
    pointer-events: none;
    opacity: 0;
    transform: translateY(6px);
    transition: opacity 0.28s ease, transform 0.28s ease;
  }
  #coach-tip.show { opacity: 1; transform: translateY(0); }
  #coach-tip::after {
    content: '';
    position: absolute;
    bottom: -6px; left: var(--arrow-x, 50%); margin-left: -7px;
    border-left: 7px solid transparent;
    border-right: 7px solid transparent;
    border-top: 7px solid rgba(20, 14, 30, 0.92);
  }
  /* The pointed-at button breathes so the tip and the target read as one thing. */
  .powerup-btn.coached {
    animation: coachPulse 1.3s ease-in-out infinite;
  }
  @keyframes coachPulse {
    0%, 100% { transform: scale(1);    filter: brightness(1); }
    50%      { transform: scale(1.09); filter: brightness(1.18); }
  }
  @media (prefers-reduced-motion: reduce) {
    #coach-tip { transition: none; }
    .powerup-btn.coached { animation: none; }
  }

  #toast {
    position: fixed;
    /* Clear of the controls. 90px was tuned against a 36px pill row; the icon
       tiles carry a label under them and the row measures 57-64px on a tall
       phone, so the toast landed across the middle of it and hid two badges.
       Measured worst case: 48px of bottom margin + a 64px row, so it sits above
       both with room to spare. Short phones drop the labels and get the tighter
       offset below. */
    bottom: calc(env(safe-area-inset-bottom, 0px) + 124px);
    left: 50%;
    transform: translateX(-50%) translateY(10px);
    background: rgba(20, 14, 30, 0.88);
    color: white;
    padding: 9px 18px;
    border-radius: 100px;
    font-family: 'Fredoka', -apple-system, "SF Pro Display", system-ui, sans-serif;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.3px;
    z-index: 200;
    pointer-events: none;
    opacity: 0;
    box-shadow: 0 6px 18px rgba(20, 14, 30, 0.35);
    transition: opacity 0.18s ease, transform 0.28s cubic-bezier(0.32, 1.4, 0.5, 1);
  }
  #toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  /* Tap-to-Play button: when the selected theme is locked, show a small
     inline "▶ AD" tag inside the button — tapping the button auto-plays
     the rewarded ad and starts the game in that world. */
  #start-btn { display: inline-flex; align-items: center; gap: 8px; }
  #start-btn .start-ad-tag {
    display: none;
    align-items: center;
    gap: 3px;
    font-size: 10px;
    font-weight: 900;
    letter-spacing: 0.6px;
    padding: 3px 8px 3px 7px;
    border-radius: 100px;
    background: rgba(255,255,255,0.26);
    border: 1px solid rgba(255,255,255,0.5);
    color: white;
    line-height: 1;
  }
  #start-btn .start-ad-tag .ad-icon { font-size: 8px; line-height: 1; }
  #start-btn.locked-theme .start-ad-tag { display: inline-flex; }

  .btn {
    background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);
    color: white; border: none;
    padding: 14px 36px;
    border-radius: 100px;
    font-weight: 700; font-size: 16px;
    cursor: pointer;
    box-shadow: 0 10px 24px rgba(255,107,157,0.36), inset 0 1px 0 rgba(255,255,255,0.4);
    transition: transform 0.15s ease, box-shadow 0.2s ease;
    letter-spacing: 0.4px;
  }
  /* Game-over world offer. Deliberately NOT a .btn — Play Again has to stay the
     loudest thing on this screen, or we have traded a returning player for a
     single unlock. This reads as a card that happens to be tappable: quieter
     fill, no big shadow, and it sits above the primary rather than beside it. */
  .go-world {
    display: flex; align-items: center; gap: 11px;
    width: 100%; max-width: 300px;
    margin: 2px auto 14px;
    padding: 9px 11px;
    border-radius: 18px;
    border: 1.5px solid rgba(255,255,255,0.55);
    background: linear-gradient(150deg, rgba(255,255,255,0.90) 0%, rgba(255,244,248,0.86) 100%);
    box-shadow: 0 6px 16px rgba(90,50,90,0.14);
    font-family: inherit; color: var(--ink);
    cursor: pointer; text-align: left;
    animation: goWorldIn 0.42s ease-out both;
  }
  @keyframes goWorldIn {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  .go-world-text { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
  .go-world-kicker {
    font-size: 9px; font-weight: 800; letter-spacing: 1px; text-transform: uppercase;
    color: #c62a6c;
  }
  .go-world-name {
    font-size: 15px; font-weight: 800; line-height: 1.15;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  }
  .go-world-cta {
    flex-shrink: 0;
    padding: 7px 14px;
    border-radius: 100px;
    background: linear-gradient(150deg, #ff85ac 0%, #ff9f7e 100%);
    color: #fff; font-size: 12px; font-weight: 800; letter-spacing: 0.3px;
    box-shadow: 0 4px 10px rgba(220,90,140,0.30);
  }
  .go-world:active { transform: scale(0.985); }
  @media (prefers-reduced-motion: reduce) { .go-world { animation: none; } }

  .btn:hover { transform: translateY(-2px); box-shadow: 0 14px 28px rgba(255,107,157,0.44), inset 0 1px 0 rgba(255,255,255,0.4); }
  .btn:active { transform: translateY(0); }
  .btn-ghost {
    background: transparent; color: var(--ink-soft);
    border: none; padding: 8px 16px;
    font-weight: 600; font-size: 13px;
    cursor: pointer; margin-top: 6px;
    letter-spacing: 0.4px; opacity: 0.7;
    transition: opacity 0.2s ease;
  }
  .btn-ghost:hover { opacity: 1; }

  /* Close sits at the bottom of a glass panel over the blurred jar, where a
     transparent label had nothing behind it to read against. It is the only way
     out of this panel, so it gets a surface and a real 46px height. */
  #close-settings-btn {
    margin-top: 14px;
    min-height: 46px;
    padding: 12px 34px;
    border-radius: 99px;
    font-size: 15px; font-weight: 600;
    color: var(--ink);
    opacity: 1;
    background: rgba(255, 255, 255, 0.92);
    border: 1px solid rgba(255, 255, 255, 0.9);
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.95),
      0 2px 5px rgba(72, 60, 95, 0.10),
      0 8px 18px rgba(72, 60, 95, 0.13);
    transition: transform 0.18s cubic-bezier(0.34,1.5,0.64,1), box-shadow 0.18s ease;
  }
  #close-settings-btn:active {
    transform: scale(0.965);
    box-shadow: inset 0 2px 4px rgba(72, 60, 95, 0.16);
  }

  #gameover .game-over-title { font-size: 30px; font-weight: 800; margin-bottom: 8px; color: var(--ink); }
  #gameover .label-small { font-size: 11px; letter-spacing: 1.2px; text-transform: uppercase; opacity: 0.55; font-weight: 700; }
  #gameover .final-score { font-size: 56px; font-weight: 900; line-height: 1; margin: 4px 0 4px;
    background: linear-gradient(95deg, var(--accent) 0%, var(--accent-2) 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    font-variant-numeric: tabular-nums;
  }
  #gameover .new-best {
    font-size: 12px; font-weight: 800; letter-spacing: 1.5px;
    color: var(--accent); text-transform: uppercase;
    margin-bottom: 4px; opacity: 0;
    transition: opacity 0.4s;
  }
  #gameover .new-best.show { opacity: 1; }
  #gameover .best-line { margin-bottom: 22px; opacity: 0.6; font-size: 13px; }

  .hidden { display: none !important; }


  /* Fade overlay for level/theme transitions. Sits above every overlay
     (z-index 9999). NOT a hard blackout — uses a translucent dark-plum
     tint with backdrop blur, so the transition reads as a soft veil
     instead of a harsh cut to black. Eye finds the swap less jarring. */
  #fade-cover {
    position: fixed; inset: 0;
    background: rgba(20, 16, 32, 0.78);
    /* The blur moved onto .active. A backdrop-filter on a full-screen fixed
       element is a compositor pass over the whole viewport, and it was declared
       on the resting state — so the one frame it exists for (a theme swap) was
       paying for it, and so was every frame of gameplay in between. */
    opacity: 0;
    pointer-events: none;
    z-index: 9999;
    transition: opacity 0.18s cubic-bezier(0.4, 0, 0.6, 1);
  }
  #fade-cover.active {
    opacity: 1;
    pointer-events: auto;
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
  }

  /* SPACE power-up — pill button in the bottom row, replacing the static
     "Drag to aim · Tap to drop" hint (moved into the title-screen tagline).
     Sits OUTSIDE the playfield so it can never be obscured by bodies, can't
     be tapped by accident while aiming, and the count badge is always
     readable. Disabled (dim, no pulse) when game isn't running so it reads
     as contextually inactive on the title/gameover screens. */
  #powerup-bar {
    position: relative;
    /* Capped and centred: past ~400px the buttons stop growing and the row stays
       a group rather than four widening slabs. Below it they still share the
       width equally, so nothing can clip.
       width:100% + align-self, NOT auto margins — auto margins on a flex item
       cancel the parent's stretch, which shrank the row to its content and left
       it 219px wide inside a 304px column on a small phone. */
    width: 100%;
    max-width: 400px;
    align-self: center;
    display: flex; justify-content: center; align-items: center;
    /* ONE source of truth for the gutter. It used to be set in three places —
       base 10px, 4px under max-width:430, and 12px under min-height:800 — and on
       a 390x844 phone the last two both matched, so the tall-screen rule won and
       widened the gutter by 24px while the buttons kept their narrow-screen
       size. The row then ran 18px past the column and the outer two buttons were
       clipped, on iPhone 14, Pixel 8 and Galaxy A alike. */
    gap: clamp(4px, 1.6vw, 10px);
    /* Close enough to read as one bottom zone, far enough apart that the shelf
       and the controls keep separate roles. 18px of computed gap had them
       floating as two unrelated bands. The negative pull is gone: with the shelf
       no longer carrying its own wash the two need a real, if small, separation.
       #wrap's gap gives the base; this adds to it purely out of the slack that
       tall screens have and short ones do not — 0 extra at 640px tall, ~10 at
       844, ~11 at 932. The column is bottom-anchored, so the extra pushes the
       board UP into that slack rather than the controls down. */
    margin-top: clamp(0px, 1.15vh, 12px);
    min-height: 30px;
  }
  /* Grounding, not a container. A rounded panel here would be a third slab and
     would out-weigh the controls it is meant to support; a wide soft pool of
     light has edges that never resolve, so it separates the row from the
     playfield without drawing a box around it. */
  /* NO BACKING AT ALL down here now.
     There were two wash layers — one behind the shelf, one behind the controls —
     which is what read as "background, glow, UI, another glow". Merging them into
     one broad field looked right in the abstract and wrong on screen: reaching up
     far enough to hold both, it bleached the collection icons it was supposed to
     be grounding. Measured over 90px of reach at up to 34% white.
     Neither layer was earning its cost. The power-ups are the most saturated
     things on the screen and carry their own hue-tinted shadows; the collectibles
     are 3D objects. Spacing groups them, the world shows through, and the zone is
     one composition because its rhythm says so — not because a panel says so. */

  /* Four buttons instead of three, and the row is inside the 420-wide play
     column rather than the viewport — so on a 390 or 412 phone the bar has only
     ~374-396px to work with. The buttons are content-sized, so shrinking the gap
     alone did nothing: they stayed 100/97/83/92 = 372px and the first and last
     were clipped off both edges.
     Everything scales together here — padding, type, icon and badge — because
     taking it out of one of them alone makes the pill look starved rather than
     compact. Going vertical (icon over label) would be narrower still, but the
     bar sits ~18px above the bottom on a 667-tall phone and the extra height
     would not fit. */
  /* These once shrank a row of PILLS to fit a narrow phone. The tiles size
     themselves from the viewport, so width is no longer the constraint — height
     is, and that is handled by the short-screen block further down. */
  @media (max-width: 430px) {
    #powerup-bar .powerup-icon svg { width: 23px; height: 23px; }
  }
  @media (max-width: 380px) {
    #powerup-bar .powerup-icon svg { width: 22px; height: 22px; }
  }
  /* Last resort. The label is the least load-bearing part once the icon is a
     picture of the thing and the badge carries the count. */
  @media (max-width: 345px) {
    #powerup-bar .powerup-label { display: block; }
  }
  /* POWER-UP BUTTONS — one component, four accents.
   *
   * They used to be four fully saturated gradient pills, all pulsing, all at the
   * same visual weight. Against 33 deliberately soft worlds that made the busiest
   * thing on screen the UI rather than the emojis, which are supposed to be the
   * hero. The surface is now a milky white that is ALMOST OPAQUE — deliberately
   * not translucent, because a see-through pill inherits whatever world is behind
   * it and therefore cannot look the same on Cosmos and on Studio. Opaque is the
   * only way "consistent across every theme" is actually achievable.
   *
   * Each button carries its accent in three small places only: the icon, the
   * counter badge, and a 10% wash in the surface. Never as a solid fill. */
  .powerup-btn.shuffle {
    /* Warmer and more saturated than the contrast search first landed on: at
       hue 17 the fill came out terracotta-brown rather than the coral the design
       calls for. Hue 10 with more saturation reads as coral and costs only a
       little contrast (3.5:1), which bold rounded type carries fine. */
    --pw-top: #dc7b64; --pw-bot: #d24f33; --pw-solid: #d6644b;
    --pw-shade: rgba(178, 74, 52, 0.33);
  }
  /* Twin — deep violet-magenta, the one colour the bar did not already use
     (violet Shoot, warm-orange Space, green-teal Fill). */
  .powerup-btn.twin {
    --pw-top: #d166ae; --pw-bot: #c5449a; --pw-solid: #c04395;
    --pw-shade: rgba(160, 55, 125, 0.33);
  }
  /* The creature itself, drawn from the tier art the jar already loaded. */
  /* Twin shows the creature it would hand you rather than a glyph, so it cannot
     match the stroked icons directly. A soft accent ring gives it the same weight
     and footprint as the other three. */
  /* Twin shows the creature it would hand you rather than a glyph. The soft well
     behind it gives it the same footprint and the same light as the three stroked
     icons, so it reads as the fourth member rather than as a photo in a row of
     drawings. */
  /* Twin shows the creature it would hand you rather than a glyph. The pale well
     behind it matches the counter on the other side of the label, so the button
     reads as one object with two lit recesses. */
  .twin-icon {
    width: 22px; height: 22px;
    background-size: 15px 15px; background-repeat: no-repeat; background-position: center;
    background-color: rgba(255,255,255,0.90);
    border-radius: 50%;
    box-shadow: inset 0 1px 1.5px rgba(40, 28, 80, 0.16);
    filter: none;
    font-size: 13px; line-height: 22px; text-align: center;
  }

  /* Fill power-up — cool green-teal variant; distinct from violet (Shoot)
     and warm-orange (Space). No pulse (reuses base) so the bar isn't busy. */
  .powerup-btn.fill {
    /* Lifted and pushed green: contrast-matching to the others had driven this
       down to a deep teal when the brief asks for mint. */
    --pw-top: #41ac94; --pw-bot: #2f8375; --pw-solid: #389c88;
    --pw-shade: rgba(28, 110, 96, 0.33);
  }
  /* POWER-UPS — pastel bodies, not white chips and not the old arcade gradients.
   *
   * The milky version made these read as information rather than as controls,
   * which stopped being survivable once the top HUD became milky too: nothing on
   * screen said "tap me". They carry a real colour again, but a MEASURED one.
   *
   * Every fill is tuned so white text lands at 3.8-4.0:1. That is the whole
   * ruleset. It is also why the four are not the same lightness — teal and coral
   * carry far more luminance than violet at equal HSL lightness, so holding one
   * flat lightness across the row would have made FILL unreadable and SHOOT murky.
   * Worth noting what it replaced: the saturated version ran white text at
   * 2.59 (Shoot), 1.90 (Space), 2.11 (Fill) and 3.48 (Twin) — so this is both
   * calmer AND materially more legible than the bright original.
   *
   * Saturation is capped around 52. Past that these stop being pastel and slide
   * straight back to arcade. */
  .powerup-btn {
    /* Shoot is the default; the other three override above. */
    --pw-top: #8a7fda;
    --pw-bot: #6a5dcd;
    --pw-solid: #7a6dd4;
    --pw-shade: rgba(78, 66, 165, 0.33);
    /* Equal shares of the row, so four buttons ALWAYS fit whatever the gutter,
       the font or the label length works out to — a control dock rather than
       four content-sized pills that happen to add up. min-width:0 is what lets
       a flex item actually shrink below its content. */
    flex: 1 1 0; min-width: 0;
    display: inline-flex; align-items: center; justify-content: center;
    overflow: hidden;
    height: 38px;
    padding: 0 6px;
    border-radius: 100px;
    background: linear-gradient(180deg, var(--pw-top) 0%, var(--pw-bot) 100%);
    border: 1px solid rgba(255,255,255,0.40);
    color: #ffffff;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.5px; text-transform: uppercase;
    /* Barely there, and doing real work: on a mid-tone pastel the white letter
       edges soften into the fill without it. Not an outline — an outline is what
       makes UI look like a mobile-game sticker. */
    text-shadow: 0 1px 1.5px rgba(48, 34, 78, 0.20);
    cursor: pointer;
    box-shadow:
      /* A brighter top highlight and a deeper bottom shade than anything else on
         screen: this is the layer the finger is meant to go to, and depth is what
         says "press me" without a glow or an outline. */
      inset 0 1.5px 0 rgba(255,255,255,0.50),
      inset 0 -1.5px 0 rgba(30, 20, 60, 0.10),
      0 1px 2px rgba(60, 50, 90, 0.12),
      /* The ambient shadow is tinted with the button's OWN hue rather than grey.
         A grey shadow under a coloured toy reads as dirt; a hue-matched one reads
         as light. */
      0 5px 14px var(--pw-shade);
    transition: transform 0.26s cubic-bezier(0.34, 1.5, 0.64, 1),
                box-shadow 0.2s ease, opacity 0.2s ease, filter 0.2s ease;
  }
  /* Icon and label are one unit; the counter is a separate piece of information.
     A single uniform gap made all three read as equally unrelated. */
  .powerup-btn .powerup-icon { margin-right: 5px; }
  .powerup-btn .powerup-count { margin-left: 7px; }
  .powerup-btn:hover {
    transform: translateY(-1px);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.46),
      inset 0 -1px 0 rgba(30, 20, 60, 0.07),
      0 2px 4px rgba(60, 50, 90, 0.10),
      0 7px 17px var(--pw-shade);
  }
  /* Press is near-instant, release springs back past 1 and settles. Two
     different durations on one property is the whole trick — a single symmetric
     transition reads as a slider moving, not as a soft button letting go. */
  .powerup-btn:active {
    transform: scale(0.94);
    transition-duration: 0.06s;
    box-shadow:
      inset 0 2px 4px rgba(30, 20, 60, 0.20),
      0 1px 2px var(--pw-shade);
  }
  .powerup-btn:disabled {
    opacity: 0.42;
    filter: saturate(0.5);
    cursor: not-allowed;
    animation: none;
    pointer-events: none;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.28),
      0 1px 2px rgba(60, 50, 90, 0.07);
  }
  /* Shoot-aim active: brighter gradient + stronger glow so player knows
     they're in aiming mode. Tap the button again to cancel. */
  /* Aiming is a MODE the player is stuck in until they cancel, so this is the one
     place a solid accent is right — quiet everywhere else, unmistakable here. */
  /* Aiming is a mode the player stays in until they cancel. The body is already
     coloured, so this deepens and rings it rather than reaching for a new hue. */
  #powerup-btn.aiming {
    --pw-top: #6d5fca; --pw-bot: #5748b4; --pw-solid: #5748b4;
    border-color: rgba(255,255,255,0.62);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.34),
      0 1px 2px rgba(60, 50, 90, 0.12),
      0 4px 13px rgba(72, 58, 160, 0.34),
      0 0 0 3px rgba(122, 106, 214, 0.24);
  }
  /* The icon inherits the accent, which is how the accent reaches the eye first. */
  .powerup-btn .powerup-icon {
    display: inline-flex; line-height: 0;
    color: #ffffff;
    filter: drop-shadow(0 1px 1px rgba(48, 34, 78, 0.22));
  }
  .powerup-btn .powerup-label { line-height: 1; white-space: nowrap; }
  /* The visual pill stays 38px so it does not eat the board, but the finger gets
     the 44px Apple and 48dp Android both ask for. Invisible, costs no layout. */
  .powerup-btn::after {
    content: '';
    position: absolute;
    left: 0; right: 0; top: 50%;
    transform: translateY(-50%);
    height: max(100%, 44px);
  }
  .powerup-btn { position: relative; }
  /* A circle, not a pill: the count was previously the same shape as the button
     containing it, so at a glance the bar read as eight objects instead of four.
     It only stretches once the number needs two digits. */
  /* A well pressed INTO the pill, not a sticker sitting on it. The badge used to
     be a solid accent disc with its own white ring and its own drop shadow — three
     separate signals that it was a different object, which is exactly why it read
     as a coloured circle dropped on top. Now it is a soft tint of the accent with
     the accent as its ink, and a faint inner shadow so the light agrees with the
     rest of the button. It also drops the accent to about a sixth of its former
     area, which is what "moderately soft" actually costs. */
  /* A light disc set INTO the coloured body — it reads as a hole the light comes
     through rather than as a second sticker laid on top, and the accent numeral
     ties it back to the button it belongs to. */
  .powerup-count {
    min-width: 20px; height: 20px;
    border-radius: 999px;
    background: rgba(255,255,255,0.93);
    color: var(--pw-solid);
    font-size: 11px; font-weight: 700;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0;
    padding: 0 2px;
    box-shadow: inset 0 1px 1.5px rgba(40, 28, 80, 0.16);
    text-shadow: none;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0;
  }
  .powerup-count.zero {
    background: rgba(255,255,255,0.62);
    color: rgba(60, 48, 80, 0.50);
  }
  /* Spending presses IN, gaining pops OUT. Two directions for two meanings —
     one shared "number changed" animation would have said nothing about which.
     Both are under 350ms and neither moves anything but the badge itself. */
  @keyframes countSpent {
    0%   { transform: scale(1); }
    32%  { transform: scale(0.76); }
    100% { transform: scale(1); }
  }
  @keyframes countGained {
    0%   { transform: scale(1); }
    38%  { transform: scale(1.30); }
    100% { transform: scale(1); }
  }
  .powerup-count.spent  { animation: countSpent  0.28s cubic-bezier(0.34,1.40,0.64,1); }
  .powerup-count.gained { animation: countGained 0.34s cubic-bezier(0.34,1.50,0.64,1); }
  @media (prefers-reduced-motion: reduce) {
    .powerup-count.spent, .powerup-count.gained { animation: none; }
  }
  /* AVAILABLE vs UNAVAILABLE. A spent power-up keeps its shape, its icon and its
     name — it must still read as TWIN — but drains most of its colour and sits
     flatter, so the eye picks the usable ones out of the row without reading any
     numbers. Skipped when .ad-ready is on the badge: an empty slot with an ad
     behind it is an offer, not a dead end, and dimming it would hide the offer. */
  .powerup-btn:has(.powerup-count.zero:not(.ad-ready)) {
    filter: saturate(0.34);
    opacity: 0.78;
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.30),
      0 1px 2px rgba(60, 50, 90, 0.08);
  }
  .powerup-btn.reward-flash { animation: powerupReward 0.7s ease-out; }
  @keyframes powerupReward {
    0%   { transform: scale(1); }
    30%  { transform: scale(1.18) rotate(-4deg); }
    60%  { transform: scale(1.08) rotate(3deg); }
    100% { transform: scale(1) rotate(0); }
  }
  /* Ads-free badge — replaces the pink count with a gold ∞ once Remove Ads is owned. */
  .powerup-count.infinite {
    background: linear-gradient(135deg, #ffd93f 0%, #ffaa3a 100%);
    color: white;
    letter-spacing: 0;
    font-size: 14px;
    line-height: 1;
    padding: 0 6px;
    box-shadow: 0 1px 6px rgba(255, 170, 58, 0.55);
  }
  /* Rewarded waiting — a zero count with an ad in hand is not a dead end, so the
     grey 0 becomes a play glyph. Ambient by design: it just sits there whenever an
     ad is earnable, rather than interrupting to announce itself.
     Scoped to #powerup-bar because `.powerup-btn.shuffle .powerup-count` (0,3,0)
     out-specifies a bare `.powerup-count.ad-ready` (0,2,0) and would keep its own
     background on the Space button. The 0 is hidden rather than removed so the
     badge keeps its width and nothing reflows. */
  #powerup-bar .powerup-count.ad-ready {
    /* No `position` here. The badge is absolutely positioned on the tile's
       corner, and an absolutely positioned element is already a containing block
       for its own ::after — re-declaring `relative` put it back into the button's
       column and the teal play badge hung off the bottom edge as a stray green
       circle. */
    background: linear-gradient(180deg, #5fbdaf 0%, #4da8bf 100%);
    color: transparent;
    box-shadow:
      inset 0 1px 1.5px rgba(20, 70, 80, 0.18),
      0 0 0 2.5px rgba(95, 189, 175, 0.18);
  }
  #powerup-bar .powerup-count.ad-ready::after {
    content: '';
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-38%, -50%);
    width: 0; height: 0;
    border-left: 6px solid white;
    border-top: 4px solid transparent;
    border-bottom: 4px solid transparent;
  }
  /* The nudge: two slow breaths, then it settles into the ambient state above and
     never asks again. Only ever applied to a player we actually turned away. */
  #powerup-bar .powerup-count.ad-ready.nudge {
    animation: adReadyBreathe 0.6s ease-in-out 2;
  }
  @keyframes adReadyBreathe {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.2); }
  }
  @media (prefers-reduced-motion: reduce) {
    #powerup-bar .powerup-count.ad-ready.nudge { animation: none; }
  }

  /* Shop modal — full-stage frosted overlay laid out as a flex COLUMN:
     compact header pinned at top, scrollable item list filling the rest,
     and a top-right X to close (no bottom button eating vertical space). */
  #shop-modal {
    justify-content: flex-start;
    padding: 20px 18px 16px;
  }
  /* Corner close — Windows-style X, top-right of the modal. Generic class so
     other modals can reuse it later. */
  .modal-close-x {
    position: absolute;
    top: 14px; right: 14px;
    width: 32px; height: 32px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.75);
    background: rgba(255,255,255,0.5);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: var(--ink-soft);
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    padding: 0;
    z-index: 5;
    box-shadow: 0 2px 8px rgba(40, 70, 110, 0.12);
    transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease;
  }
  .modal-close-x:hover { transform: scale(1.1); background: rgba(255,255,255,0.85); color: var(--ink); }
  .modal-close-x:active { transform: scale(0.92); }
  .modal-close-x svg { width: 16px; height: 16px; display: block; }
  /* Compact header — small bag + wordmark on one row (was a big 62px icon
     stacked over the title, which read oversized and pushed content down). */
  .shop-header {
    display: flex; align-items: center; justify-content: center;
    gap: 9px;
    flex: 0 0 auto;
    margin-bottom: 14px;
  }
  #shop-modal .shop-icon { width: 32px; height: 32px; display: block; filter: drop-shadow(0 3px 8px rgba(255, 170, 60, 0.4)); }
  #shop-modal .shop-title {
    font-size: 24px; font-weight: 800; letter-spacing: -0.4px;
    background: linear-gradient(95deg, #ffaa3a 0%, #ff5b7a 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }
  /* Item list flex-fills the remaining modal height and scrolls internally —
     using flex (not a vh cap) keeps it correct even when the stage is shorter
     than the viewport, so the hero bundle is always visible at the top and the
     list never clips. Soft mask + thin scrollbar; padding-top clears the
     bundle's "Best Value" badge (top:-8px) from the overflow clip. */
  #shop-items {
    width: 100%; max-width: 320px;
    display: flex; flex-direction: column; gap: 10px;
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    padding: 12px 4px 4px;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: thin;
    scrollbar-color: rgba(120,80,140,0.28) transparent;
    mask-image: linear-gradient(to bottom, transparent 0, #000 14px, #000 calc(100% - 12px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 14px, #000 calc(100% - 12px), transparent 100%);
  }
  /* When the shop is open, hide the title-screen's bottom chrome (theme
     tier-strip preview + power-up bar) and the floating "i" so the modal
     reads as a focused, self-contained screen. */
  body.shop-active #tier-strip,
  body.shop-active #powerup-bar,
  body.shop-active .floating-icon-btn { display: none !important; }
  #shop-items::-webkit-scrollbar { width: 5px; }
  #shop-items::-webkit-scrollbar-thumb { background: rgba(120,80,140,0.22); border-radius: 3px; }
  #shop-items::-webkit-scrollbar-track { background: transparent; }
  /* Section divider label — quiet small-caps tag that groups consumable
     power-ups apart from the permanent Premium tier. */
  .shop-section-label {
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-size: 10px; font-weight: 800;
    letter-spacing: 1.4px; text-transform: uppercase;
    color: var(--ink-soft); opacity: 0.6;
    text-align: left;
    margin: 4px 2px -2px;
  }
  .shop-section-label:first-child { margin-top: 0; }
  .shop-item {
    display: flex; align-items: center; gap: 10px;
    background: rgba(255,255,255,0.78);
    border: 1px solid var(--glass-border);
    border-radius: 16px;
    padding: 11px 12px;
    box-shadow: var(--shadow-soft);
    text-align: left;
  }
  /* 3D Fluent icon in a soft tinted chip — matches the game's emoji language
     (tier images, next-fruit, HUD action icons) instead of flat unicode, and
     the per-row tint ties each item to its in-game HUD button colour. */
  .shop-item-icon {
    width: 42px; height: 42px;
    flex-shrink: 0;
    display: flex; align-items: center; justify-content: center;
    border-radius: 13px;
    background: rgba(255,255,255,0.55);
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.6);
  }
  .shop-item-icon img {
    width: 32px; height: 32px; display: block;
    filter: drop-shadow(0 2px 4px rgba(40,60,90,0.22));
  }
  #shop-item-shoot  .shop-item-icon { background: linear-gradient(135deg, rgba(168,144,255,0.26), rgba(93,200,255,0.20)); }
  #shop-item-space  .shop-item-icon { background: linear-gradient(135deg, rgba(255,170,58,0.26), rgba(255,91,138,0.20)); }
  #shop-item-fill   .shop-item-icon { background: linear-gradient(135deg, rgba(78,200,138,0.26), rgba(42,163,200,0.20)); }
  #shop-item-bundle .shop-item-icon { background: linear-gradient(135deg, rgba(255,170,140,0.34), rgba(255,140,170,0.26)); }
  #shop-item-adfree .shop-item-icon { background: linear-gradient(135deg, rgba(255,217,63,0.36), rgba(255,170,58,0.26)); }
  .shop-item-info { flex: 1; min-width: 0; }
  .shop-item-name { font-size: 14px; font-weight: 800; line-height: 1.2; }
  .shop-item-desc { font-size: 11px; opacity: 0.62; margin-top: 2px; line-height: 1.3; }
  .btn-buy {
    background: linear-gradient(135deg, #ffaa3a 0%, #ff5b7a 100%);
    color: white; border: none;
    padding: 8px 14px;
    border-radius: 100px;
    font-weight: 800; font-size: 12px;
    letter-spacing: 0.4px;
    cursor: pointer;
    box-shadow: 0 6px 14px rgba(255, 100, 100, 0.32), inset 0 1px 0 rgba(255,255,255,0.4);
    transition: transform 0.15s ease, box-shadow 0.2s ease;
    flex-shrink: 0;
    white-space: nowrap;
    min-width: 64px;
  }
  .btn-buy:hover { transform: translateY(-1px); box-shadow: 0 8px 18px rgba(255, 100, 100, 0.4), inset 0 1px 0 rgba(255,255,255,0.4); }
  .btn-buy:active { transform: translateY(0); }
  .btn-buy:disabled { cursor: not-allowed; transform: none; }
  .shop-item.owned .btn-buy {
    background: rgba(120, 80, 200, 0.14);
    color: #5d3a8a;
    box-shadow: none;
    pointer-events: none;
  }
  .shop-item.processing .btn-buy {
    background: rgba(120, 80, 200, 0.18);
    color: #5d3a8a;
    pointer-events: none;
  }
  .shop-item.owned { background: linear-gradient(135deg, rgba(168,144,255,0.14), rgba(93,200,232,0.10)); }
  /* "Best Value" badge — top-right corner of the Power Bundle row,
     a gradient pill that draws the eye to the bundle as the smart pick. */
  .shop-item { position: relative; }
  .shop-item-badge {
    position: absolute;
    top: -8px; right: 12px;
    background: linear-gradient(95deg, #ff7d4f, #ff4d8a);
    color: #fff;
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-size: 9px; font-weight: 800;
    letter-spacing: 0.6px;
    padding: 3px 8px;
    border-radius: 100px;
    text-transform: uppercase;
    box-shadow: 0 4px 10px rgba(255, 90, 130, 0.35);
  }
  /* Bundle item gets a subtle warm tint so it pops from siblings. */
  #shop-item-bundle {
    background: linear-gradient(135deg, rgba(255, 220, 200, 0.6), rgba(255, 195, 220, 0.55));
    border-color: rgba(255, 170, 140, 0.5);
  }
  /* Confirmation toast for power-up purchases. */
  .shop-toast {
    position: absolute;
    top: 20px;
    left: 50%;
    transform: translate(-50%, -8px);
    background: linear-gradient(95deg, var(--accent), var(--accent-2));
    color: #fff;
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-weight: 700; font-size: 12px;
    letter-spacing: 0.3px;
    padding: 8px 16px;
    border-radius: 100px;
    box-shadow: 0 8px 18px rgba(0,0,0,0.18);
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    z-index: 30;
  }
  .shop-toast.show {
    animation: shopToastInOut 2.2s ease forwards;
  }
  @keyframes shopToastInOut {
    0%   { opacity: 0; transform: translate(-50%, -8px); }
    15%  { opacity: 1; transform: translate(-50%, 0); }
    80%  { opacity: 1; transform: translate(-50%, 0); }
    100% { opacity: 0; transform: translate(-50%, -8px); }
  }

  /* Ad modal — same overlay system as title/gameover. Reuses .btn for the watch
     CTA, with a violet→cyan gradient skin (.btn-ad) to match the power-up theme. */
  #ad-modal .ad-icon { font-size: 56px; line-height: 1; margin-bottom: 10px; filter: drop-shadow(0 4px 12px rgba(168, 144, 255, 0.5)); }
  #ad-modal .ad-title {
    font-size: 22px; font-weight: 800; margin-bottom: 8px; letter-spacing: -0.4px;
    background: linear-gradient(95deg, #7a4dc0 0%, #3aaad8 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }
  #ad-modal .ad-desc { font-size: 14px; opacity: 0.7; max-width: 280px; line-height: 1.45; margin-bottom: 22px; }
  .btn-ad {
    background: linear-gradient(135deg, #a890ff 0%, #5dc8ff 100%);
    box-shadow: 0 10px 24px rgba(120, 80, 200, 0.4), inset 0 1px 0 rgba(255,255,255,0.4);
  }
  .btn-ad:hover { box-shadow: 0 14px 28px rgba(120, 80, 200, 0.5), inset 0 1px 0 rgba(255,255,255,0.4); }
  .ad-progress {
    width: 240px;
    background: rgba(255,255,255,0.55);
    border: 1px solid var(--glass-border);
    border-radius: 14px;
    padding: 14px 16px;
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
  }
  .ad-progress-bar-track {
    height: 8px; background: rgba(168, 144, 255, 0.18);
    border-radius: 4px; overflow: hidden;
    margin-bottom: 8px;
  }
  .ad-progress-bar {
    height: 100%; width: 0;
    background: linear-gradient(90deg, #a890ff 0%, #5dc8ff 100%);
    border-radius: 4px;
    transition: width 0.08s linear;
  }
  .ad-progress-text { font-size: 12px; opacity: 0.7; text-align: center; font-weight: 600; }

  /* Continue modal — see the CONTINUE MODAL block below for the card itself. */
  #continue-modal .continue-desc { font-size: 14px; opacity: 0.72; line-height: 1.45; margin-bottom: 16px; text-align: center; }
  .continue-timer {
    position: relative;
    width: 90px; height: 90px;
    margin-bottom: 18px;
  }
  .timer-svg { width: 100%; height: 100%; transform: rotate(-90deg); }
  .timer-bg, .timer-fill {
    fill: none;
    stroke-width: 6;
  }
  .timer-bg { stroke: rgba(120, 80, 140, 0.15); }
  .timer-fill {
    stroke: url(#timer-grad);
    stroke-linecap: round;
    stroke-dasharray: 289;        /* 2π × 46 ≈ 289 */
    stroke-dashoffset: 0;
    transition: stroke-dashoffset 0.1s linear;
  }
  /* Gradient stroke for the timer ring via inline SVG defs (added by JS) */
  .timer-fill { stroke: #ff6b3a; }
  .timer-count {
    position: absolute;
    inset: 0;
    display: flex; align-items: center; justify-content: center;
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-size: 30px; font-weight: 700;
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }
  #continue-modal .ad-progress { margin-top: 10px; }

  /* ===== CONTINUE MODAL =========================================
     Cut from the game's own material. Everything here was previously a
     hand-picked hex — a white slab, a #c8623f coral and a #5f6fce CTA — and
     blue does not exist anywhere else in this game, so the loudest thing on the
     screen was also the only foreign one. These are the same --ui-* tokens the
     HUD panels and the power-up dock use, and --ui-progress, the single warm
     coral that already means "progress" throughout. */

  #continue-modal {
    padding: 18px;
    background: rgba(255,255,255,0.40);
    backdrop-filter: blur(20px) saturate(78%);
    -webkit-backdrop-filter: blur(20px) saturate(78%);
  }
  .cont-card {
    width: min(304px, 100%);
    display: flex; flex-direction: column; align-items: center;
    padding: clamp(17px, 3.2vh, 23px) 20px clamp(12px, 2.4vh, 17px);
    border-radius: 26px;
    background: linear-gradient(180deg, var(--ui-top), var(--ui-bot));
    border: 1px solid var(--ui-edge);
    box-shadow: var(--ui-hair), var(--ui-lift), 0 16px 36px rgba(72,60,95,0.16);
    animation: contCardIn 0.34s cubic-bezier(0.22,1,0.36,1) both;
  }
  @keyframes contCardIn {
    from { opacity: 0; transform: translateY(10px) scale(0.97); }
    to   { opacity: 1; transform: none; }
  }

  #continue-modal .continue-title {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(21px, 3vh, 25px); font-weight: 600;
    letter-spacing: -0.2px; margin-bottom: 3px;
    background: none; -webkit-background-clip: border-box; background-clip: border-box;
    color: var(--ui-ink);
  }
  #continue-modal .continue-desc {
    font-size: 13px; font-weight: 500; line-height: 1.4;
    color: var(--ui-ink-soft); opacity: 0.9;
    margin-bottom: clamp(11px, 2vh, 15px); text-align: center;
  }
  /* The full jar is the REASON, so it sits under the headline at reason size. */
  #continue-modal.is-offer .continue-desc { display: block; }

  /* ---- the reward, shown as the jar it is about ---- */
  #continue-modal .cont-reward { display: none; }
  #continue-modal.is-offer .cont-reward {
    display: flex; align-items: center; justify-content: center; gap: 14px;
    width: 100%;
    background: color-mix(in srgb, var(--ui-progress) 9%, transparent);
    border-radius: 20px;
    padding: 12px 16px;
    margin-bottom: clamp(13px, 2.4vh, 18px);
  }
  @supports not (color: color-mix(in srgb, red 50%, blue)) {
    #continue-modal.is-offer .cont-reward { background: rgba(208,130,104,0.09); }
  }
  .cont-jar { position: relative; flex: 0 0 auto; }
  .cont-jar-svg { display: block; width: 44px; height: 56px; }
  .jar-stays  { fill: var(--ui-progress); opacity: 0.34; }
  /* The part that goes: same colour, brighter, and it lifts and fades on a loop
     so the drawing says "this clears" without a sentence. */
  .jar-clears { fill: var(--ui-progress-deep); animation: jarClear 2.6s ease-in-out infinite; }
  .jar-edge   { fill: none; stroke: var(--ui-progress-deep); stroke-width: 2; opacity: 0.55; }
  .jar-line   { stroke: var(--ui-base); stroke-width: 2; opacity: 0.9; }
  @keyframes jarClear {
    0%, 62%, 100% { transform: translateY(0);    opacity: 0.92; }
    78%           { transform: translateY(-4px); opacity: 0.30; }
    90%           { transform: translateY(0);    opacity: 0.92; }
  }
  .cont-spark {
    position: absolute; top: -7px; right: -11px;
    width: 24px; height: 24px;
    filter: drop-shadow(0 2px 4px rgba(150, 90, 60, 0.30));
    animation: contSpark 2.6s ease-in-out infinite;
  }
  @keyframes contSpark {
    0%, 62%, 100% { transform: scale(0.88); opacity: 0.75; }
    78%           { transform: scale(1.10); opacity: 1; }
  }
  .cont-reward-copy { text-align: left; }
  .cont-reward-line { display: flex; align-items: baseline; }
  .cont-pct {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(38px, 6vh, 46px); font-weight: 600; line-height: 0.92;
    letter-spacing: -1.4px;
    color: var(--ui-progress-deep);
  }
  .cont-of {
    font-size: 12.5px; font-weight: 600; line-height: 1.25;
    color: var(--ui-ink-soft); margin-top: 3px;
  }

  /* ---- CTA. The strongest thing on the card, and now in the game's colour. ---- */
  #continue-modal .btn-ad {
    width: 100%;
    display: flex; align-items: center; justify-content: center; gap: 9px;
    height: 52px; border-radius: 99px;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 16px; font-weight: 600; letter-spacing: 0.2px;
    color: #fff; border: 1px solid rgba(255,255,255,0.42);
    background: linear-gradient(180deg, #e09070 0%, var(--ui-progress-deep) 100%);
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.42),
      inset 0 -1.5px 0 rgba(90, 40, 20, 0.12),
      0 2px 5px rgba(120, 70, 50, 0.16),
      0 8px 20px rgba(180, 100, 70, 0.30);
    transition: transform 0.24s cubic-bezier(0.34,1.5,0.64,1), box-shadow 0.2s ease;
    animation: contCtaBreathe 3.6s ease-in-out infinite;
  }
  #continue-modal .btn-ad:active {
    transform: scale(0.965);
    transition-duration: 0.06s;
    box-shadow: inset 0 2px 5px rgba(90, 40, 20, 0.24), 0 1px 3px rgba(120, 70, 50, 0.16);
  }
  /* Rests, then one small lift — a button that pulses without pause reads as an
     advert, which is the one thing this must not look like. */
  @keyframes contCtaBreathe {
    0%, 72%, 100% { transform: translateY(0); }
    82%           { transform: translateY(-2px); }
    92%           { transform: translateY(0); }
  }
  .cta-play {
    width: 0; height: 0;
    border-left: 10px solid #fff;
    border-top: 6.5px solid transparent;
    border-bottom: 6.5px solid transparent;
    margin-left: -2px;
  }

  /* Countdown + decline share one quiet row. */
  .cont-foot {
    display: flex; align-items: center; justify-content: center; gap: 12px;
    margin-top: clamp(9px, 1.8vh, 14px);
  }
  #continue-modal .continue-timer { width: 26px; height: 26px; margin: 0; }
  #continue-modal .timer-count { font-size: 12px; font-weight: 600; color: var(--ui-ink-quiet); }
  /* In the offer state the countdown is a footnote beside the decline. In the
     other states it is the only thing telling you what happens next. */
  #continue-modal:not(.is-offer) .continue-timer { width: 54px; height: 54px; margin-top: 4px; }
  #continue-modal:not(.is-offer) .timer-count { font-size: 20px; }
  #continue-modal:has(#continue-ad-progress:not(.hidden)) .continue-timer { display: none; }
  #continue-modal .timer-bg { stroke: rgba(90, 75, 130, 0.14); }
  #continue-modal .timer-fill { stroke: color-mix(in srgb, var(--ui-progress) 70%, transparent); }
  @supports not (color: color-mix(in srgb, red 50%, blue)) {
    #continue-modal .timer-fill { stroke: rgba(208,130,104,0.7); }
  }
  #continue-modal .btn-ghost {
    font-size: 13.5px; font-weight: 600; color: var(--ui-ink-soft); opacity: 0.72;
    /* Generous target even though the label is small — a decline that is hard to
       hit is a dark pattern whatever it says. And it says what actually happens:
       "No thanks" hid the consequence, "End run" states it. */
    padding: 12px 16px; margin: 0;
  }
  #continue-modal .btn-ghost:active { opacity: 1; }
  #continue-modal .ad-progress {
    width: 100%; background: none; border: 0; box-shadow: none; padding: 0; margin-top: 14px;
  }

  @media (prefers-reduced-motion: reduce) {
    .cont-card, #continue-modal .btn-ad, .jar-clears, .cont-spark { animation: none; }
  }

  /* ---- coins ---------------------------------------------------------------
     Drawn, not an emoji: the asset set has no coin, and borrowing Candy or
     Spiral shell would collide with tier art. A disc in the game's own progress
     coral keeps it out of casino-gold territory, which the audit ruled out
     explicitly, and costs nothing to load. */
  /* A drawn disc was the only non-emoji object in a game made entirely of 3D
     Fluent art, and at 12px it read as a flat brown dot. The glowing star is a
     real asset from the same set, so it sits with the tier art and the globe —
     and it is not used as a tier in any of the 33 worlds, so it cannot be
     mistaken for something droppable. Warm rather than gold: still not casino. */
  .coin-icon {
    width: 1.15em; height: 1.15em; flex: none; display: block;
    filter: drop-shadow(0 1px 2px rgba(150, 110, 40, 0.30));
  }
  /* ---- compact dialog ------------------------------------------------------
     A decision this small does not need the whole screen. The scrim only has
     to separate the card from the list; the list staying visible is what keeps
     this feeling like part of the game rather than a system dialog. */
  .overlay.overlay-light {
    background: rgba(255,255,255,0.28);
    backdrop-filter: blur(5px) saturate(96%);
    -webkit-backdrop-filter: blur(5px) saturate(96%);
    padding: 20px;
  }
  .ad-card {
    width: min(300px, 100%);
    display: flex; flex-direction: column; align-items: center;
    padding: clamp(16px, 3vh, 22px) 22px clamp(12px, 2.2vh, 16px);
    border-radius: 28px;
    background: linear-gradient(180deg, var(--ui-top), var(--ui-bot));
    border: 1px solid var(--ui-edge);
    box-shadow: var(--ui-hair), var(--ui-lift), 0 18px 40px rgba(72,60,95,0.18);
    animation: contCardIn 0.32s cubic-bezier(0.22,1,0.36,1) both;
  }
  /* The world's own art, not a padlock. A player deciding whether to unlock
     Xmas should be looking at Xmas. */
  .unlock-art {
    width: clamp(64px, 17vw, 82px); height: clamp(64px, 17vw, 82px);
    border-radius: 20px; display: block; margin-bottom: 9px;
    box-shadow: 0 6px 16px rgba(80,55,110,0.24), 0 0 0 3px rgba(255,255,255,0.75);
  }
  #ad-modal .ad-icon { font-size: 34px; line-height: 1; margin-bottom: 4px; }
  #ad-modal.is-unlock .ad-icon { display: none; }
  #ad-modal .ad-title {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(20px, 5.4vw, 24px); font-weight: 600;
    letter-spacing: -0.3px; color: var(--ui-ink); margin-bottom: 3px;
  }
  #ad-modal .ad-desc {
    font-size: 13px; line-height: 1.5; color: var(--ui-ink-soft);
    text-align: center; margin-bottom: clamp(11px, 2.2vh, 16px);
  }
  #ad-modal .btn-ad {
    width: 100%; height: 48px; border-radius: 99px;
    white-space: nowrap;
    display: flex; align-items: center; justify-content: center; gap: 8px;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 15.5px; font-weight: 600; color: #fff;
    border: 1px solid rgba(255,255,255,0.42);
  }
  #ad-modal .btn-ghost {
    font-size: 13.5px; font-weight: 600; color: var(--ui-ink-soft);
    opacity: 0.72; padding: 11px 16px; margin-top: 2px;
  }
  #ad-modal .ad-progress { width: 100%; background: none; border: 0; box-shadow: none; padding: 0; }

  /* The unlock offer's numbers. Cost leads because it is the decision; what
     they hold is the context under it. Same coral the coins use everywhere. */

  /* The unlock CTA in the game's own colour. The shared .btn-ad is periwinkle,
     which appears nowhere else in Squishy Merge — the continue modal was moved
     off it for the same reason. Same action, same colour. */
  #ad-modal .btn-ad {
    background: linear-gradient(180deg, #e09070 0%, var(--ui-progress-deep) 100%);
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.42),
      inset 0 -1.5px 0 rgba(90, 40, 20, 0.12),
      0 2px 5px rgba(120, 70, 50, 0.16),
      0 8px 20px rgba(180, 100, 70, 0.30);
  }
  #ad-modal .btn-ad:active {
    box-shadow: inset 0 2px 5px rgba(90, 40, 20, 0.24), 0 1px 3px rgba(120, 70, 50, 0.16);
  }

  /* Top-right, opposite the shop, above the list. Present without competing:
     it answers "how close am I" at a glance and is never the loudest thing. */
  .coin-chip-home {
    position: fixed;
    top: 10px; right: 46px;
    z-index: 100;
    font-size: 16px;
    padding: 7px 14px 7px 9px;
    gap: 6px;
    border-radius: 99px;
    background: linear-gradient(180deg, rgba(255,255,255,0.62), rgba(255,255,255,0.42));
    border: 1px solid rgba(255,255,255,0.72);
    backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 3px 10px rgba(80,55,110,0.12);
    color: var(--ui-ink);
  }
  body:not(.home-active) .coin-chip-home { display: none; }

  /* The unlock dialog's numbers. */
  .ad-desc .u-line, .ad-desc .u-have { display: block; }
  .u-cost {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 1.7em; font-weight: 600; color: var(--ui-progress-deep);
    vertical-align: -2px;
  }
  .u-have { font-size: 0.94em; opacity: 0.75; margin-top: 3px; }
  #watch-ad-btn.is-coin-buy {
    background: linear-gradient(180deg, #e09070 0%, var(--ui-progress-deep) 100%);
  }
  .coin-chip {
    grid-area: coin;
    display: inline-flex; align-items: center; gap: 4px;
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 12px; font-weight: 800;
    color: var(--ui-ink); justify-self: center;
    margin-top: 2px;
    text-shadow: 0 1px 2px rgba(255,255,255,0.7);
  }


  .run-reward {
    display: flex; flex-direction: column; align-items: center; gap: 8px;
    margin: 2px 0 10px;
  }
  .run-coin-row {
    display: inline-flex; align-items: center; gap: 6px;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(19px, 5.2vw, 24px); font-weight: 600;
    color: var(--ui-progress-deep);
  }
  .run-double {
    display: inline-flex; align-items: center; justify-content: center; gap: 8px;
    min-height: 42px; padding: 0 20px; border-radius: 99px;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 14.5px; font-weight: 600; color: #fff;
    border: 1px solid rgba(255,255,255,0.42);
    background: linear-gradient(180deg, #e09070 0%, var(--ui-progress-deep) 100%);
    box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.40), 0 5px 14px rgba(180,100,70,0.28);
    transition: transform 0.18s cubic-bezier(0.34,1.5,0.64,1);
  }
  .run-double:active { transform: scale(0.95); }

  /* ---- rewarded unavailable ------------------------------------------------
     The honest third state. Not greyed out to the point of looking broken —
     just clearly not an offer right now, so the player stops tapping it. */
  .powerup-btn.is-ad-unavailable { opacity: 0.55; }
  .powerup-btn.is-ad-unavailable .pcount { background: rgba(90,75,120,0.30); color: rgba(255,255,255,0.75); }
  .theme-row.is-ad-unavailable .theme-row-play { opacity: 0.5; filter: saturate(0.6); }
  .theme-row.is-ad-unavailable .play-ad { background: rgba(90, 75, 120, 0.62); }
  /* Affordable with coins: the price chip carries the coin colour so it reads as
     a price rather than as another AD marker. */
  /* Locked rows carry a coin, not a price. */
  .theme-row.locked .play-ad {
    background: linear-gradient(180deg, rgba(255,255,255,0.92), rgba(255,255,255,0.78));
    border: 1px solid rgba(255,255,255,0.9);
    padding: 3px; line-height: 0;
    box-shadow: 0 2px 5px rgba(80,50,100,0.20);
  }
  .theme-row.locked .play-ad img { width: 14px; height: 14px; display: block; }
  .theme-row.is-ad-unavailable .play-ad { opacity: 0.8; }

  /* ---- goal banner ------------------------------------------------------
     Same glass material as the HUD panels, sized in viewport-relative units so
     it holds from a 360-wide phone to a tall 430. Anchored to the stage, not to
     the viewport, so it cannot collide with a notch or the system bars. */
  .goal-banner {
    position: absolute;
    left: 50%; top: 14%;
    transform: translateX(-50%);
    z-index: 6;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    max-width: min(86%, 320px);
    padding: clamp(9px, 2.2vh, 14px) clamp(14px, 4.5vw, 22px);
    border-radius: 20px;
    background: linear-gradient(180deg, var(--ui-top), var(--ui-bot));
    border: 1px solid var(--ui-edge);
    box-shadow: var(--ui-hair), var(--ui-lift);
    pointer-events: none;
    animation: goalIn 0.42s cubic-bezier(0.22,1,0.36,1) both;
  }
  .goal-banner.is-leaving { animation: goalOut 0.4s ease-in both; }
  @keyframes goalIn  { from { opacity:0; transform:translateX(-50%) translateY(-8px) scale(0.96) } }
  @keyframes goalOut { to   { opacity:0; transform:translateX(-50%) translateY(-6px) scale(0.98) } }
  .goal-lead {
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 9.5px; font-weight: 800; letter-spacing: 1.4px;
    text-transform: uppercase; color: var(--ui-ink-quiet);
  }
  .goal-main { display: flex; align-items: center; gap: 8px; }
  .goal-emoji { width: clamp(26px, 7vw, 34px); height: clamp(26px, 7vw, 34px); display: block;
                filter: drop-shadow(0 3px 6px rgba(60,40,90,0.24)); }
  .goal-main b {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(16px, 4.6vw, 20px); font-weight: 600; letter-spacing: -0.2px;
    color: var(--ui-ink); white-space: nowrap;
  }
  /* The destination, marked on the strip the game already has rather than in a
     second progress bar. A ring, not a fill — it is a place, not a level. */
  .tier-dot.is-goal-tier {
    box-shadow: 0 0 0 2.5px var(--ui-progress), 0 0 0 4.5px rgba(255,255,255,0.75);
  }
  .tier-img.is-goal-tier { filter: drop-shadow(0 2px 5px rgba(200,110,70,0.5)); }
  @media (prefers-reduced-motion: reduce) {
    .goal-banner, .goal-banner.is-leaving { animation: none; }
  }

  /* Mid-game ad-break card — lighter frost than the full overlay so the jar
     stays visible behind it (reads as a brief pause, not a screen takeover). */
  .break-overlay {
    background: rgba(255, 255, 255, 0.42);
    backdrop-filter: blur(10px) saturate(125%);
    -webkit-backdrop-filter: blur(10px) saturate(125%);
    z-index: 12;
  }
  .break-card {
    display: flex; flex-direction: column; align-items: center;
    max-width: 300px;
    padding: 26px 30px 22px;
    background: rgba(255, 255, 255, 0.72);
    border: 1px solid rgba(255, 255, 255, 0.8);
    border-radius: 26px;
    box-shadow: 0 18px 50px rgba(80, 50, 110, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.7);
    animation: breakPop 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
  }
  @keyframes breakPop {
    from { opacity: 0; transform: translateY(10px) scale(0.9); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes buoyBob {
    0%, 100% { transform: translateY(0) rotate(-4deg); }
    50%      { transform: translateY(-5px) rotate(4deg); }
  }
  .break-icon {
    width: 62px; height: 62px; margin-bottom: 10px;
    filter: drop-shadow(0 6px 14px rgba(150, 110, 60, 0.35));
    animation: buoyBob 2.4s ease-in-out infinite;
  }
  .break-title { font-family: 'Fredoka', sans-serif; font-size: 22px; font-weight: 700; color: var(--ink); margin-bottom: 6px; }
  .break-desc { font-size: 13.5px; opacity: 0.72; line-height: 1.45; margin-bottom: 16px; text-align: center; }
  .break-timer { width: 62px; height: 62px; margin-bottom: 0; }
  .break-timer .timer-count { font-size: 22px; }
  @media (prefers-reduced-motion: reduce) {
    .break-icon { animation: none; }
    .break-card { animation: none; }
  }

  /* When the share modal is open, hide everything that competes with the
     jar card — the HUD up top and the tier-strip / power-up bar below. */
  body.share-active #hud,
  body.share-active #tier-strip,
  body.share-active #powerup-bar,
  body.share-active .floating-icon-btn { display: none !important; }

  /* Share modal — full viewport, transparent. Breaks out of the .wrap
     so brand/score/buttons sit OUTSIDE the (flattened) game container,
     not cramped inside it. */
  #share-modal {
    position: fixed;
    inset: 0;
    background: transparent;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    justify-content: space-between;
    padding: 44px 24px 34px;
    gap: 16px;
  }
  /* Small home button, top-left corner of the share screen — replaces the
     bottom "Play again" link. Tapping returns to the title screen. */
  .share-home-btn, .share-replay-btn {
    position: absolute;
    top: 16px;
    width: 38px; height: 38px;
    border-radius: 50%;
    border: 1px solid rgba(255,255,255,0.85);
    background: rgba(255,255,255,0.38);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: #ffffff;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer; padding: 0; z-index: 5;
    box-shadow: 0 2px 10px rgba(40,70,110,0.14);
    transition: transform 0.15s ease, background 0.2s ease;
  }
  .share-home-btn { left: 16px; }
  .share-replay-btn { right: 16px; }
  .share-home-btn:hover, .share-replay-btn:hover { transform: scale(1.1); background: rgba(255,255,255,0.55); }
  .share-home-btn:active, .share-replay-btn:active { transform: scale(0.92); }
  .share-home-btn svg, .share-replay-btn svg { width: 18px; height: 18px; display: block; filter: drop-shadow(0 1px 2px rgba(40,70,110,0.45)); }
  /* Onboarding tutorial — first-play coaching layer. Sits above the
     canvas but doesn't block input (pointer-events: none on container,
     only Skip catches clicks). Hidden after the player completes it. */
  #tutorial {
    position: absolute;
    inset: 0;
    z-index: 8;
    pointer-events: none;
  }
  #tutorial.hidden { display: none; }
  .tutorial-finger {
    position: absolute;
    top: 18%;
    left: 50%;
    font-size: 44px;
    line-height: 1;
    filter: drop-shadow(0 6px 14px rgba(0,0,0,0.28));
    transform: translate(-50%, 0);
    will-change: transform;
    animation: tutFingerSwipe 1.6s ease-in-out infinite;
  }
  /* Always the 3D Fluent hand (native 👆 renders flat/2D on some Android devices) */
  .tutorial-finger img { width: 48px; height: 48px; display: block; }
  @keyframes tutFingerSwipe {
    0%, 100% { transform: translate(calc(-50% - 60px), 0) rotate(-8deg); }
    50%      { transform: translate(calc(-50% + 60px), 0) rotate(8deg); }
  }
  .tutorial-caption {
    /* Below the animated finger (at top: 18%) but well above the
       body pile that builds up near the floor. */
    position: absolute;
    left: 50%;
    top: 32%;
    transform: translateX(-50%);
    background: linear-gradient(95deg, var(--accent), var(--accent-2));
    color: #fff;
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-weight: 700; font-size: 13px;
    letter-spacing: 0.2px;
    padding: 9px 18px;
    border-radius: 100px;
    box-shadow: 0 8px 18px rgba(0,0,0,0.16);
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
  }
  .tutorial-caption.show {
    animation: tutCaptionIn 0.4s ease forwards;
  }
  @keyframes tutCaptionIn {
    from { opacity: 0; transform: translate(-50%, 8px); }
    to   { opacity: 1; transform: translate(-50%, 0); }
  }
  /* When share modal is active: flatten the entire game container so
     the inner jar card is the ONLY visible chrome on screen. */
  body.share-active #game-canvas { visibility: hidden; }
  body.share-active #stage {
    background: transparent !important;
    box-shadow: none !important;
    border: none !important;
  }
  body.share-active #stage::before,
  body.share-active #stage::after { display: none !important; }
  #share-modal .share-top {
    display: flex; flex-direction: column; align-items: center;
    width: 100%;
  }
  #share-modal .share-bottom {
    display: flex; flex-direction: column; align-items: center;
    width: 100%;
  }
  #share-modal .share-brand {
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-size: 12px; font-weight: 800;
    letter-spacing: 3.2px;
    color: #ffffff;
    text-shadow: 0 1px 3px rgba(50, 30, 55, 0.4);
    margin-bottom: 6px;
    opacity: 0;
    animation: shareItemIn 0.5s ease 0.05s forwards;
  }
  #share-modal .share-score-big {
    font-size: 28px; font-weight: 900;
    letter-spacing: -0.4px;
    line-height: 1;
    /* White with a soft dark shadow so it stays readable + pops on every
       theme's light pastel bg, warm or cool. */
    color: #ffffff;
    text-shadow: 0 1px 3px rgba(50, 30, 55, 0.45), 0 2px 9px rgba(50, 30, 55, 0.3);
    margin-bottom: 4px;
    opacity: 0;
    animation: shareScoreIn 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) 0.15s forwards;
  }
  #share-modal .share-points-label {
    font-family: 'Fredoka', -apple-system, sans-serif;
    font-size: 10px; font-weight: 800;
    letter-spacing: 2.5px;
    color: rgba(255, 255, 255, 0.88);
    text-shadow: 0 1px 3px rgba(50, 30, 55, 0.4);
    margin-bottom: 0;
    opacity: 0;
    animation: shareItemIn 0.5s ease 0.25s forwards;
  }
  @keyframes shareItemIn {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @keyframes shareScoreIn {
    0%   { opacity: 0; transform: scale(0.6); }
    60%  { opacity: 1; transform: scale(1.08); }
    100% { opacity: 1; transform: scale(1); }
  }

  /* The captured jar is a transparent body PNG; the card reproduces the
     in-game #stage glass EXACTLY so the share card reads as the same glossy
     glass jar: frosted gradient + diagonal sun-shine sit BEHIND the PNG (so
     they glow through the gaps between bodies, like in the live game), inset
     highlights form the bright glass rim/walls, and ::after is the vertical
     reflection stripe as front-glass gloss. */
  .share-jar-card {
    flex: 0 1 auto;
    position: relative;
    width: auto;
    max-width: 92vw;
    max-height: 62vh;
    border-radius: 30px 30px 24px 24px;
    overflow: hidden;
    background:
      radial-gradient(ellipse 55% 30% at 24% 12%,
        rgba(255,255,255,0.50) 0%, rgba(255,255,255,0.12) 46%, transparent 76%),
      linear-gradient(180deg,
        rgba(255,255,255,0.55) 0%,
        rgba(255,255,255,0.30) 16%,
        rgba(255,255,255,0.26) 50%,
        rgba(255,255,255,0.26) 100%);
    border: 1.5px solid rgba(255, 255, 255, 0.82);
    box-shadow:
      0 22px 48px rgba(40, 70, 110, 0.26),
      0 10px 18px rgba(40, 70, 110, 0.12),
      0 3px 8px rgba(40, 70, 110, 0.08),
      inset 0 3px 0 rgba(255,255,255,1.0),
      inset 0 8px 14px rgba(255,255,255,0.42),
      inset -4px 0 16px rgba(255,255,255,0.30),
      inset 4px 0 16px rgba(255,255,255,0.26);
    margin: 0;
    opacity: 0;
    animation: shareJarPop 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) 0.32s forwards;
  }
  /* Vertical reflection stripe — front-glass gloss, same as #stage::after. */
  .share-jar-card::after {
    content: '';
    position: absolute;
    top: 12%; right: 6%;
    width: 7px; height: 48%;
    background: linear-gradient(180deg,
      transparent 0%,
      rgba(255,255,255,0.62) 28%,
      rgba(255,255,255,0.22) 75%,
      transparent 100%);
    border-radius: 8px;
    filter: blur(2.5px);
    pointer-events: none;
    z-index: 3;
  }
  #share-modal .share-jar-img {
    display: block;
    width: auto;
    height: auto;
    max-width: 92vw;
    max-height: 62vh;
  }
  @keyframes shareJarPop {
    0%   { transform: scale(0.7) rotate(-4deg); opacity: 0; }
    60%  { transform: scale(1.03) rotate(0.6deg);  opacity: 1; }
    100% { transform: scale(1) rotate(0); opacity: 1; }
  }
  /* Deliberately NOT the pink .btn treatment Share wears — this screen's own
     job is sharing, and two solid pills fighting each other would just make the
     screen louder without making either one clearer. Glass over the world's
     backdrop, with the art carrying the colour. */
  /* The world offer is the loudest thing on this screen — Share gave up its
     pulse and some of its width for it. A run just ended: sharing is optional,
     but a world they have never seen is what decides whether they open the game
     again tomorrow, and 25 ad-gated worlds between them get fewer plays than the
     one free one.
     Content-sized rather than pinned to Share's width: at a fixed width the
     longest world ("Play Halloween World") ellipsised on a 360px phone, cutting
     the one word the button exists to say. */
  #share-modal .share-world {
    display: inline-flex; align-items: center; gap: 9px;
    width: auto;
    max-width: min(96%, 344px);
    box-sizing: border-box;
    position: relative;
    margin-bottom: 12px;
    padding: 7px 11px 7px 7px;
    border-radius: 100px;
    border: 1.5px solid rgba(255,255,255,0.80);
    background: rgba(255,255,255,0.94);
    font-family: inherit; color: var(--ink);
    cursor: pointer;
    opacity: 0;
    animation:
      shareItemIn 0.45s ease 0.6s forwards,
      shareWorldPulse 1.9s ease-in-out 1.2s infinite;
  }
  @keyframes shareWorldPulse {
    0%, 100% { transform: scale(1);     box-shadow: 0 8px 20px rgba(60,40,70,0.20), 0 0 0 0 rgba(255,107,157,0.42); }
    50%      { transform: scale(1.035); box-shadow: 0 10px 24px rgba(60,40,70,0.24), 0 0 0 12px rgba(255,107,157,0); }
  }
  /* An explicit target to press. The pill was tappable everywhere but looked
     like a label, and a row with nothing button-shaped in it reads as
     information rather than as an offer. */
  #share-modal .share-world-go {
    flex-shrink: 0;
    width: 26px; height: 26px;
    border-radius: 50%;
    background: linear-gradient(150deg, #ff85ac 0%, #ff9f7e 100%);
    box-shadow: 0 3px 8px rgba(220,90,140,0.36);
    position: relative;
  }
  /* Play triangle drawn in borders, the same shape .play-tri uses in the picker,
     so the two controls speak the same language. */
  #share-modal .share-world-go::after {
    content: '';
    position: absolute; top: 50%; left: 50%;
    transform: translate(-42%, -50%);
    width: 0; height: 0;
    border-left: 8px solid #fff;
    border-top: 5.5px solid transparent;
    border-bottom: 5.5px solid transparent;
  }
  #share-modal .share-world-label {
    flex: 0 1 auto; min-width: 0;
    font-size: 15px; font-weight: 800; letter-spacing: 0.1px;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    text-align: left;
  }
  @media (max-width: 400px) {
    #share-modal .share-world { gap: 7px; padding: 6px 9px 6px 6px; }
    #share-modal .share-world-art { width: 40px; height: 40px; }
    #share-modal .share-world-label { font-size: 14px; }
    #share-modal .share-world-go { width: 24px; height: 24px; }
  }
  /* The longest world makes the longest string — "Merge Halloween Squishies" —
     and it ellipsised on a 360px screen. Everything gives up a little so the
     world's name, the one word this button exists to say, always survives. */
  @media (max-width: 375px) {
    #share-modal .share-world { gap: 6px; padding: 5px 8px 5px 5px; }
    #share-modal .share-world-art { width: 36px; height: 36px; }
    #share-modal .share-world-label { font-size: 13px; letter-spacing: 0; }
    #share-modal .share-world-go { width: 22px; height: 22px; }
    #share-modal .share-world-go::after { border-left-width: 7px; border-top-width: 4.5px; border-bottom-width: 4.5px; }
  }
  #share-modal .share-world-label b { font-weight: 800; color: #c62a6c; }
  /* The tap spends a rewarded ad, so the row says so. Quiet, but never absent —
     the picker already prices every locked world this way and a button that hid
     the cost here would be the one place in the game that lied about it. */
  /* The cost sits ON the pill rather than in a column of its own: inline it ate
     ~35px and truncated "Play Mystic World" to an ellipsis on 360-390px phones,
     which is the one word the button exists to say. */
  /* The cost still has to be visible — the picker prices every locked world the
     same way and this must not be the one place that hides it. But a hard dark
     capsule was the single strongest "this is an advert" cue on the button, so
     it is now a quiet pastel chip that belongs to the game's palette. */
  #share-modal .share-world-ad {
    position: absolute;
    top: -4px; right: -2px;
    font-size: 7.5px; font-weight: 800; letter-spacing: 0.5px;
    color: #a2517d; padding: 2px 6px; border-radius: 999px;
    background: rgba(255, 228, 238, 0.96);
    box-shadow: 0 0 0 1.5px rgba(255,255,255,0.9);
    pointer-events: none;
  }
  /* The coin route shows the currency itself rather than a word — the same
     symbol the picker rows carry, so a player learns one mark and not two. No
     number on it: the price only means something next to the balance, and that
     pairing lives in the unlock card. */
  #share-modal .share-world-ad.is-coin {
    top: -5px; right: -4px;
    padding: 3px;
    display: grid; place-items: center;
    background: linear-gradient(160deg, #fff7dc 0%, #ffe6a8 100%);
    box-shadow: 0 0 0 1.5px rgba(255,255,255,0.95), 0 2px 5px rgba(150,110,20,0.26);
  }
  #share-modal .share-world-ad.is-coin img { width: 15px; height: 15px; display: block; }
  #share-modal .share-world:active { transform: scale(0.98); }
  @media (prefers-reduced-motion: reduce) {
    #share-modal .share-world { animation: shareItemIn 0.45s ease 0.6s forwards; }
  }

  #share-modal .share-cta {
    width: 54%;
    max-width: 196px;
    padding: 11px 24px;
    font-size: 14px; font-weight: 800;
    letter-spacing: 0.3px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    color: #fff;
    box-shadow:
      0 12px 26px rgba(255, 127, 156, 0.48),
      0 2px 6px rgba(255, 127, 156, 0.25);
    opacity: 0;
    /* Pulse removed and moved to the world button. Two pulsing things on one
       screen cancel each other out, and this screen's job has shifted: sharing
       is what a player does after a good run, unlocking a world is what brings
       them back tomorrow. */
    animation: shareItemIn 0.45s ease 0.5s forwards;
  }
  #share-modal .share-cta svg {
    stroke: #fff;
    margin-top: -1px;
  }
  @keyframes shareCtaPulse {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.04); }
  }
  #share-modal .btn-ghost {
    margin-top: 10px;
    opacity: 0;
    animation: shareGhostIn 0.4s ease 0.65s forwards;
  }
  @keyframes shareGhostIn {
    from { opacity: 0; }
    to   { opacity: 0.55; }
  }

  /* Info ("i") on home + Settings (cog) in gameplay. Pinned to the
     viewport's top-right corner via position: fixed — outside the wrap
     entirely, always at the absolute screen corner. */
  .floating-icon-btn {
    position: fixed;
    top: 12px;
    right: 12px;
    width: 22px; height: 22px;
    border-radius: 50%;
    /* Level 6: almost invisible until looked for. It was a white ring at 85%
       over a 38% white fill, which on the pale worlds was brighter than the HUD
       card beside it. */
    border: 1px solid rgba(255, 255, 255, 0.55);
    background: rgba(255, 255, 255, 0.26);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    color: #ffffff;
    opacity: 0.72;
    display: flex; align-items: center; justify-content: center;
    cursor: pointer;
    box-shadow: 0 1px 3px rgba(40, 70, 110, 0.10);
    transition: transform 0.15s ease, background 0.2s ease;
    padding: 0;
    z-index: 100;
  }
  /* Only the GEAR moves. .floating-icon-btn is shared with #info-btn, which
     lives on the home screen — moving the class sent both to the same corner. */
  /* Only the gear grows. .floating-icon-btn is shared with #info-btn on the
     home screen, and 22px was under half the 44px minimum touch target — on the
     game screen, where it sits alone in the corner over live gameplay, that is
     a button you have to aim at. */
  #settings-btn {
    left: 10px; right: auto; top: 10px;
    width: 38px; height: 38px;
    opacity: 0.82;
    border-color: rgba(255, 255, 255, 0.62);
    background: rgba(255, 255, 255, 0.30);
    box-shadow: 0 2px 6px rgba(40, 70, 110, 0.13);
  }
  #settings-btn svg { width: 18px; height: 18px; }
  /* Reach beyond the drawn circle so the thumb does not have to be precise. */
  #settings-btn::after {
    content: ''; position: absolute;
    top: 50%; left: 50%; transform: translate(-50%, -50%);
    width: 46px; height: 46px;
  }
  .floating-icon-btn svg { width: 11px; height: 11px; fill: #ffffff; }
  .floating-icon-btn:hover { opacity: 1; }
  .floating-icon-btn:hover {
    transform: scale(1.15);
    background: rgba(255, 255, 255, 0.65);
  }
  .floating-icon-btn:active { transform: scale(0.92); }

  /* Info modal — single short attribution line + close. Same overlay
     glass language as the rest. */
  #info-modal .info-icon { font-size: 44px; line-height: 1; margin-bottom: 6px; filter: drop-shadow(0 4px 10px rgba(60, 140, 200, 0.3)); }
  #info-modal .info-title {
    font-size: 24px; font-weight: 800; margin-bottom: 14px; letter-spacing: -0.4px;
    background: linear-gradient(95deg, var(--accent), var(--accent-2));
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }
  #info-modal .info-text {
    font-family: 'Fredoka', -apple-system, "SF Pro Display", system-ui, sans-serif;
    font-size: 13px;
    line-height: 1.55;
    color: var(--ink-soft);
    max-width: 270px;
    text-align: center;
    margin-bottom: 20px;
  }
  #info-modal .info-text strong { color: var(--ink); font-weight: 700; }

  /* Settings modal — same overlay glass language as shop/ad. */
  #settings-modal .settings-icon { font-size: 44px; line-height: 1; margin-bottom: 6px; filter: drop-shadow(0 4px 10px rgba(120, 80, 200, 0.3)); }
  #settings-modal .settings-title {
    font-size: 24px; font-weight: 800; margin-bottom: 18px; letter-spacing: -0.4px;
    background: linear-gradient(95deg, var(--accent), var(--accent-2));
    -webkit-background-clip: text; background-clip: text; color: transparent;
  }
  .settings-rows {
    width: 100%; max-width: 280px;
    display: flex; flex-direction: column; gap: 10px;
    margin-bottom: 18px;
  }
  .settings-row {
    display: flex; align-items: center; justify-content: space-between;
    gap: 10px;
    background: rgba(255,255,255,0.78);
    border: 1px solid var(--glass-border);
    border-radius: 16px;
    padding: 10px 14px;
    box-shadow: var(--shadow-soft);
  }
  .settings-row-label {
    font-family: 'Fredoka', -apple-system, "SF Pro Display", system-ui, sans-serif;
    font-size: 15px; font-weight: 600;
    color: var(--ink);
    display: flex; align-items: center; gap: 8px;
  }
  .settings-row-label .icon { font-size: 18px; line-height: 1; }
  .settings-toggle {
    background: linear-gradient(135deg, #6fc850 0%, #4ea436 100%);
    color: white;
    border: none;
    padding: 6px 16px;
    border-radius: 100px;
    font-weight: 800; font-size: 11px;
    letter-spacing: 0.7px;
    cursor: pointer;
    min-width: 58px;
    transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease;
    box-shadow: 0 3px 8px rgba(80, 180, 60, 0.32), inset 0 1px 0 rgba(255,255,255,0.4);
    text-transform: uppercase;
  }
  .settings-toggle:active { transform: scale(0.95); }
  .settings-toggle.off {
    background: rgba(120, 80, 140, 0.18);
    color: var(--ink-soft);
    box-shadow: none;
  }
  /* Tiny attribution line at the bottom of the settings modal — replaces
     the separate Info modal so home + gameplay stay uncluttered. */
  .settings-attribution {
    font-family: 'Fredoka', -apple-system, "SF Pro Display", system-ui, sans-serif;
    font-size: 11px;
    color: var(--ink-soft);
    opacity: 0.75;
    margin-bottom: 14px;
    line-height: 1.4;
  }
  .settings-attribution strong { color: var(--ink); font-weight: 600; }

  /* =====================================================================
   *  HOME-SCREEN JUICE — entrance staggers + idle micro-anims.
   *  All animations use transform/opacity only (GPU-composited).
   *  Reduced-motion guard at the bottom of this block disables everything.
   * ===================================================================== */

  /* HUD panels fade + slide-down on first boot. Stagger by ~60ms across
     the four visible cells. Home-btn shares cell 1 with shop-btn and is
     display:none on the title screen, so the stagger order is
     shop → score → next → best. */
  .boot-revealing #hud > * {
    animation: hudPanelReveal 0.45s ease-out both;
  }
  .boot-revealing #hud > #shop-btn { animation-delay: 0ms; }
  .boot-revealing #hud > .score-panel { animation-delay: 60ms; }
  .boot-revealing #hud > .next-panel { animation-delay: 120ms; }
  .boot-revealing #tier-strip {
    animation: stripReveal 0.45s 600ms ease-out both;
  }
  @keyframes hudPanelReveal {
    from { opacity: 0; transform: translateY(-8px); }
    to   { opacity: 1; transform: translateY(0); }
  }
  @keyframes stripReveal {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* Title-screen children — replays every time the screen mounts (boot
     and goHome). JS adds .is-revealing to #title-screen, removes ~1s later. */
  .is-revealing .title {
    animation: titleReveal 0.45s 100ms cubic-bezier(0.25, 1.45, 0.5, 1) both;
  }
  .is-revealing #theme-picker-label {
    animation: labelReveal 0.35s 320ms ease-out both;
  }
  .is-revealing #theme-picker .theme-row:nth-child(1) { animation: rowReveal 0.32s 240ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(2) { animation: rowReveal 0.32s 280ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(3) { animation: rowReveal 0.32s 320ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(4) { animation: rowReveal 0.32s 360ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(5) { animation: rowReveal 0.32s 400ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(6) { animation: rowReveal 0.32s 440ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(7) { animation: rowReveal 0.32s 480ms ease-out both; }
  .is-revealing #theme-picker .theme-row:nth-child(8) { animation: rowReveal 0.32s 520ms ease-out both; }
  .is-revealing #start-btn {
    animation: playReveal 0.5s 580ms cubic-bezier(0.25, 1.5, 0.5, 1) both;
  }
  @keyframes titleReveal {
    0%   { opacity: 0; transform: scale(0.92); }
    100% { opacity: 1; transform: scale(1); }
  }
  @keyframes labelReveal {
    from { opacity: 0; }
    to   { opacity: 0.65; }
  }
  @keyframes rowReveal {
    from { opacity: 0; transform: translateX(-6px); }
    to   { opacity: 1; transform: translateX(0); }
  }
  @keyframes playReveal {
    0%   { opacity: 0; transform: scale(0.86); }
    100% { opacity: 1; transform: scale(1); }
  }

  /* Idle micro-anims — continuous, gentle, GPU-friendly. The entrance
     animations above use the same transform property, so they cleanly
     hand off to these once the entrance class is removed. */
  .title {
    animation: titleBob 3s ease-in-out infinite;
  }
  #start-btn {
    animation: ctaBreathe 2.4s ease-in-out infinite;
    position: relative;
    overflow: hidden;
  }
  /* Gel-sheen sweep — a soft diagonal glint slides across the CTA every few
     seconds (long pause between), so the button reads as glossy candy. Pure
     transform on a pseudo (GPU-cheap, mobile-safe); off under reduced motion. */
  #start-btn::after {
    content: '';
    position: absolute;
    top: -20%; bottom: -20%;
    left: -40%;
    width: 34%;
    background: linear-gradient(100deg, transparent 0%, rgba(255,255,255,0.55) 50%, transparent 100%);
    transform: translateX(0) skewX(-20deg);
    pointer-events: none;
    animation: ctaSheen 4.5s ease-out infinite;
  }
  @keyframes ctaSheen {
    0%   { transform: translateX(0) skewX(-20deg); }
    8%   { transform: translateX(430%) skewX(-20deg); }
    100% { transform: translateX(430%) skewX(-20deg); }
  }
  @media (prefers-reduced-motion: reduce) {
    #start-btn::after { animation: none; opacity: 0; }
  }
  /* Gentle float — visible at a glance but doesn't draw attention away
     from the theme list. */
  @keyframes titleBob {
    0%, 100% { transform: translateY(0)   scale(1); }
    50%      { transform: translateY(-3px) scale(1.02); }
  }
  @keyframes ctaBreathe {
    0%, 100% { transform: scale(1); }
    50%      { transform: scale(1.025); }
  }

  /* Stronger tap squish on theme rows — overrides the earlier subtle
     0.985 with a more tactile 0.96 for mobile press feedback. */
  .theme-row:active { transform: scale(0.96); }

  /* Every world's boss-emoji gently floats — staggered into a soft wave down
     the list (via per-row delays) so the whole picker feels alive at a glance.
     Transform-only = GPU-composited, cheap even across all rows. */
  @keyframes themeEmojiFloat {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-3.5px); }
  }
  /* Selected world pops more — bigger bob + a playful tilt to draw the eye
     to the current pick. Overrides the gentle float above. */
  @keyframes selectedEmojiBob {
    0%, 100% { transform: translateY(0) scale(1) rotate(0deg); }
    30%      { transform: translateY(-6px) scale(1.09) rotate(-6deg); }
    65%      { transform: translateY(-2px) scale(1.05) rotate(5deg); }
  }

  /* Accessibility: respect reduced-motion. Kill all home-screen anims —
     elements still appear in their final state thanks to `both` fill mode. */
  @media (prefers-reduced-motion: reduce) {
    .boot-revealing #hud > *,
    .boot-revealing #tier-strip,
    .is-revealing .title,
    .is-revealing #theme-picker-label,
    .is-revealing #theme-picker .theme-row,
    .is-revealing #start-btn,
    .title,
    #start-btn,
    .theme-row-emoji,
    .theme-row.selected .theme-row-emoji {
      animation: none !important;
    }
    .theme-row:active { transform: none; }
  }
  body.ad-freeze, body.ad-freeze * { animation-play-state: paused !important; }

  /* Tall phones have height the playfield cannot use: the jar's width is capped
     at the viewport, and its height follows from that width, so it simply
     cannot grow. Filling the leftover WITH the jar would mean pushing the world
     back to roughly its old size and undoing the whole reason it was shortened.
     So the chrome takes the room instead — the HUD, the ladder and the buttons
     all breathe, and the playfield is left exactly as tuned. Below 800px tall
     none of this applies and everything stays compact. */

  @media (min-height: 800px) {
    /* The room the strip gave up goes to the chrome, NOT to the jar.
       #stage cannot simply be made taller: the renderer maps a fixed logical
       world (W x H in 01-core-themes.js) onto the canvas by width alone, so a
       taller box just leaves empty canvas under the glass floor — which is
       exactly the stripe that showed up under the jar. A genuinely deeper jar
       means raising the world's H, and that is a gameplay change (more room to
       stack), not a layout one. */
    :root { --vert-reserved: 200px; }

    .panel { padding: 5px 14px; border-radius: 18px; }   /* HUD stays compact so
       the ladder below gets the room instead */
    #powerup-bar { min-height: 40px; }
    #tier-strip { padding: clamp(4px, 0.7vh, 7px) clamp(10px, 2.6vw, 16px); }
    .tier-img { max-width: clamp(30px, 4.9vh, 44px); }
    .tier-progress { font-size: clamp(9.5px, 1.25vh, 11.5px); }
  }
  @media (min-height: 880px) {
    :root { --vert-reserved: 214px; }
  }


  /* Short screens: tighten the ladder rather than drop the caption — the
     caption is the part that gives a reason to take another turn. Placed at the
     END of the stylesheet on purpose: a media query adds no specificity, so
     anywhere above the base rules it would simply be overridden. */
  @media (max-height: 700px) {
    #tier-strip { gap: 2px; padding: 5px clamp(9px, 2.6vw, 14px); }
    .tier-row   { padding-top: 5px; }
    .tier-rail  { height: 9px; margin-top: 0; }
    .tier-progress { font-size: 8.2px; letter-spacing: 1.1px; }
    .tier-dot { width: 5px; height: 5px; margin: -2.5px 0 0 -2.5px; }
    .tier-dot.next { width: 8px; height: 8px; margin: -4px 0 0 -4px; }
  }
  /* Below this there is genuinely no room for both. */
  @media (max-height: 600px) { .tier-progress { display: none; } }
  /* Short screens: the cloud keeps its shape but stops rising, which hands back
     the ~12px the HUD borrowed for it. */
  @media (max-height: 660px) {
    /* The lower zone compresses FIRST — gaps and insets before anything anyone
       has to read. This is what buys back the ~15px that was overflowing. */
    #wrap { gap: 5px; }
    #tier-strip { margin-top: 1px; }
    /* The extra shelf-to-controls breathing is a tall-screen luxury; here the
       thumb needs the bottom inset more than the eye needs the gap. */
    #powerup-bar { margin-top: 0; }
    body { padding-top: 4px;
           padding-bottom: max(13px, calc(env(safe-area-inset-bottom) + 7px)); }
    #hud { padding-top: 8px; }
    #hud > .score-panel { margin-top: -3px; }
    #hud > .action-panel { margin-top: 9px; }
  }

  /* ==================================================================
     HOME SCREEN REDESIGN
     ------------------------------------------------------------------
     Measured before touching anything, at 390x844 with all 33 worlds:
       - only 5 rows visible, and the 5th was half cut
       - 133px of dead space above the title
       - 53px between the completion line and the first card
       - each row 103px tall, of which the art was 90px and the rest padding
       - the most saturated pixels on the whole screen were the FREE / NEW
         badges: the least important information, shouting loudest
       - four stacked white layers per row: page, card, name chip, art tile
     The washed-out feeling is that last one. Colour was being spent on the
     wrong things and then diluted everywhere else.
     ================================================================== */

  /* ---- background: depth, not decoration ---------------------------- */
  body.home-active::before {
    content: '';
    position: fixed; inset: -10%;
    pointer-events: none; z-index: 0;
    /* Three soft colour clouds, wide and low-opacity. No particles, no stars,
       no texture — just enough variation that the page stops reading as one
       flat sheet behind the cards. */
    background:
      radial-gradient(60% 42% at 18% 12%, rgba(255,168,200,0.55) 0%, transparent 70%),
      radial-gradient(55% 38% at 88% 30%, rgba(255,214,166,0.50) 0%, transparent 72%),
      radial-gradient(70% 45% at 50% 92%, rgba(178,196,255,0.48) 0%, transparent 74%);
    filter: blur(6px);
  }
  body.home-active #wrap { position: relative; }
  body.home-active #wrap, body.home-active #title-screen { z-index: 1; }

  /* ---- header: tighter, and the completion state actually lands ------ */
  body.home-active .title {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(27px, 7.4vw, 33px); font-weight: 600;
    letter-spacing: -0.8px;
    /* The old ramp ended on #6bafff, and a pink-to-blue sweep is the single
       most generic treatment a mobile game title can have. Warm to rose, in
       the game's own family, with one soft lift underneath for depth. */
    background: linear-gradient(96deg, #ff8f6b 0%, #ff6b9d 55%, #d178cf 100%);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    filter: drop-shadow(0 2px 4px rgba(180, 90, 140, 0.18));
    margin-bottom: 6px;
  }
  body.home-active .theme-picker-label {
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 10.5px; letter-spacing: 1.6px; opacity: 0.55; margin-bottom: 6px;
  }
  body.home-active .theme-picker-label .progress-count {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 16px; font-weight: 600; opacity: 1;
    color: #d8608f; letter-spacing: 0.2px;
    vertical-align: -1px;
  }
  body.home-active .worlds-progress { margin: 5px auto 7px; height: 7px; width: 58%; max-width: 200px; }
  /* Completion is a state worth marking, and a bar that is simply full does not
     mark it. One slow sheen crossing a filled bar does — restrained enough to
     miss, which is the point. */
  @keyframes worldsSheen {
    0%, 55%  { background-position: 180% 0; }
    85%, 100%{ background-position: -80% 0; }
  }
  #worlds-next {
    font-family: 'Plus Jakarta Sans', -apple-system, sans-serif;
    font-size: 13px; font-weight: 700; color: #7d6a86;
  }
  /* 53px of nothing sat between the completion line and the first card. */
  body.home-active .picker-wrap { margin-top: 2px; }
  body.home-active #stage { margin-top: 0; }
  body.home-active #title-screen {
    /* 14px here plus the HUD's own trailing space left 33px of nothing between
       the header row and the wordmark, so the top of the screen read as two
       unrelated blocks instead of one masthead. */
    padding: 2px 18px 10px;
    /* .overlay lays a 55% white wash over everything so gameplay reads through
       it as a blur. On the home screen there is no jar behind it — the wash was
       only draining the colour out of the page and is most of why the screen
       read as "white glass UI over a pastel background". Thinned so the ambient
       colour underneath actually arrives. The home backdrop is static, so this
       does not vary per world and the text contrast below is fixed. */
    background: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* ---- rows: one surface per row, not four ------------------------- */
  .theme-row {
    gap: 12px;
    padding: 9px 12px;
    min-height: 88px;
    border-radius: 22px;
  }
  /* The name was sitting on its own frosted pill inside the card — a white
     shape on a white shape, which is most of why the screen read as pale. The
     word can simply BE the word. */
  /* The name gets a surface of its own again, tinted by the world it names.
     Bare ink on the card's soft radial had nothing to sit on, so thirty-three
     names read as thirty-three identical grey words and the only thing telling
     the rows apart was the picture. The chip takes its fill AND its ink from
     the same world accent the artwork already carries, so each label belongs to
     its world without any of them having to shout.
     Tint, never the raw accent: a saturated fill repeated down a column of 33
     is the loudest thing on the screen carrying the least urgent information. */
  .theme-row-name {
    background: linear-gradient(165deg, rgba(255,255,255,0.92), rgba(255,255,255,0.74));
    box-shadow: inset 0 1px 0 rgba(255,255,255,0.9), 0 1px 2px rgba(80,50,100,0.07);
    padding: 3px 11px 4px; border-radius: 12px;
    font-size: clamp(19px, 5.4vw, 22px); letter-spacing: -0.4px;
    color: #4c3a5c;
  }
  @supports (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-name {
      background: linear-gradient(165deg,
        color-mix(in srgb, var(--chip-c1) 30%, #fff) 0%,
        color-mix(in srgb, var(--chip-c2) 38%, #fff) 100%);
      /* A ring so a pale world's chip still has an edge — several accents are
         light enough that a 30% tint lands within a shade of the card itself. */
      box-shadow:
        inset 0 1px 0 rgba(255,255,255,0.85),
        0 0 0 1px color-mix(in srgb, var(--chip-c1) 26%, #fff),
        0 1px 3px rgba(80,50,100,0.09);
      color: color-mix(in srgb, var(--chip-c1) 42%, #3b2a4c);
    }
  }
  /* The chip's padding is new width the name did not have to pay for before,
     and on a 360px screen "Halloween" — the longest world in the catalogue —
     landed 2px over. The padding gives way, never the word: the name is the one
     thing this row exists to say. */
  @media (max-width: 380px) {
    .theme-row-name { padding: 3px 8px 4px; letter-spacing: -0.5px; }
  }
  .theme-row-body { gap: 3px; }
  .theme-row-goal { font-size: 13.5px; padding-left: 1px; color: rgba(48,32,62,0.62); }
  .theme-row-meta.is-progress { padding-left: 1px; padding-top: 3px; }
  .theme-row-bar { height: 5px; max-width: 118px; }
  .theme-row-art { width: 78px; height: 78px; }

  /* ---- badges: status, not focal points ---------------------------- */
  /* These were six solid saturated fills — the loudest colour on the screen,
     carrying the least important line of information on it. Same six hues,
     now as tinted labels: the hue still identifies the kind, the weight no
     longer competes with the play button. */
  .theme-row-tag {
    top: 10px; right: 12px;
    font-size: 8.5px; letter-spacing: 0.6px;
    padding: 4px 9px; border-radius: 99px;
    /* Its own glass surface. The card's ground is a soft radial with no edge,
       so a flat chip read as floating in mid-air above the row. */
    background: linear-gradient(180deg, rgba(255,255,255,0.88), rgba(255,255,255,0.70));
    border: 1px solid rgba(255,255,255,0.85);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
    box-shadow: 0 2px 6px rgba(80,50,100,0.12);
  }
  .tag-done     { background: rgba(111,187,155,0.20); color: #3f8a6c; }
  .tag-most     { background: rgba(236,143,174,0.22); color: #c25e85; }
  .tag-new      { background: rgba(108,196,192,0.22); color: #3d9490; }
  .tag-popular  { background: rgba(121,168,216,0.22); color: #4d7cad; }
  .tag-premium  { background: rgba(166,145,207,0.22); color: #7a63ac; }
  .tag-seasonal { background: rgba(223,169,106,0.24); color: #b1793a; }
  .tag-free     { background: rgba(143,196,122,0.22); color: #55913c; }
  .tag-ready    { background: rgba(240,185,60,0.26); color: #96660f; }

  /* ---- play: the one control, made tactile ------------------------- */
  .theme-row-play {
    width: 50px; height: 50px;
    background:
      radial-gradient(120% 100% at 32% 20%, rgba(255,255,255,0.55) 0%, transparent 55%),
      linear-gradient(150deg, #ff8fb4 0%, #ff9d86 100%);
    box-shadow:
      0 7px 15px rgba(255,120,160,0.30),
      0 2px 4px rgba(90,40,90,0.14),
      inset 0 2px 0 rgba(255,255,255,0.70),
      inset 0 -3px 7px rgba(190,80,120,0.16);
    transition: transform 0.18s cubic-bezier(0.34,1.6,0.64,1), box-shadow 0.18s ease;
  }
  .theme-row:active .theme-row-play {
    transform: scale(0.90);
    box-shadow: 0 2px 6px rgba(255,120,160,0.26), inset 0 2px 6px rgba(160,60,100,0.22);
  }
  .play-tri { border-left-width: 14px; border-top-width: 9.5px; border-bottom-width: 9.5px; }

  /* ---- the direction that won, promoted ----------------------------
     Three were built and rendered at 390x844 side by side. A (soft premium)
     stayed too pale — the cards read as one white slab repeated 33 times.
     C (toy shelf) had lovely material but its asymmetric corners and under-glow
     do not survive at 88px row height. B wins because it is the only one where
     you can tell the worlds apart at a glance, and it does that by CONCENTRATING
     the colour on the artwork plate rather than smearing a tint across the whole
     card — which is the brief's own rule: vibrancy from world art, not from a
     saturated UI. C's pillow highlight and coloured under-glow were worth
     keeping and are folded in below; they add depth without adding colour area.
     -------------------------------------------------------------------- */
  .theme-row {
    background:
      linear-gradient(to bottom, rgba(255,255,255,0.55) 0%, rgba(255,255,255,0.08) 30%, transparent 52%),
      linear-gradient(96deg, var(--card-wash-1, transparent) 0%, transparent 58%),
      rgba(255,255,255,0.86);
    padding-left: 9px;
    box-shadow:
      0 14px 26px var(--card-glow, rgba(88,48,112,0.16)),
      0 6px 12px rgba(88,48,112,0.08),
      0 2px 3px rgba(88,48,112,0.05),
      inset 0 2px 2px rgba(255,255,255,0.98),
      inset 0 -2px 4px rgba(150,110,170,0.07);
  }

  /* The artwork plate. Deliberately a FRAME, not a panel: the preview images
     already carry their own light rounded ground, so a padded plate around one
     produced a coloured box holding a white box holding the art — the same
     nested-surface problem the name chip had. 3px of colour reads as an edge
     the art sits in. border-box because the img is sized at 100%: with the
     default content-box the padding pushed the artwork out of its own frame,
     which is what was clipping the bottom row of emoji. */
  .theme-row-art {
    box-sizing: border-box;
    /* One value, two consumers: the art box and the row height that has to
       contain it. They drifted apart once already and the art spilled over the
       row below, so neither is allowed its own number any more. */
    width: var(--row-art); height: var(--row-art);
    border-radius: 19px;
    padding: 3px;
    background:
      radial-gradient(85% 70% at 50% 16%, rgba(255,255,255,0.55) 0%, transparent 60%),
      linear-gradient(155deg, var(--chip-c1) 0%, var(--chip-c2) 100%);
  }
  @supports (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-art {
      background:
        radial-gradient(85% 70% at 50% 16%, rgba(255,255,255,0.50) 0%, transparent 60%),
        linear-gradient(155deg,
          color-mix(in srgb, var(--chip-c1) 52%, #fff) 0%,
          color-mix(in srgb, var(--chip-c2) 52%, #fff) 100%);
    }
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.50),
      0 4px 10px var(--card-glow, rgba(120,80,140,0.22));
  }
  .theme-row-art img {
    border-radius: 16px;
    filter: saturate(1.14) contrast(1.06) drop-shadow(0 2px 5px rgba(40,25,60,0.22));
  }
  /* Worlds whose preview never rendered fall back to a bare boss emoji, which
     has no ground of its own — it needs the padding the framed version does not. */
  .theme-row-art.is-fallback { padding: 12px; }

  /* A card that answers the finger. The row was already tappable but only the
     disc moved, so the object you pressed and the object that responded were
     different things. */
  .theme-row:active {
    transform: scale(0.975);
    box-shadow:
      0 6px 14px var(--card-glow, rgba(88,48,112,0.14)),
      0 2px 4px rgba(88,48,112,0.07),
      inset 0 2px 2px rgba(255,255,255,0.98);
  }


  /* ---- SHOP: part of the same game ---------------------------------
     It was a full-size white card with a diagonal red SALE band across the
     corner — the loudest, hardest-edged object on a screen made of soft pastel
     pills, and the only one shouting. It becomes the same glass pill the
     in-game WORLDS control uses, at a size that says "utility, not the point",
     and the sale becomes a small coral dot instead of a banner. */
  body.home-active #shop-btn {
    min-width: 0;
    flex-direction: row;
    gap: 7px;
    padding: 8px 14px 8px 11px;
    border-radius: 16px;
    background: linear-gradient(180deg, rgba(255,255,255,0.62), rgba(255,255,255,0.42));
    border: 1px solid rgba(255,255,255,0.72);
    backdrop-filter: blur(9px); -webkit-backdrop-filter: blur(9px);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.85),
      0 1px 2px rgba(80, 55, 110, 0.07),
      0 5px 13px rgba(80, 55, 110, 0.11);
    overflow: visible;
  }
  body.home-active #shop-btn .action-icon { width: 24px; height: 24px; order: 0; }
  body.home-active #shop-btn .label {
    order: 1; color: #7a5a78; opacity: 0.95;
    font-size: 11px; letter-spacing: 0.9px;
    text-shadow: none;
  }
  /* The 42px diagonal band becomes a dot. It still says "there is something on
     offer" — which is all a badge has to do — without cutting a hard red edge
     across a screen with no other hard edges on it. */
  body.home-active #shop-btn .corner-ribbon {
    top: -4px; left: auto; right: -4px;
    width: 12px; height: 12px;
    border-radius: 50%;
    overflow: visible;
    background: linear-gradient(180deg, #ff8f7a, #f2695f);
    box-shadow: 0 1px 3px rgba(190, 70, 60, 0.45), inset 0 1px 0 rgba(255,255,255,0.5);
    animation: shopDot 4.4s ease-in-out infinite;
  }
  body.home-active #shop-btn .corner-ribbon i { display: none; }
  @keyframes shopDot {
    0%, 78%, 100% { transform: scale(1); }
    88%           { transform: scale(1.25); }
  }
  @media (prefers-reduced-motion: reduce) {
    body.home-active #shop-btn .corner-ribbon { animation: none; }
  }

  /* Worlds were separating weakly once the plate was lifted 52% toward white —
     Garden and Kitty came out nearly the same pale grey-pink. 40% keeps them in
     the pastel register while giving the hue back enough presence to identify. */
  @supports (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-art {
      background:
        radial-gradient(85% 70% at 50% 16%, rgba(255,255,255,0.48) 0%, transparent 60%),
        linear-gradient(155deg,
          color-mix(in srgb, var(--chip-c1) 62%, #fff) 0%,
          color-mix(in srgb, var(--chip-c2) 62%, #fff) 100%);
    }
  }
  /* 13px between cards on a list of 33 spends a whole row every four. */
  body.home-active #theme-picker { gap: 10px; }

  /* The world's picture is the only thing on this screen that says what a world
     actually IS — the name is a word and the goal line is a promise, but the
     art is the product. It gets the space.
     Declared on :root, not on body.home-active: a custom property that only
     exists under one class resolves to nothing everywhere else, and an invalid
     width on the art box is a collapsed row rather than a smaller one. */
  :root { --row-art: clamp(92px, 27vw, 112px); }


  /* ==================================================================
     WORLD GALLERY — composition pass
     ------------------------------------------------------------------
     The previous pass fixed the colour but left the shape: every world was a
     white rounded rectangle holding a coloured plate holding a white-grounded
     preview holding the art — three nested surfaces to show one picture — plus
     a badge, a bar and a 50px disc. Thirty-three of those in a column is a
     settings list, whatever colour it is.
     The correction is to stop drawing containers. Separation between worlds
     comes from spacing; emphasis comes from scale and weight. Below: the
     surface work is stripped back to one frame around the artwork, and three
     compositions are laid over that to be compared.
     ================================================================== */

  /* ---- shared: one frame, not three ------------------------------- */
  /* The preview image already carries its own light rounded ground — it IS a
     tile. So the coloured plate stops being a panel behind it and becomes a
     glow around it: the world's colour with no extra edge of its own. */
  .theme-row-art {
    padding: 0;
    background: none;
    box-shadow: none;
  }
  @supports (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-art { background: none; }
  }
  .theme-row-art img {
    border-radius: 18px;
    box-shadow:
      0 0 0 2px color-mix(in srgb, var(--chip-c1) 40%, #fff),
      0 6px 14px var(--card-glow, rgba(120,80,140,0.28)),
      0 2px 4px rgba(60,40,80,0.10);
    filter: saturate(1.16) contrast(1.07);
  }
  @supports not (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-art img { box-shadow: 0 6px 14px var(--card-glow, rgba(120,80,140,0.28)); }
  }

  /* ---- shared: the play control loses its mass -------------------- */
  /* 50px of saturated pink repeated 33 times was the heaviest thing in the
     column and it out-weighed the artwork it was meant to serve. 42px, and the
     depth comes from an inner highlight rather than a large outer glow. */
  .theme-row-play {
    width: 42px; height: 42px;
    background:
      radial-gradient(120% 100% at 34% 18%, rgba(255,255,255,0.60) 0%, transparent 58%),
      linear-gradient(155deg, #ff90b2 0%, #ff9f87 100%);
    box-shadow:
      0 4px 10px rgba(255,120,160,0.26),
      0 1px 2px rgba(90,40,90,0.12),
      inset 0 1.5px 0 rgba(255,255,255,0.72),
      inset 0 -2px 5px rgba(190,80,120,0.16);
  }
  .play-tri {
    margin-left: 3px;
    border-left-width: 12px; border-top-width: 8px; border-bottom-width: 8px;
  }
  .theme-row:active .theme-row-play { transform: scale(0.88); }

  /* ---- shared: header, tightened --------------------------------- */
  body.home-active .worlds-progress { margin: 4px auto 5px; }
  /* "6% collected · next Fastfood" repeated what "WORLDS · 2/33" and the bar
     directly above it already said, and it was the most prominent line in the
     header on a screen whose job is to get the player to the worlds. */
  body.home-active #worlds-next { display: none; }
  body.home-active .picker-wrap { margin-top: 6px; }
  /* The peek fade was thick enough to grey out the name of the row underneath.
     4px only softens the cut. */
  #theme-picker {
    mask-image: linear-gradient(to bottom, transparent 0, #000 8px, #000 calc(100% - 4px), transparent 100%);
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 8px, #000 calc(100% - 4px), transparent 100%);
  }

  /* ---- the composition that won -----------------------------------
     Three were built and compared at 390x844. A (no container at all, hairline
     separators) is the cleanest but the entries float — nothing holds a world
     together as one object. B (soft cushion) is still a card, just a quieter
     one. C wins: it gives each world an ATMOSPHERE instead of a container. The
     world's own colour pools around its artwork and is gone before it reaches
     the text, so a row is identified by the light it sits in rather than by a
     box drawn around it — which is the brief's "atmospheric accent", and it is
     the only one of the three that is both container-less and vibrant.
     -------------------------------------------------------------------- */
  .theme-row {
    border: 0;
    border-radius: 24px;
    box-shadow: none;
    padding: 10px 10px;
    min-height: calc(var(--row-art) + 20px);
    /* Anchored on the artwork and radial, so it has no edge anywhere — the
       first attempt was a linear gradient starting at full strength on the
       row's left, which drew exactly the vertical edge a container would. */
    overflow: visible;
    background: radial-gradient(62% 150% at 13% 50%,
      color-mix(in srgb, var(--chip-c1) 30%, transparent) 0%,
      color-mix(in srgb, var(--chip-c1) 10%, transparent) 40%,
      transparent 68%);
  }
  @supports not (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row {
      background: radial-gradient(58% 120% at 12% 50%,
        var(--card-wash-1) 0%, transparent 74%);
    }
  }
  /* The lift belongs to the artwork, not to a card. */
  .theme-row:active { transform: scale(0.978); box-shadow: none; }
  body.home-active #theme-picker { gap: 6px; }

  /* One frame. The 2px colour ring plus the preview's own light ground was two
     frames around one picture — the nesting the whole pass exists to remove.
     The world's colour is already in the glow behind it. */
  .theme-row-art img {
    /* The preview art already has its own rounded panel baked in, with
       transparent corners. A box-shadow is drawn on the element BOX, so it
       traced a second rectangle a few pixels outside that panel — the double
       frame. drop-shadow follows the alpha instead, so there is one shape. */
    box-shadow: none;
    filter:
      saturate(1.2) contrast(1.08)
      drop-shadow(0 8px 16px var(--card-glow, rgba(120,80,140,0.40)))
      drop-shadow(0 2px 4px rgba(60,40,80,0.20));
  }

  /* ==================================================================
     CALM-DOWN PASS — three things that had each grown on their own and
     stopped agreeing with one another.
     ================================================================== */

  /* ── 1. THEMES tile: same information, less furniture ──────────────
     A navigation control had ended up with more presence than the SCORE
     it sits beside: 9px of padding, a 25px globe and a 17px figure.
     Everything shrinks EXCEPT grid-template-rows, which is shared with
     .next-panel and is the only reason the two captions sit on one line
     — they have come apart once already over exactly this. */
  #home-btn { padding: 4px 8px 1px; border-radius: 12px; }
  #home-btn .action-icon { width: 18px; height: 18px; margin-right: 2px; }
  #home-btn .hud-badge { padding: 2px 8px 3px; font-size: 13.5px; }
  #home-btn .hud-badge::after { margin-left: 4px; font-size: 0.92em; }
  /* Both captions or neither: THEMES and NEXT are meant to read as one line
     across the top, so shrinking one of them is how the pair comes apart. */
  #home-btn .label, .next-panel .label { font-size: 9px; letter-spacing: 0.8px; }

  /* ── 2a. The ladder said everything twice ──────────────────────────
     Eleven dots on the rail, directly under eleven emoji, carrying the
     same eleven facts — the emoji are already lit, dimmed or enlarged.
     Only the one being worked toward survives, because "you are here" is
     the single thing the row of emoji does not say on its own.
     Hidden with opacity, not display: layoutTierDots() measures these to
     place the rail, and a display:none dot measures zero. */
  #tier-strip .tier-dot { opacity: 0; }
  #tier-strip .tier-dot.next,
  #tier-strip .tier-dot.is-goal-tier { opacity: 1; }

  /* ── 2b. One dock, not four loose stickers ─────────────────────────
     Four buttons in four hues at four different lightnesses read as four
     things from four different apps, floating on the world with nothing
     holding them together. A single tray gives them one edge and one
     ground, so the row becomes an instrument panel — and the tray is the
     HUD's glass, so the bottom of the screen now speaks the same language
     as the top. */
  /* The tray is gone. It existed to hold four mismatched pills together; the
     tiles are one set on their own, and a box around them was a second frame
     around a frame. */
  #powerup-bar {
    box-sizing: border-box;
    padding: 0;
    background: none;
    border: 0;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    box-shadow: none;
    min-height: 0;
  }
  /* Same four hues, but the chroma is cut 30% and every fill is solved to
     land white text at 4.20:1 — one number, all four. The spread is what
     made them clash: they used to run 3.34 (Fill) to 4.69 (Twin), which is
     four different lightnesses as much as four different colours. Holding
     the contrast fixed holds the lightness fixed, and four hues at one
     lightness read as a set. Legibility improves at the same time.
     Recomputed, not eyeballed — see the ratios above. */
  .powerup-btn            { --pw-top: #968dcc; --pw-bot: #6054b3; --pw-solid: #7c72c0; --pw-shade: rgba(65, 55, 135, 0.26); }
  .powerup-btn.shuffle    { --pw-top: #c57968; --pw-bot: #a55340; --pw-solid: #bb634f; --pw-shade: rgba(123, 57, 42, 0.26); }
  .powerup-btn.fill       { --pw-top: #4d998a; --pw-bot: #3a7469; --pw-solid: #44877a; --pw-shade: rgba(40, 86, 77, 0.26); }
  .powerup-btn.twin       { --pw-top: #be78a6; --pw-bot: #9f4d83; --pw-solid: #b26096; --pw-shade: rgba(117, 52, 95, 0.26); }

  /* ── The power-ups are icons on glass ────────────────────────────────────
     Four solid fills read as four muddy chips, and they read differently on
     every one of 33 washes — the terracotta one in particular. Frosted glass
     belongs to all of them, because it takes its colour from the world behind
     it: it is the jar's own material, so the bottom of the screen now speaks
     the same language as the thing being played with.

     The hue moves to the ICON. --pw-ink is solved to 4.8:1 against the frost;
     the old --pw-solid measured 3.5:1 on it. The badge takes the same ink with
     white numerals, which colour-codes each power-up without another fill.

     The tile is the picture, the badge is the count, and the word underneath is
     there for the first time you see it. */
  .powerup-btn            { --pw-ink: #6d44e2; }
  .powerup-btn.shuffle    { --pw-ink: #b04028; }
  .powerup-btn.fill       { --pw-ink: #2b7164; }
  .powerup-btn.twin       { --pw-ink: #af338b; }

  #powerup-bar {
    gap: clamp(12px, 5vw, 26px);
    align-items: flex-start;
  }
  .powerup-btn {
    background: none;
    border: 0;
    box-shadow: none;
    padding: 0;
    height: auto;
    min-width: 0;
    flex: 0 0 auto;
    letter-spacing: normal;
    display: flex; flex-direction: column; align-items: center;
    gap: 4px;
    position: relative;
    overflow: visible;
  }
  .powerup-btn .powerup-icon {
    width: clamp(44px, 12.4vw, 50px); height: clamp(44px, 12.4vw, 50px);
    margin: 0;
    border-radius: 30%;
    display: grid; place-items: center;
    /* LONGHAND, never the `background` shorthand. Twin paints the piece you
       would be handed as an inline background-image, and a shorthand here wipes
       it and leaves an empty tile — that happened twice while designing this. */
    background-color: rgba(255, 255, 255, 0.30);
    border: 1.5px solid rgba(255, 255, 255, 0.80);
    backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
    box-shadow:
      inset 0 2px 0 rgba(255, 255, 255, 0.96),
      inset 0 7px 12px rgba(255, 255, 255, 0.42),
      inset 0 -3px 8px rgba(255, 255, 255, 0.22),
      0 6px 14px rgba(40, 25, 60, 0.15),
      0 1px 2px rgba(40, 25, 60, 0.10);
    color: var(--pw-ink);
  }
  .powerup-btn:not(.twin) .powerup-icon {
    background-image: linear-gradient(180deg, rgba(255,255,255,0.46), rgba(255,255,255,0.10));
  }
  .powerup-btn .powerup-icon svg { width: 25px; height: 25px; stroke-width: 2; }
  /* On glass the piece sits in it like a small jar of its own. */
  .powerup-btn.twin .powerup-icon {
    background-color: rgba(255, 255, 255, 0.44);
    background-size: 62% 62%;
    background-repeat: no-repeat;
    background-position: center;
    border-radius: 30%;
    font-size: 24px; line-height: 1;
  }
  .powerup-btn .powerup-label {
    display: block;
    margin: 0;
    font-size: clamp(8.5px, 1.05vh, 10px);
    font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase;
    color: #fff;
    text-shadow:
      0 1px 0 rgba(54, 36, 74, 0.50), 0 -1px 0 rgba(54, 36, 74, 0.30),
      1px 0 0 rgba(54, 36, 74, 0.30), -1px 0 0 rgba(54, 36, 74, 0.30),
      0 2px 6px rgba(54, 36, 74, 0.42);
  }
  .powerup-btn .powerup-count {
    position: absolute; top: -6px; right: -6px;
    margin: 0;
    min-width: 21px; height: 21px;
    border-radius: 99px;
    padding: 0 5px;
    background: var(--pw-ink);
    color: #fff;
    font-size: 11.5px; font-weight: 800;
    display: grid; place-items: center;
    box-shadow: 0 1.5px 4px rgba(40, 25, 60, 0.28), inset 0 0 0 1.5px rgba(255, 255, 255, 0.92);
  }
  .powerup-btn:disabled { opacity: 1; }
  .powerup-btn:disabled .powerup-icon { opacity: 0.58; filter: saturate(0.30); }
  .powerup-btn:disabled .powerup-count { background: #a99fbb; color: #fff; }
  .powerup-btn:disabled .powerup-label { opacity: 0.66; }
  .powerup-btn:active .powerup-icon { transform: translateY(1px) scale(0.96); }
  /* Inside a tray the buttons no longer need to lift themselves off the
     world — the tray does that — so the big ambient glow under each one
     comes down. Four glows inside one box was the other half of the mess. */
  .powerup-btn {
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.46),
      inset 0 -1.5px 0 rgba(30, 20, 60, 0.10),
      0 2px 6px var(--pw-shade);
  }

  /* ── 3. Badges that sit ON the card instead of dissolving into it ──
     Every tag was a 20-26% wash of its hue, so what you actually saw was
     the row's own radial ground showing through a tint — and that ground
     is a different colour on all 33 worlds. The same badge therefore had
     33 appearances, none of them the one it was designed as.
     Opaque now: one lightness across all eight, a hairline edge of the
     same hue, and ink solved to 4.6:1 on its OWN fill rather than on
     whatever happens to be behind it. */
  .theme-row-tag {
    background: #fff;
    border: 0;
    backdrop-filter: none; -webkit-backdrop-filter: none;
  }
  .tag-done     { background: #deede7; box-shadow: inset 0 0 0 1px #bcdcce, 0 1px 3px rgba(80,50,100,0.13); color: #39735a; }
  .tag-most     { background: #f4d7e1; box-shadow: inset 0 0 0 1px #ebadc2, 0 1px 3px rgba(80,50,100,0.13); color: #b82455; }
  .tag-new      { background: #ddeeed; box-shadow: inset 0 0 0 1px #b9dfdd, 0 1px 3px rgba(80,50,100,0.13); color: #317370; }
  .tag-popular  { background: #dbe5f0; box-shadow: inset 0 0 0 1px #b4cce4, 0 1px 3px rgba(80,50,100,0.13); color: #32679e; }
  .tag-premium  { background: #e3deed; box-shadow: inset 0 0 0 1px #c7bbdd, 0 1px 3px rgba(80,50,100,0.13); color: #7050af; }
  .tag-seasonal { background: #f2e6d9; box-shadow: inset 0 0 0 1px #e8ceb0, 0 1px 3px rgba(80,50,100,0.13); color: #8e5c22; }
  .tag-free     { background: #e2edde; box-shadow: inset 0 0 0 1px #c5ddbb, 0 1px 3px rgba(80,50,100,0.13); color: #477336; }
  .tag-ready    { background: #f4ebd7; box-shadow: inset 0 0 0 1px #ebd7ad, 0 1px 3px rgba(80,50,100,0.13); color: #876319; }
  /* RESUME is the one badge that changes what the tap does, so it is the one
     badge allowed to be a little louder than the rest. */
  .tag-resume   { background: #dfeaf7; box-shadow: inset 0 0 0 1px #b2cbe6, 0 1px 3px rgba(80,50,100,0.13); color: #2f6493; }

  /* ── 4. The home header stops shouting its own name ────────────────
     The wordmark was 33px of gradient at the top of a screen the player
     only ever reaches from inside the game — they know what they are
     playing, and the thing they came here to read is the list. It keeps
     its identity and gives up its size.
     The line under it gains the word the number was missing: "11/33" is
     a fraction of nothing until something names it. And it gains a
     surface, because a bare uppercase line floating on the peach wash
     read as a caption for the wordmark rather than as a status of its
     own. */
  body.home-active .title {
    font-size: clamp(19px, 5.2vw, 23px);
    letter-spacing: -0.4px;
    margin-bottom: 7px;
  }
  body.home-active .theme-picker-label {
    display: inline-block;
    padding: 4px 13px 5px;
    border-radius: 999px;
    opacity: 1;
    color: rgba(92, 52, 94, 0.78);
    background: linear-gradient(180deg, rgba(255,255,255,0.80), rgba(255,255,255,0.58));
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.92),
      0 0 0 1px rgba(255,255,255,0.62),
      0 2px 6px rgba(140, 90, 130, 0.12);
    backdrop-filter: blur(7px); -webkit-backdrop-filter: blur(7px);
    margin-bottom: 7px;
  }
  body.home-active .theme-picker-label .progress-count {
    font-size: 14.5px;
    color: #d0537f;
  }

  /* ── 5. The row becomes a card again, and the goal joins its world ──
     The atmosphere version — a radial pool of the world's colour under the
     artwork, gone before it reached the text — was deliberately
     container-less, on the argument that a box around each row reads as
     furniture. It does avoid that. What it also does is leave two thirds
     of every row sitting directly on the page wash, so the right-hand half
     (name, goal, progress, play) has no surface under it at all and the
     whole list reads as faded.
     So: a real card, but one still made of its own world rather than of
     grey. The colour pools where the artwork is and thins across to the
     right — the same gradient idea — except it now lands on a glass floor
     instead of on nothing, and the card has an edge and a lift so it is an
     object you could pick up. */
  .theme-row {
    background:
      radial-gradient(64% 140% at 14% 46%,
        color-mix(in srgb, var(--chip-c1) 30%, transparent) 0%,
        color-mix(in srgb, var(--chip-c1) 11%, transparent) 42%,
        transparent 70%),
      linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0.46));
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.92),
      0 0 0 1px rgba(255,255,255,0.58),
      /* Tinted with the world's own colour, not grey: a grey shadow under a
         coloured object reads as dirt, a hue-matched one reads as light. */
      0 4px 14px var(--card-glow, rgba(120,80,140,0.16));
    backdrop-filter: blur(5px); -webkit-backdrop-filter: blur(5px);
  }
  @supports not (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row {
      background:
        radial-gradient(60% 130% at 13% 46%, var(--card-wash-1) 0%, transparent 72%),
        linear-gradient(180deg, rgba(255,255,255,0.72), rgba(255,255,255,0.46));
    }
  }
  /* The card answers the finger now that there is a card to press. */
  .theme-row:active {
    transform: scale(0.978);
    box-shadow:
      inset 0 2px 3px rgba(90, 55, 110, 0.10),
      0 0 0 1px rgba(255,255,255,0.5),
      0 1px 4px var(--card-glow, rgba(120,80,140,0.14));
  }
  /* The goal is the one line that differs between all 33 worlds — "Make a
     Whale", "Make a Lion" — and it was the same grey on every one of them.
     It takes the world's colour too, but a third less of it than the name:
     the name is what you pick, the goal is what you get. */
  .theme-row-goal { color: rgba(48, 32, 62, 0.62); }
  @supports (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-goal { color: color-mix(in srgb, var(--chip-c1) 30%, #4a3560); }
  }

  /* ══════════════════════════════════════════════════════════════════
     ART DIRECTION — one ground, one action colour, thirty-three accents.
     ------------------------------------------------------------------
     What was wrong was not any single colour, it was that everything was
     an accent. The page ran warm pink at the top to PERIWINKLE at the
     bottom, so the lower rows sat on a cool ground while their artwork and
     tints stayed warm — that hue collision is what read as "dingy", not
     low saturation. On top of that, thirty-three cards each contributed
     their own hue at roughly the same strength as the page itself, so
     nothing was foreground.
     The fix is the oldest one there is: quiet the ground, and spend the
     colour where it means something.
       GROUND   one warm blush-to-cream family, no cool note anywhere
       ACTION   pink for play, amber for shop — the only saturated things
       ACCENT   each world's hue, pooled under its own artwork
     ══════════════════════════════════════════════════════════════════ */

  /* The bottom cloud was periwinkle (178,196,255). It is now a pale sand,
     so the ground drifts in VALUE down the page instead of in hue, and a
     world's colour is the only cool thing that can ever appear on it. */
  body.home-active::before {
    background:
      radial-gradient(60% 42% at 18% 12%, rgba(255,176,203,0.50) 0%, transparent 70%),
      radial-gradient(55% 38% at 88% 30%, rgba(255,219,178,0.46) 0%, transparent 72%),
      radial-gradient(74% 48% at 50% 94%, rgba(255,232,214,0.62) 0%, transparent 76%);
  }
  body.home-active::after { background: #ffd9c2; opacity: 0.16; }

  /* The one control on every row, and on a blush ground the old fill was
     within a few steps of the page itself. Deeper, warmer, and the only
     saturated pink left on the screen now that the ground has stepped back. */
  .theme-row-play {
    background:
      radial-gradient(120% 100% at 34% 18%, rgba(255,255,255,0.58) 0%, transparent 58%),
      linear-gradient(155deg, #fb7fa4 0%, #fb8f6d 100%);
    box-shadow:
      0 4px 11px rgba(233, 96, 126, 0.32),
      0 1px 2px rgba(90, 40, 90, 0.14),
      inset 0 2px 0 rgba(255,255,255,0.62),
      inset 0 -3px 7px rgba(190, 80, 120, 0.18);
  }

  /* Ink comes forward with the ground going back — the names and goals were
     tuned against a busier page and now have room to be read. */
  @supports (color: color-mix(in srgb, red 50%, blue)) {
    .theme-row-name { color: color-mix(in srgb, var(--chip-c1) 40%, #33243f); }
    .theme-row-goal { color: color-mix(in srgb, var(--chip-c1) 26%, #3f2c52); }
  }

  /* ── SHOP: the second thing you can press, so it looks like one ─────
     It was white glass with a grey-violet label — the same material as the
     THEMES tile, which is navigation, and quieter than every row below it.
     Shop is money, and the only other money on this screen is the coin
     chip beside it, so it takes the coin's amber. Two warm controls at the
     top, pink ones down the list, and nothing else saturated.
     Ink solved to 5.0:1 on its own fill rather than picked. */
  /* #hud #shop-btn, not #shop-btn: `body.home-active #hud .panel` carries two
     classes and an id and was quietly winning the background off this rule. */
  body.home-active #hud #shop-btn {
    padding: 9px 16px 9px 12px;
    border-radius: 18px;
    background: linear-gradient(180deg, #fff1d8 0%, #ffdfb4 100%);
    border: 1px solid #f3ce9b;
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.95),
      0 2px 4px rgba(150, 100, 30, 0.10),
      0 7px 18px rgba(214, 150, 60, 0.26);
  }
  body.home-active #hud #shop-btn .action-icon { width: 27px; height: 27px; }
  body.home-active #hud #shop-btn .label {
    color: #8c5811;
    font-size: 11.5px; font-weight: 700; letter-spacing: 1px;
    opacity: 1;
  }
  /* ── The header was three things on three different lines ──────────
     SHOP's centre sat at y=45, the coin chip's at y=27 and the info dot's
     at y=23. Nothing was aligned to anything, which is what reads as
     "position thoda off" — the pill growing into an amber button only made
     the mismatch easier to see. All three now share one centre line, and
     each is placed from ITS OWN height so the line survives a size change.
     SHOP: top 22 + 47/2 = 45.5. */
  body.home-active .coin-chip-home { top: 28px; }       /* 45.5 - 35/2 */
  body.home-active .floating-icon-btn { top: 34px; }    /* 45.5 - 22/2 */

  /* ── Coin collection ───────────────────────────────────────────────
     One sprite per flight, positioned in px and animated with a transform
     only — no layout is touched while fourteen of these are in the air.
     They sit above the card (z 260) because they travel ACROSS it. */
  .coin-fly {
    position: fixed;
    z-index: 260;
    width: 22px; height: 22px;
    margin: -11px 0 0 -11px;       /* the px position is its centre */
    pointer-events: none;
    will-change: transform, opacity;
    filter: drop-shadow(0 2px 4px rgba(150, 105, 25, 0.45));
    animation: coinFly 460ms cubic-bezier(0.45, 0.02, 0.35, 1) both;
  }
  @keyframes coinFly {
    /* A small pop away from the jar first, so the coin reads as being SHED by
       it rather than as sliding along a wire. */
    0%   { transform: translate(0, 0) scale(0.45); opacity: 0; }
    14%  { transform: translate(calc(var(--dx) * 0.04), calc(var(--dy) * 0.04 - 16px)) scale(1.12); opacity: 1; }
    82%  { opacity: 1; }
    100% { transform: translate(var(--dx), var(--dy)) scale(0.60); opacity: 0; }
  }
  /* The figure takes the hit when the last coin lands. */
  .coin-land { animation: coinLand 380ms cubic-bezier(0.34, 1.6, 0.64, 1); }
  @keyframes coinLand {
    0%   { transform: scale(1); }
    34%  { transform: scale(1.16); }
    100% { transform: scale(1); }
  }
  #run-coins { display: inline-block; }
  @media (prefers-reduced-motion: reduce) {
    .coin-fly { display: none; }
    .coin-land { animation: none; }
  }

  body.home-active #hud #shop-btn:active {
    transform: scale(0.96);
    background: linear-gradient(180deg, #ffe7c4 0%, #f8d2a2 100%);
    box-shadow: inset 0 2px 4px rgba(150, 100, 30, 0.18), 0 1px 3px rgba(150, 100, 30, 0.12);
  }

  /* ── Short phones: the tile alone, and the gear out of the way ──────────
     An icon tile with a word under it is TALLER than the pill it replaced, and
     measured on a 412x732 that cost 16px the layout does not have. Below 760px
     the label goes: the picture and the count still say everything.

     The settings gear is the other half of it. Once the collection strip moved
     above the jar the HUD came up with it, and the gear's fixed corner did not
     follow — it sat on the THEMES panel on 8 of 10 device sizes, by as much as
     38x20px. The THEMES panel hangs from the HUD's bottom edge, so shortening it
     only buys a few pixels; the rest has to come from the HUD starting lower.
     Tall phones have 42-48px of slack to spend on that. Short ones do not, so
     they get a tighter panel and a smaller gear instead. */
  #settings-btn { left: 8px; top: 6px; width: 32px; height: 32px; }
  #settings-btn svg { width: 13px; height: 13px; }
  #hud > .action-panel { padding: 4px 9px 5px; gap: 2px; }
  #hud > .action-panel .label { font-size: 8.5px; letter-spacing: 0.06em; }

  @media (min-height: 780px) {
    #hud { padding-top: 34px; }
  }
  @media (max-height: 779px) {
    /* Short phones set the ceiling on how deep the world may be. They have no
       spare height, so any depth they cannot fit would have to come out of the
       jar's WIDTH — and a deeper jar that is smaller overall is not a bigger
       jar. 515 is the most that fits every size without that happening. */
    :root { --vert-reserved: 196px; }
    /* The gear keeps a real tap target; the HUD moves down to clear it instead
       of the gear shrinking into something a thumb cannot hit. */
    #hud { padding-top: 12px; }
    #settings-btn { width: 30px; height: 30px; top: 2px; left: 4px; }
    #settings-btn svg { width: 12px; height: 12px; }
    #hud > .action-panel { padding: 1px 5px 2px; gap: 0; }
    #hud > .action-panel .label { font-size: 6.5px; }
    #hud > .action-panel .action-icon { width: 15px; height: 15px; }
  }
  @media (max-height: 760px) {
    #toast { bottom: calc(env(safe-area-inset-bottom, 0px) + 86px); }
    #powerup-bar .powerup-label { display: none; }
    #powerup-bar .powerup-btn { gap: 0; }
    #powerup-bar .powerup-icon {
      width: clamp(40px, 11.5vw, 45px); height: clamp(40px, 11.5vw, 45px);
    }
    #powerup-bar .powerup-icon svg { width: 22px; height: 22px; }
    #powerup-bar .powerup-count { min-width: 19px; height: 19px; font-size: 10.5px; }
    #powerup-bar { gap: clamp(14px, 6vw, 28px); }
    #tier-strip { padding: clamp(3px, 0.7vh, 6px) clamp(8px, 2.6vw, 12px); }
    .tier-img { max-width: clamp(17px, 2.6vh, 22px); }
    .tier-rail { height: 7px; margin: 2px 0 0; }
    .tier-progress { margin-top: 4px; }
  }

  /* The shortest phones still in the wild (a 320x568 iPhone SE) need a few more
     pixels back than the general short-screen rule gives. Measured, not
     guessed: at 196px of reserve that size ran 4px past the bottom. */
  @media (max-height: 600px) {
    :root { --vert-reserved: 206px; }
  }

  /* Wide but short — a 412x732 tablet-ish phone. It is the one size where the
     jar stays WIDTH-limited even on a short screen, so it grows tall enough to
     leave the HUD nothing. Reserving more here lets the height term win, and the
     jar keeps its proportions at a slightly smaller size. Scoped by width so a
     360x640 (the same aspect, but narrow) is untouched. */
  @media (max-height: 779px) and (min-width: 400px) {
    :root { --vert-reserved: 250px; }
    /* The reserve buys slack; spend 8px of it clearing the gear off the THEMES
       panel, which on this size it still just touches. */
    #hud { padding-top: 20px; }
  }

  /* ══ The home wash, cross-faded ════════════════════════════════════════
     Two stacked layers, one fading up as the other fades down, so scrolling
     the road moves the screen from one world's colour into the next instead of
     snapping. Only opacity animates. Behind everything, and only on the home
     screen — in-game the wash on <html> is the one that shows. */
  .wash-layer {
    position: fixed; inset: 0;
    z-index: -3;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.55s ease;
  }
  /* The home wash is the same world palette the board uses, but it is a
     BACKDROP here, not the playfield: at full strength a saturated teal-into-
     violet fought the glass jars and the road sitting on top of it. A white
     scrim lifts it into an airy tint — the world is still recognisable, the
     jars still read as glass, and the road stays visible on every one of the
     33. Heavier at the top and bottom, lighter through the middle, so the
     screen has some depth instead of being one flat sheet. */
  .wash-layer::after {
    content: ''; position: absolute; inset: 0;
    background:
      radial-gradient(120% 70% at 50% 18%, rgba(255,255,255,0.50) 0%, rgba(255,255,255,0) 62%),
      linear-gradient(180deg,
        rgba(255,255,255,0.40) 0%,
        rgba(255,255,255,0.20) 42%,
        rgba(255,255,255,0.16) 62%,
        rgba(255,255,255,0.34) 100%);
  }
  body.home-active .wash-layer { z-index: -2; }
  body:not(.home-active) .wash-layer { opacity: 0 !important; }

  /* ══ HOME HEADER — one row, one baseline ═══════════════════════════════
     Shop sat in the flow while the coin chip and the info button were fixed to
     the top, so the three of them landed on three different lines and read as
     three unrelated things. They are one header: same top edge, same height,
     same glass, one on the left and two on the right. */
  body.home-active #shop-btn,
  body.home-active .coin-chip-home,
  body.home-active #info-btn {
    position: fixed;
    top: max(10px, env(safe-area-inset-top));
    height: 36px;
    z-index: 100;
    box-sizing: border-box;
    display: inline-flex; align-items: center;
    border-radius: 999px;
    background: linear-gradient(180deg, rgba(255,255,255,0.66), rgba(255,255,255,0.44));
    border: 1px solid rgba(255,255,255,0.74);
    backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.90),
      0 1px 2px rgba(80,55,110,0.08),
      0 6px 14px -6px rgba(80,55,110,0.22);
    margin: 0;
  }
  body.home-active #shop-btn {
    left: max(12px, env(safe-area-inset-left));
    padding: 0 15px 0 8px; gap: 7px; min-width: 0;
    /* Explicitly the same glass as the coin chip beside it. The cream panel
       fill it inherited from .panel made it read as a different material from
       everything else in the header. */
    background-image: linear-gradient(180deg, rgba(255,255,255,0.66), rgba(255,255,255,0.44));
    background-color: transparent;
  }
  body.home-active #shop-btn::before,
  body.home-active #shop-btn::after { content: none; }
  body.home-active .coin-chip-home {
    right: calc(12px + 36px + 8px);
    padding: 0 13px 0 8px; gap: 6px;
    font-size: 15px;
  }
  body.home-active #info-btn {
    right: max(12px, env(safe-area-inset-right));
    width: 36px; padding: 0;
    justify-content: center;
    color: var(--ui-ink-soft);
  }
  body.home-active #shop-btn .action-icon { width: 21px; height: 21px; }
  body.home-active .coin-chip-home .coin-icon { width: 19px; height: 19px; }
  body.home-active #shop-btn .label {
    font-size: 11px; letter-spacing: 0.08em; font-weight: 800;
    color: var(--ui-ink-soft); text-transform: uppercase;
  }
  body.home-active .coin-chip-home .coin-value {
    font-variant-numeric: tabular-nums; font-weight: 700;
  }
  /* The header is fixed and #title-screen already starts below it, so this only
     needs breathing room — 46px of extra clearance was stacking a second empty
     band under the one the header had already left. */
  body.home-active #title-screen { padding-top: 6px; }

  /* ── The title band, tightened ────────────────────────────────────────
     Title, a count and a bar were three stacked blocks doing one job. The count
     now rides on the bar's line, and the bar is a hairline instead of a slab. */
  body.home-active .title { margin-bottom: 2px; }
  body.home-active .theme-picker-label {
    font-family: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
    font-size: 11.5px; font-weight: 700; letter-spacing: 0.06em;
    text-transform: uppercase;
    background: none; box-shadow: none; border: 0;
    padding: 0; margin: 0 0 6px;
    color: var(--ui-ink-soft);
  }
  /* The number belongs to the world, like the score does — a fixed pink read as
     a leftover from a different screen on 30 of the 33 washes. */
  body.home-active .theme-picker-label .progress-count {
    color: var(--score-ink, var(--ui-ink));
    font-variant-numeric: tabular-nums;
    font-weight: 800;
  }
  body.home-active .worlds-progress {
    width: 54%; max-width: 190px; height: 5px;
    margin: 0 auto 4px;
    background: rgba(255,255,255,0.46);
    box-shadow: inset 0 1px 2px rgba(90,65,120,0.14);
  }

  /* Shop in glass. The amber fill above was deliberate — shop is money, and it
     matched the coin chip — but sitting in a header of glass controls it read as
     a different material stuck on. It keeps a WARM tint and an amber label
     instead, so it is still the money control without being the odd one out.
     Two IDs to outrank `body.home-active #hud #shop-btn`. */
  body.home-active #hud #shop-btn {
    height: 36px;
    padding: 0 15px 0 8px;
    border-radius: 999px;
    background:
      linear-gradient(180deg, rgba(255,246,232,0.62), rgba(255,236,206,0.40));
    border: 1px solid rgba(255,255,255,0.74);
    backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.92),
      0 1px 2px rgba(80,55,110,0.08),
      0 6px 14px -6px rgba(140,95,40,0.26);
  }
  body.home-active #hud #shop-btn .action-icon { width: 21px; height: 21px; }
  body.home-active #hud #shop-btn .label {
    color: #8a5a1f;
    font-size: 11px; font-weight: 800; letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  /* ══ The title block, lifted and given a surface ═══════════════════════
     It was floating halfway down an empty band with nothing under it, on a wash
     that changes colour on every scroll — so its ink was fighting 33 different
     grounds. It moves up under the header and sits on the same glass the header
     controls use, which fixes both at once: a fixed surface to read against, and
     one less empty band. */
  body.home-active #title-screen .title,
  body.home-active #title-screen #theme-picker-label,
  body.home-active #title-screen .worlds-progress { position: relative; z-index: 2; }

  body.home-active #title-screen {
    padding-top: 4px;
  }
  body.home-active .title {
    margin: 0 auto 0;
    padding: 9px 20px 7px;
    border-radius: 20px 20px 0 0;
    font-size: 25px;
    letter-spacing: -0.018em;
    /* One ink, not a three-stop gradient. The gradient was a wordmark trick for
       a fixed peach backdrop; against 33 washes a single deep berry holds its
       colour and its contrast everywhere. */
    background: none;
    -webkit-background-clip: border-box; background-clip: border-box;
    color: #7a2f63;
    filter: none;
    text-shadow: 0 1px 0 rgba(255,255,255,0.65);
  }
  /* The surface the whole block sits on. */
  body.home-active .title::before {
    content: ''; position: absolute; z-index: -1;
    left: 50%; transform: translateX(-50%);
    top: 0; width: min(86vw, 330px); height: calc(100% + 52px);
    border-radius: 22px;
    background: linear-gradient(180deg, rgba(255,255,255,0.58), rgba(255,255,255,0.34));
    border: 1px solid rgba(255,255,255,0.70);
    backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,0.90),
      0 8px 20px -10px rgba(70,45,100,0.24);
  }
  body.home-active .theme-picker-label {
    margin: 0 0 7px;
    color: #6b6280;
    font-size: 11px;
  }
  body.home-active .theme-picker-label .progress-count { color: #7a2f63; }
  body.home-active .worlds-progress {
    width: min(64vw, 240px);
    margin: 0 auto 16px;
    background: rgba(120, 95, 150, 0.16);
  }

  /* ══ THE COLLECTION ════════════════════════════════════════════════════
     A redesign, not a polish pass. What it replaces: a winding dotted road of
     33 tall glass cases, a title panel that took a third of the screen, and a
     UI whose ink and buttons changed colour with every world.

     The rules this screen is built to:
       - the OBJECT is the hero. It gets no card; a card would make the card
         the subject and the object its contents.
       - ONE fixed UI palette. The wash behind changes with the world, the ink,
         the surfaces and the CTA never do — that is what stopped the screen
         feeling like a different app per theme.
       - containers only where they earn it. There are two on this screen.
       - one CTA. Play / Resume / Unlock is the same button wearing the state.  */
  :root {
    --col-ink:      #45264f;     /* deep plum, the only text colour that matters */
    --col-ink-soft: #7c6489;
    --col-ink-mute: #a292ab;
    --col-cta-a:    #ff8a5c;     /* warm, and the only saturated UI on the screen */
    --col-cta-b:    #f4623a;
    --col-surface:  rgba(255,255,255,0.58);
  }

  #world-stage {
    display: flex; flex-direction: column; align-items: center;
    flex: 1 1 auto; min-height: 0;
    width: 100%;
    padding: 2px 0 clamp(8px, 2vh, 18px);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  /* ── Branding: small, and doing two jobs at once ─────────────────────── */
  .brand { text-align: center; width: 100%; }

  /* ── The reel. The object at size, its neighbours half in frame. ─────── */
  .reel {
    position: relative;
    display: flex; align-items: center; justify-content: center;
    width: 100%;
    margin: clamp(10px, 3vh, 26px) 0 0;
    touch-action: pan-y;
  }
  .hero {
    position: relative;
    width: min(56vw, 226px);
    aspect-ratio: 1;
    display: grid; place-items: center;
    flex: 0 0 auto;
  }
  /* The ground it sits on — a shadow, not a plate. This is the only thing
     under the object, and it is what makes it read as sitting somewhere
     rather than floating. */
  .hero::after {
    content: ''; position: absolute; left: 50%; bottom: 4%;
    width: 58%; height: 11%;
    transform: translateX(-50%);
    border-radius: 50%;
    background: radial-gradient(ellipse at center,
      rgba(70,40,95,0.24) 0%, rgba(70,40,95,0.10) 55%, transparent 76%);
    filter: blur(3px);
    z-index: 0;
  }
  #world-hero-img {
    position: relative; z-index: 1;
    width: 100%; height: auto; display: block;
    filter:
      drop-shadow(0 10px 16px rgba(60,35,85,0.22))
      drop-shadow(0 2px 3px rgba(60,35,85,0.16));
  }
  .peek {
    flex: 0 0 auto;
    border: 0; background: none; padding: 0; cursor: pointer;
    touch-action: manipulation;
    width: min(22vw, 92px);
    opacity: 0.44;
    transition: opacity 0.22s ease, transform 0.22s cubic-bezier(0.2,0,0,1);
  }
  .peek img {
    width: 100%; height: auto; display: block;
    filter: drop-shadow(0 6px 10px rgba(60,35,85,0.18));
  }
  .peek:active { opacity: 0.66; }

  /* The swap: the object leaves the way your thumb pushed it. */
  @keyframes reelInL { from { opacity: 0; transform: translateX(26px) scale(0.92); } to { opacity: 1; transform: none; } }
  @keyframes reelInR { from { opacity: 0; transform: translateX(-26px) scale(0.92); } to { opacity: 1; transform: none; } }

  /* ── Name, purpose, state ─────────────────────────────────────────────── */
  .world-name {
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(28px, 7.6vw, 36px); font-weight: 600;
    letter-spacing: -0.024em; line-height: 1.02;
    color: var(--col-ink); margin: 0;
    text-wrap: balance;
  }
  .world-goal {
    font-family: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
    font-size: clamp(13px, 3.6vw, 15px); font-weight: 500;
    color: var(--col-ink-soft); margin: 5px 0 0;
    text-wrap: pretty;
  }
  .world-note {
    font-family: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
    font-size: 12px; font-weight: 600; letter-spacing: 0.01em;
    color: var(--col-ink-mute); margin: 6px 0 0;
    min-height: 15px;
    font-variant-numeric: tabular-nums;
  }

  /* ── One button ───────────────────────────────────────────────────────── */
  .cta {
    margin-top: clamp(14px, 3vh, 24px);
    width: min(62vw, 232px);
    padding: 15px 22px;
    border: 0; border-radius: 999px;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: 18px; font-weight: 600; letter-spacing: 0.012em;
    color: #fff;
    background: linear-gradient(180deg, var(--col-cta-a), var(--col-cta-b));
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.46),
      inset 0 -2px 0 rgba(150,50,20,0.20),
      0 2px 4px rgba(60,25,15,0.14),
      0 12px 22px -10px rgba(210,80,45,0.58);
    cursor: pointer; touch-action: manipulation;
    transition: transform 0.12s cubic-bezier(0.2,0,0,1), box-shadow 0.18s ease;
  }
  .cta:active {
    transform: translateY(2px) scale(0.978);
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.38),
      0 1px 2px rgba(60,25,15,0.16),
      0 5px 10px -6px rgba(210,80,45,0.5);
  }
  /* Locked is the same button, quieter — not a different control. */
  .cta.is-locked {
    background: linear-gradient(180deg, #b9a4c4, #9c85ab);
    box-shadow:
      inset 0 1.5px 0 rgba(255,255,255,0.40),
      0 2px 4px rgba(60,40,80,0.12),
      0 10px 20px -10px rgba(110,80,140,0.45);
  }

  /* ── Where you are in the set ─────────────────────────────────────────── */
  .dots {
    display: flex; align-items: center; justify-content: center;
    gap: 7px; margin-top: clamp(12px, 2.6vh, 20px);
    height: 10px;
  }
  .dots i {
    width: 6px; height: 6px; border-radius: 99px;
    background: rgba(90,60,110,0.20);
    transition: width 0.26s cubic-bezier(0.22,1,0.36,1), background-color 0.26s ease;
  }
  .dots i.got { background: rgba(90,60,110,0.34); }
  .dots i.on {
    width: 20px;
    background: linear-gradient(90deg, var(--col-cta-a), var(--col-cta-b));
  }

  /* One ink for the whole screen, whatever the wash behind it is doing. */
  body.home-active .title,
  body.home-active .theme-picker-label,
  body.home-active .worlds-progress { display: none; }

  /* The 33-row picker is not part of this screen. It stays in the markup
     because other flows rebuild it, but nothing here shows it. */
  body.home-active #title-screen .picker-wrap,
  #title-screen .picker-wrap.is-stowed { display: none; }

  /* The stage owns the height the picker used to take, and centres in it —
     otherwise the whole composition sits high and the screen ends in a dead
     band, which is the single biggest thing that made it feel unfinished. */
  body.home-active #title-screen { justify-content: center; }
  #world-stage { justify-content: center; }

  /* Both neighbours keep their place even when there is no world on that side,
     so the hero never slides off centre at the ends of the set. */
  .peek[style*="hidden"] { pointer-events: none; }

  /* The brand sits UNDER THE HEADER, not in the middle of the composition.
     Centring the whole column together left a third of the screen empty above
     the title and another third below the dots; pinning the progress line to
     the top lets the object take the room it should. */
  #world-stage { justify-content: flex-start; padding-top: 6px; }
  #world-stage .brand { flex: 0 0 auto; }
  /* Everything about the world itself is one group, centred in what is left. */
  #world-stage .reel { flex: 0 0 auto; margin-top: 0; }
  #world-stage .brand + .reel { margin-top: auto; }
  #world-stage .dots { margin-bottom: auto; }

  .hero { width: min(64vw, 262px); }
  .peek { width: min(24vw, 104px); opacity: 0.40; }

  /* ══ THE 3D STAGE ══════════════════════════════════════════════════════
     A real perspective scene, not a flat row of scaled images.

     WHY CSS AND NOT WEBGL. The objects are already 3D renders; what was missing
     was a SPACE for them to stand in. `perspective` + `preserve-3d` gives that
     space on the compositor — no library, no new art, nothing to download, and
     it animates on transform alone, which is the one thing that stays smooth on
     a 120Hz phone. A WebGL scene would cost a bundle and a frame budget to
     re-create depth the browser already has.

     The neighbours are not "smaller" — they are FURTHER AWAY and turned away,
     which is why the scene reads as depth rather than as three sizes. */
  .slot {
    grid-area: 1 / 1;
    border: 0; background: none; padding: 0; margin: 0;
    transform-style: preserve-3d;
    transition:
      transform 0.42s cubic-bezier(0.22,1,0.36,1),
      opacity 0.42s ease,
      filter 0.42s ease;
  }
  .slot img { display: block; width: 100%; height: auto; }

  /* Front and centre, with a touch of top-light. */
  /* Breathing. Squishy is the whole personality, and a thing that is alive is
     the difference between a product shot and a game. */
  @keyframes heroFloat {
    0%, 100% { transform: translateZ(64px) translateY(0)      rotateZ(-0.6deg); }
    50%      { transform: translateZ(64px) translateY(-7px)   rotateZ(0.6deg); }
  }

  /* Turned away and set back — this is the depth cue that scale alone cannot
     give. Pointer-events stay on so they are still the way to change world. */

  /* The ground. A plane laid back in 3D, so the scene has a floor instead of
     the object hanging in a gradient. */
  .ground {
    position: absolute; left: 50%; bottom: 4%;
    width: 150%; height: 190px;
    transform: translateX(-50%) rotateX(74deg);
    transform-origin: 50% 100%;
    background:
      radial-gradient(ellipse 52% 60% at 50% 46%,
        rgba(255,255,255,0.34) 0%, rgba(255,255,255,0.10) 46%, transparent 72%);
    pointer-events: none;
    z-index: 0;
  }
  /* The hero's contact shadow, ON the ground plane rather than under the image. */

  /* The turn. The whole table swings, so the worlds move THROUGH the space
     rather than cutting to new positions. */

  @media (prefers-reduced-motion: reduce) {
    .turntable, .slot { transition: none; }
  }

  /* ── The neighbours stand BESIDE the hero, not behind it ──────────────
     translateX on a narrower slot moves it by its own width, so 72% left both
     of them tucked behind the whale. Pushed out in absolute terms instead, and
     the hero given the front of the Z stack. */

  /* ── Selling the unlock ───────────────────────────────────────────────
     The point of this screen is the 31 worlds you do not have yet. A locked one
     shows what it costs, how far along you already are, and the other way in —
     a grey "Unlock" on its own tells a player nothing about whether that is one
     run away or ten. */
  .unlock {
    width: min(62vw, 232px);
    margin-top: clamp(10px, 2.2vh, 16px);
    text-align: center;
  }
  .unlock-bar {
    width: 100%; height: 6px; border-radius: 99px;
    background: rgba(255,255,255,0.58);
    box-shadow: inset 0 0 0 1px rgba(90,60,110,0.08);
    overflow: hidden;
  }
  .unlock-bar i {
    display: block; height: 100%; width: 0%;
    border-radius: 99px;
    background: linear-gradient(90deg, #ffc65c, #ff9f2e);
    transition: width 0.5s cubic-bezier(0.22,1,0.36,1);
  }
  .unlock-line {
    font-family: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
    font-size: 12.5px; font-weight: 600;
    color: var(--col-ink-soft);
    margin: 7px 0 0;
    font-variant-numeric: tabular-nums;
  }
  /* The second way in, when there is one. Quiet — it is an alternative, not a
     competing call to action. */
  .unlock[hidden] { display: none; }

  /* A neighbour's price, so the gaps in the collection are visible from the
     first screen rather than two taps away. */
  .pip {
    position: absolute; right: 2%; bottom: 8%;
    display: inline-flex; align-items: center; gap: 3px;
    padding: 3px 8px 3px 6px;
    border-radius: 999px;
    background: rgba(255,255,255,0.94);
    color: #8a5a1f;
    font-family: 'Plus Jakarta Sans', -apple-system, system-ui, sans-serif;
    font-size: 10.5px; font-weight: 800;
    font-variant-numeric: tabular-nums;
    box-shadow: 0 2px 5px rgba(60,35,85,0.22);
    pointer-events: none;
  }
  .pip::before {
    content: ''; width: 10px; height: 10px; border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #ffd977, #f0a828 70%);
    box-shadow: inset 0 -1px 0 rgba(140,90,10,0.35);
  }
  .pip[hidden] { display: none; }

  /* ── The jar, wrapped around the hero in depth ─────────────────────────
     Not a card behind the object and not a frame around it: the jar's back wall
     and floor sit BEHIND the object on their own Z, its front glass sits in
     FRONT. The object is inside it, its lower half read through the glass, its
     head clear of the rim — which is the picture the game is actually about
     while leaving the object as the subject.

     Same material as #stage: white frost, a white rim, light from the top left. */
  .jar {
    position: absolute;
    left: 50%; bottom: 2%;
    width: 84%; height: 62%;
    transform: translateX(-50%);
    border-radius: 16% 16% 13% 13% / 12% 12% 10% 10%;
    pointer-events: none;
  }
  /* The glass you look THROUGH. Deliberately faint: any more and it stops being
     glass and starts being a white panel across the object's belly. */
  /* One shine, top-left, the same one the playfield jar carries. */

  /* ── Arrows ───────────────────────────────────────────────────────────
     The neighbours are tappable too, but a chevron is the thing people look
     for. Kept quiet: it is a control, not a feature. */
   /* the chevron's mass sits off-centre */

  /* Narrower and shorter: at 84x62 the jar read as a white slab parked behind
     the object with its wings hanging off the sides. A jar holds the LOWER part
     of the thing in it — the silhouette has to be the object's, not the box's. */
  .jar { width: 66%; height: 50%; bottom: 3%; }
  /* And the back wall is glass, not a panel. Most of what you see back there
     should be the world behind it. */
  /* Clear of the neighbours, which are their own buttons. */

  /* ── The jar, rebuilt so it reads on ANY object ────────────────────────
     The first version was a white frosted fill, and a white frost behind a
     white object — Kawaii's bear, the egg, the cloud — is invisible. That was
     the wrong model: real glass is not read by its fill, it is read by its
     EDGES. So the fill is almost gone and what is left is what actually says
     "jar": an open rim ellipse at the top, two side edges, a base, and one
     highlight down the left.

     Every edge carries a white highlight AND a dark hairline. The white holds
     it against dark objects, the hairline holds it against pale ones — neither
     alone survives all 33 worlds. */
  .jar { width: 72%; height: 54%; bottom: 2%; }


  /* The opening. A jar is a jar because you can see into it — this ellipse is
     the single strongest cue, and it is what was missing. */
  .jar .rim {
    position: absolute; left: 50%; top: 0;
    width: 100%; height: 15%;
    transform: translate(-50%, -50%);
    border-radius: 50%;
    box-sizing: border-box;
  }
  /* One highlight running down the glass, which is what makes it curved. */
  .jar .glint {
    position: absolute; left: 9%; top: 16%;
    width: 7%; height: 62%;
    border-radius: 99px;
    background: linear-gradient(180deg,
      rgba(255,255,255,0.86) 0%, rgba(255,255,255,0.30) 55%, transparent 100%);
    filter: blur(0.4px);
  }

  /* ── The jar has to CONTAIN the object, not sit under it ───────────────
     At 72x54 the jar was smaller than every hero, so a bear's head read as
     parked on a little box. A jar is a jar when the thing is inside it: the
     glass now surrounds the object and the object is sized to fit within it,
     with only its top breaking the rim line. */
  .jar { width: 94%; height: 76%; bottom: 1%; }

  /* A visible OPENING. The rim was a flat line across the top; an ellipse is
     what says "you can see into this". Its front half is drawn on the front
     glass, its back half behind the object, so the object sits between them. */
  .jar .rim { height: 17%; top: 0; }
  /* A jar is round, so its foot is an ellipse too. */

  /* The price on the button that pays it. */
  .cta.has-price { display: inline-flex; align-items: center; justify-content: center; gap: 9px; }
  .cta-price {
    display: inline-flex; align-items: center; gap: 4px;
    padding: 3px 10px 3px 7px;
    border-radius: 999px;
    background: rgba(255,255,255,0.26);
    box-shadow: inset 0 0 0 1px rgba(255,255,255,0.34);
    font-size: 15px; font-weight: 700;
    font-variant-numeric: tabular-nums;
  }
  .cta-price::before {
    content: ''; width: 13px; height: 13px; border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, #ffe08a, #f2ae2e 70%);
    box-shadow: inset 0 -1px 0 rgba(140,90,10,0.40);
  }

  /* ── The jar, shaped like one ──────────────────────────────────────────
     It was a cube: square shoulders, straight sides, a flat foot. That reads as
     a display case, not a jar. Real jars have round shoulders and a rounded
     foot, and — the part that actually mattered — the thing inside them does
     not poke out through the glass. The bear's ears and the whale's tail were
     hanging outside the walls, which breaks the illusion faster than any amount
     of highlight detail can build it. */
  .jar {
    width: 88%; height: 82%; bottom: 0;
    border-radius: 42% 42% 26% 26% / 22% 22% 16% 16%;
  }
  /* Sized and seated to sit INSIDE those walls. */
  .jar .rim { height: 14%; width: 84%; }
  .jar .glint { left: 13%; top: 20%; width: 6%; height: 54%; }

  /* ── Vertical rhythm ───────────────────────────────────────────────────
     There was a 180px hole between the progress line and the jar, and another
     under the dots. The screen reads as one composition when the gaps are in
     proportion to the thing they separate, not when they are whatever is left
     over. */
  .cta { margin-top: clamp(12px, 2.6vh, 20px); }
  .dots { margin-top: clamp(10px, 2.2vh, 16px); margin-bottom: 0; }
  #world-stage { justify-content: center; padding-top: 0; }
  #world-stage .brand { margin-bottom: 0; }

  /* The arrows were sitting on top of the neighbours they point at. */

  /* ── The jar is the game's own, baked once ────────────────────────────
     Everything below replaces the hand-drawn CSS glass. The back layer is the
     real jar art (drawContainer), the front is only a whisper of glass over the
     object's lower half plus the rim's front highlight — enough to read as
     "inside", not enough to wash the object out. */
  .jar {
    width: 92%; height: 84%; bottom: 0;
    border-radius: 0;
    background: none; border: 0; box-shadow: none;
  }
  /* Seated on the jar's floor, inside its walls. */

  /* ── Proportions ───────────────────────────────────────────────────────
     Two things were wrong. The jar element was 92x84 of a square box while the
     jar ART is 420x524, so the drawing was being stretched — the walls came out
     the wrong thickness. And the emoji was sized as a hero standing on its own,
     which is much bigger than a piece resting in a jar: it broke out through
     the glass on every side.

     Now the jar carries its own aspect so it is never distorted, and the piece
     is sized the way a piece in a run actually is. */
  .jar {
    width: 78%;
    height: auto;
    aspect-ratio: 420 / 740;
    bottom: 1%;
  }
  /* The neighbours keep the same relationship, just further away. */

  /* The piece, sized against the jar it sits in.
     This has to be the ID: #world-hero-img sets width:100% further up, and an id
     beats every class rule below it — which is why the emoji stayed hero-sized
     and kept breaking out through the glass however small the class said it was.
     It is also what made them look soft: the art is 256px, and at full hero size
     on a 2x screen it was being upscaled past its own resolution. At this size
     it renders at or under 1:1 and stays crisp. */
  #world-stage #world-hero-img {
    width: 46%;
    height: auto;
    margin: 0 auto;
    transform: translateY(14%);
  }

  /* ── The arrows moved to the button row ───────────────────────────────
     They were 38px discs floating over the neighbours at the jar's sides: small,
     easy to miss, and sitting on top of the very thing they point at. Beside the
     CTA they are in the row the thumb is already in, big enough to hit, and they
     read as part of one decision — play this one, or go and look at the next. */
  .cta-row {
    display: flex; align-items: center; justify-content: center;
    gap: clamp(10px, 3vw, 16px);
    width: 100%;
    margin-top: clamp(12px, 2.6vh, 20px);
  }
  .cta-row .cta { margin-top: 0; width: min(48vw, 190px); }


  /* ── Balance inside the jar ────────────────────────────────────────────
     Overcorrected: at 46% the piece was a speck at the bottom of a large empty
     case, which says "this world is empty" rather than "this is what is in it".
     A jar in play is mostly full, so the piece fills the glass and the jar comes
     down in size to stop it colliding with the progress line above. */
  .hero { width: min(58vw, 234px); }
  .jar { width: 72%; bottom: 3%; }
  #world-stage #world-hero-img { width: 56%; transform: translateY(10%); }
  /* The hero has to out-weigh its neighbours, which had grown to match it. */

  /* ── Vertical ─────────────────────────────────────────────────────────
     The group was pinned high with ~380px of nothing under the dots. Centred in
     the space it has, with the gaps in proportion to what they separate. */
  #world-stage { justify-content: center; }
  .cta-row { margin-top: clamp(14px, 3vh, 24px); }
  .dots { margin-top: clamp(12px, 2.4vh, 18px); }

  /* ── No ghost neighbours ───────────────────────────────────────────────
     The faded prev/next previews are gone. With the arrows now beside the CTA
     they were not carrying navigation any more, and two half-transparent
     objects bracketing the hero made the screen busier while making the hero
     itself look less certain. One object, front and centre.

     What they did still carry was the price tag that showed a locked world
     existed. The dots take that over: a world you own is solid, one you do not
     is a ring — so the gaps in the set are still visible at a glance. */

  .dots i {
    width: 7px; height: 7px;
    background: none;
    box-shadow: inset 0 0 0 1.5px rgba(90,60,110,0.30);
  }
  .dots i.got {
    background: rgba(90,60,110,0.34);
    box-shadow: none;
  }
  .dots i.on {
    width: 22px;
    background: linear-gradient(90deg, var(--col-cta-a), var(--col-cta-b));
    box-shadow: none;
  }

  /* ── The home jar IS the gameplay jar ─────────────────────────────────
     The playfield jar is two things stacked: #stage — a frosted box with a white
     rim, layered lift shadows and a top-left shine — and drawContainer()'s
     canvas painted inside it. The home jar only had the second one, which is why
     it read as a pair of floating glass rails instead of a jar.

     Everything below is #stage's own treatment, same numbers, on the layer that
     carries the art. */
  /* #stage's frost, under the drawn glass. Two background layers: the art on
     top, the frost beneath it. */
  /* #stage::before — the shine, which is most of why the glass looks lit. */

  /* ── Seated on the drawn floor, clear of the line above ────────────────
     The piece was hanging through the bottom of the glass, and the jar's top
     was touching the progress line. The jar art puts its floor at ~97% of its
     own height, so the piece is placed against that rather than centred in the
     box it happens to live in. */
  #world-stage #world-hero-img {
    width: 52%;
    transform: translateY(-4%);
    align-self: end;
    margin-bottom: 9%;
  }

  /* ── One jar, not two ──────────────────────────────────────────────────
     The frost box above already IS #stage — the frame the player sees in the
     game. Painting drawContainer()'s art on top of it drew a SECOND set of
     walls inside the first, which at this size read as two white rods stood up
     in the glass. The art goes; the frame stays. */

  /* ── The two lines under the jar ───────────────────────────────────────
     Both were cold: a blue-plum ink over washes that run warm, which is why
     the name sat on the background instead of on top of it, and the goal line
     went to grey. Warmer ink, a light edge so it lifts off any of the 33
     washes, and the goal split into a quiet verb and a loud noun. */
  #world-stage .world-name {
    font-size: clamp(26px, 6.8vw, 33px);
    font-weight: 600;
    letter-spacing: -0.028em;
    color: #3b2036;
    text-shadow: 0 1px 0 rgba(255,255,255,0.46);
  }
  #world-stage .world-goal {
    margin-top: 7px;
    font-size: clamp(12.5px, 3.4vw, 14px);
    font-weight: 500;
    color: rgba(59,32,54,0.56);
    text-shadow: 0 1px 0 rgba(255,255,255,0.34);
  }
  #world-stage .world-goal b {
    font-weight: 700;
    color: rgba(59,32,54,0.86);
  }

  /* ── Balance the column ────────────────────────────────────────────────
     .stage3d's `margin-top: auto` and .dots' `margin-bottom: auto` were a
     pair that split the slack evenly. Three later rules give
     `.brand + .stage3d` a fixed margin-top, so only the bottom auto survived
     and it swallowed the whole 187px: the screen packed to the top with a
     dead band under the dots. Drop the orphan and let justify-content
     centre the composition. */
  #world-stage .dots { margin-bottom: 0; }

  /* ── The object sits in the middle of its case ─────────────────────────
     Measured: 66px of empty glass above the object, 23px below. It was seated
     low because the jar used to carry drawContainer()'s drawn FLOOR — with
     that gone the box is a display case, and a case centres what it holds.
     Off-centre plus 43% area fill is what read as "empty jar". */
  #world-stage #world-hero-img {
    width: 58%;
    align-self: center;
    margin-bottom: 0;
    transform: none;
  }

  /* ── Rhythm ────────────────────────────────────────────────────────────
     Measured gaps, top to bottom: 20 / 61 / 91 / 19. Cramped where the eye
     enters, floaty where it has to act — the CTA was adrift 91px under the
     line that motivates it. Evened out, with the biggest gap kept above the
     jar so the hero gets its air. */
  #world-stage .cta-row { margin-top: clamp(20px, 4.8vh, 36px); }
  #world-stage .unlock { margin-top: clamp(14px, 3vh, 22px); }

  /* ── Flatten the case ──────────────────────────────────────────────────
     Centring the object in the jar had no effect, and then pushed it through
     the floor: 82px of empty glass above, -4 below. The cause is not
     alignment. .jar-back sits at translateZ(-40px) under perspective:900px
     with perspective-origin 50% 42%, so it PROJECTS smaller and shifted while
     the object at Z=0 does not — two boxes that can never line up by box
     alignment. The Z offset was staging for the prev/next slots, and those are
     display:none now. Same plane, and the turn animation still rotates. */

  /* ── Tie the case to what it holds ─────────────────────────────────────
     The two boxes were never related. .slot-hero is display:block and takes
     its height FROM the image (142px), while .jar-back is absolute at
     top:-75.9px, height 203 — the glass hung 76px above the box it is
     supposedly the case for. That, not alignment, is the 82px of empty glass
     over the object's head, and it is why `align-items: center` did nothing:
     there was no flex or grid container to obey it.
     So: the slot becomes a box with the WORLD's ratio (the same 420/570 the
     playfield uses), the glass fills that box exactly, and the object centres
     inside it. */
  #world-stage .jar {
    position: absolute;
    inset: 0;
    width: auto; height: auto;
    transform: none;
  }
  #world-stage #world-hero-img {
    position: relative;
    width: 74%;
    height: auto;
    margin: 0;
    transform: none;
  }

  /* ── The object needs its own edge ─────────────────────────────────────
     Sampled every one of the 33 rendered worlds: the object's body against
     the frost runs 1.24 (Kawaii's white bear) to 3.76, median 1.67, with 16
     of 33 under 1.6. Tinting the glass cannot fix that end of the range — a
     white object on a white ground has no body contrast to win, whatever the
     ground is. What separates two shapes of the same value is an EDGE, and a
     drop-shadow follows the object's own silhouette, so it works for the bear
     and the black cat alike. Two shadows: one to lift, one to sit it down. */
  #world-stage #world-hero-img {
    filter:
      drop-shadow(0 7px 12px rgba(48,28,56,0.20))
      drop-shadow(0 1px 2px rgba(48,28,56,0.18));
  }

  /* ── The info button ───────────────────────────────────────────────────
     1.15-1.20 contrast on ALL 33 worlds — the same number everywhere, which
     makes it the component's fault, not any palette's: a 12px glyph at
     opacity 0.72 in a 36px button with no fill, sitting right next to a coin
     chip that has one. It now matches its neighbour. */
  #info-btn.floating-icon-btn, body.home-active #info-btn.floating-icon-btn {
    opacity: 1;
    color: #5a4363;
    background-color: rgba(255,255,255,0.82);
    border: 1px solid rgba(255,255,255,0.92);
    box-shadow:
      0 6px 14px -6px rgba(60,40,80,0.28),
      inset 0 1px 0 rgba(255,255,255,0.9);
  }
  #info-btn.floating-icon-btn:active { transform: scale(0.94); }

  /* ── Position in the collection ────────────────────────────────────────
     The dot window is a rail now; see the painter for why. */
  #world-stage .dots {
    position: relative;
    display: block;
    width: min(52vw, 186px);
    height: 5px;
    margin-left: auto; margin-right: auto;
    border-radius: 99px;
    background: rgba(74,44,92,0.15);
    gap: 0;
    overflow: hidden;
  }
  #world-stage .dots i {
    position: absolute;
    top: 0; height: 100%;
    width: auto;
    border-radius: 99px;
    background: linear-gradient(90deg, var(--col-cta-a), var(--col-cta-b));
    transition: left 0.30s cubic-bezier(0.22,1,0.36,1),
                width 0.30s cubic-bezier(0.22,1,0.36,1);
  }
  @media (prefers-reduced-motion: reduce) {
    #world-stage .dots i { transition: none; }
  }

  /* The glyph was white — not because `color` failed (it computed to
     rgb(90,67,99) correctly) but because `.floating-icon-btn svg` hard-sets
     `fill: #ffffff`, and a CSS declaration beats the `fill="currentColor"`
     presentation attribute on the element. The same rule pins width to 11px,
     which is why bumping the markup to 15 did nothing either. */
  #info-btn.floating-icon-btn svg {
    width: 16px; height: 16px;
    fill: currentColor;
  }

  /* ── Stars on the tier strip ───────────────────────────────────────────
     The three tiers that earn a world's stars (TUNING.journey.starTiers) wear
     a ★. Hollow until this world has earned it, gold once it has. ::after is
     taken by the unlock ring, so the badge is ::before. The glyph is U+2605, a
     text star from the UI font — not an emoji, so the 3D-only emoji rule does
     not apply to it. */
  .tier-img[data-star]::before {
    content: '★';
    position: absolute;
    top: -7px; right: -6px;
    font-size: 9px; line-height: 1;
    color: rgba(255,255,255,0.96);
    text-shadow: 0 0 1px rgba(60,35,70,0.9), 0 1px 2px rgba(60,35,70,0.55);
    pointer-events: none;
  }
  .tier-img.star-earned[data-star]::before {
    color: #f6b73c;
    text-shadow: 0 0 1px rgba(120,70,0,0.95), 0 1px 2px rgba(120,70,0,0.45);
  }

  /* A banner that is announcing a star, not stating a goal. */
  .goal-banner.is-star .goal-lead { color: #c4850f; }
  .goal-banner.is-star {
    border-color: rgba(246,183,60,0.55);
  }

  /* ── A world's stars on home ───────────────────────────────────────────
     Between the name and the goal line: the name says where, the stars say
     how far, the goal says what next. Earned in gold; the rest a pale outline
     of the same glyph so the row always reads as "three to win". */
  #world-stage .world-stars {
    margin: 6px 0 0;
    display: flex; justify-content: center; gap: 5px;
    font-size: clamp(17px, 4.6vw, 20px); line-height: 1;
  }
  #world-stage .world-stars i {
    font-style: normal;
    color: rgba(255,255,255,0.78);
    text-shadow: 0 0 1px rgba(59,32,54,0.55), 0 1px 2px rgba(59,32,54,0.25);
  }
  #world-stage .world-stars i.on {
    color: #f6b73c;
    text-shadow: 0 0 1px rgba(120,70,0,0.9), 0 2px 5px rgba(200,130,20,0.35);
  }
  #world-stage .world-stars + .world-goal { margin-top: 6px; }

  /* "Just opened — your next world": the one time home announces a world a
     star opened mid-run. Warm, and it arrives with a small lift so the eye
     lands on it — once, then it is an ordinary world. */
  #world-stage .world-note.is-fresh {
    color: #c4760f;
    font-weight: 700;
    animation: freshNote 0.5s cubic-bezier(0.22,1,0.36,1) both;
  }
  @keyframes freshNote { from { opacity: 0; transform: translateY(4px); } }
  @media (prefers-reduced-motion: reduce) {
    #world-stage .world-note.is-fresh { animation: none; }
  }

  /* ── Power-ups, redesigned 2026-09-26 ─────────────────────────────────
     Was: three flat line glyphs beside one 3D emoji, on a frosted tile that
     took the world's wash through it and dissolved into it, with four badge
     colours and a teal play triangle. Now: every icon is a 3D object from the
     game's own emoji set, on ONE opaque tile, with ONE neutral badge.
     The tile is white, not glass: glass is the jar's material, and the jar is
     the one thing on screen that should read as glass. No backdrop-filter —
     it was also four blurs re-rendered every frame the world moved behind. */
  #powerup-bar .powerup-btn { box-shadow: none; }
  #powerup-bar .powerup-btn .powerup-icon {
    width: clamp(50px, 13.6vw, 58px); height: clamp(50px, 13.6vw, 58px);
    border-radius: 32%;
    background-color: rgba(255, 255, 255, 0.94);
    border: 1px solid rgba(255, 255, 255, 1);
    backdrop-filter: none; -webkit-backdrop-filter: none;
    box-shadow:
      inset 0 -3px 0 rgba(90, 60, 120, 0.07),
      0 8px 16px -6px rgba(50, 28, 80, 0.30),
      0 1px 2px rgba(50, 28, 80, 0.14);
    position: relative;
  }
  /* A whisper of depth on the plain tiles. Not on Twin: its inline
     background-image IS the piece, and this would never apply there anyway
     (inline wins) — scoped out so nobody reads it as intended for Twin. */
  #powerup-bar .powerup-btn:not(.twin) .powerup-icon {
    background-image: linear-gradient(180deg, #ffffff 0%, #f5f0fa 100%);
  }
  #powerup-bar .pw-emoji {
    width: 66%; height: 66%;
    object-fit: contain;
    display: block;
    filter: drop-shadow(0 2px 3px rgba(60, 35, 90, 0.26));
    pointer-events: none;
  }
  /* Twin's icon is the piece it would hand you — without a mark it reads as
     just another piece. ×2 says what the button does. */
  #powerup-bar .powerup-btn.twin .powerup-icon::after {
    content: '×2';
    position: absolute;
    right: -5px; bottom: -5px;
    padding: 2px 5px 3px;
    border-radius: 99px;
    background: #ffffff;
    color: #45264f;
    font: 800 10px/1 'Fredoka', -apple-system, system-ui, sans-serif;
    box-shadow: 0 1px 3px rgba(50, 28, 80, 0.30), inset 0 0 0 1px rgba(69, 38, 79, 0.10);
  }
  #powerup-bar .powerup-btn:active .powerup-icon { transform: translateY(1px) scale(0.95); }

  /* One badge style. The count is information, not decoration — the colour
     lives in the 3D objects now. */
  #powerup-bar .powerup-btn .powerup-count {
    top: -6px; right: -6px;
    min-width: 20px; height: 20px;
    padding: 0 5px;
    background: #fff;
    color: #45264f;
    font: 800 11px/1 'Fredoka', -apple-system, system-ui, sans-serif;
    box-shadow: 0 1px 3px rgba(50, 28, 80, 0.30), inset 0 0 0 1px rgba(69, 38, 79, 0.10);
  }
  #powerup-bar .powerup-btn .powerup-count.zero { color: rgba(69, 38, 79, 0.42); }
  /* Out, but an ad can refill it: the same white badge with a play mark in
     the game's warm CTA colour — an offer from this game, not a media player. */
  #powerup-bar .powerup-btn .powerup-count.ad-ready {
    background: #fff;
    color: transparent;
    box-shadow: 0 1px 3px rgba(50, 28, 80, 0.30), inset 0 0 0 1px rgba(244, 98, 58, 0.35);
  }
  #powerup-bar .powerup-btn .powerup-count.ad-ready::after {
    border-left-color: #f4623a;
  }

  @media (max-height: 760px) {
    #powerup-bar .powerup-btn .powerup-icon {
      width: clamp(44px, 12.2vw, 50px); height: clamp(44px, 12.2vw, 50px);
    }
    #powerup-bar .powerup-btn .powerup-count { min-width: 19px; height: 19px; font-size: 10.5px; }
  }

  /* Twin's piece, on its own element so it can shadow like the other icons.
     The tile now matches the other three exactly. */
  #powerup-bar .powerup-btn.twin .powerup-icon {
    background-image: linear-gradient(180deg, #ffffff 0%, #f5f0fa 100%);
    /* The 66% size was for the piece when it was the tile's own background;
       left in place it drew this gradient as a small square in the middle. */
    background-size: 100% 100%;
  }
  #powerup-bar .twin-piece {
    width: 66%; height: 66%;
    display: grid; place-items: center;
    font-style: normal; font-size: 24px; line-height: 1;
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    filter: drop-shadow(0 2px 3px rgba(60, 35, 90, 0.30));
    pointer-events: none;
  }

  /* ═══ Jar Shelf home (2026-09-26, from the approved shelf.html mock) ═══════
     The world you are on is a jar on a wooden shelf, a paper label naming it;
     the shelf above holds its collection around the frontier; ‹ Resume › sits
     where the thumb is. On home #world-stage is its own full-screen layer, so
     none of the earlier home layouts' rules can shape it. Sizes come from the
     viewport (dvh), 44px minimum targets, safe areas respected. */
  body.home-active #world-stage {
    --shelf-ink: #3b2340; --lip: #e3dbee;
    --paper-1: #fffaf0; --paper-2: #f6ead3; --wood-ink: #6b3f23;
    position: fixed; inset: 0; z-index: 40; margin: 0; width: auto;
    display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
    padding: calc(env(safe-area-inset-top, 0px) + 70px) 0 max(18px, env(safe-area-inset-bottom));
    overflow: hidden;
  }
  /* striped wallpaper over the world's wash */
  body.home-active #world-stage::before {
    content: ''; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: repeating-linear-gradient(90deg, rgba(255,255,255,.08) 0 22px, rgba(255,255,255,0) 22px 44px),
                radial-gradient(ellipse 90% 55% at 50% 45%, rgba(255,255,255,.26), rgba(255,255,255,0) 70%);
  }

  /* ── shelves ── */
  #world-stage .plank { position: relative; width: 100%; height: 26px; flex: 0 0 auto;
    background: linear-gradient(180deg, #f6d7b3 0 34%, #d9a577 34% 82%, #c58c5c 82% 100%);
    box-shadow: 0 16px 22px -12px rgba(90,45,15,.5), inset 0 1px 0 rgba(255,255,255,.7), inset 0 -2px 0 rgba(120,60,20,.25); }
  #world-stage .shelf-back { width: 100%; flex: 0 0 auto; }
  #world-stage .coll { display: flex; justify-content: center; align-items: flex-end; gap: clamp(6px, 2.4vw, 12px);
    height: 62px; padding: 0 16px; margin-bottom: -3px; }
  #world-stage .ci { position: relative; width: 44px; height: 44px; display: grid; place-items: end center;
    animation: collPop .45s cubic-bezier(.34,1.56,.64,1) both; animation-delay: calc(var(--i, 0) * 45ms); }
  #world-stage .ci img { width: 100%; height: 100%; object-fit: contain; }
  #world-stage .ci.got img { filter: drop-shadow(0 4px 3px rgba(60,35,20,.28)); }
  #world-stage .ci.unk img { filter: brightness(0) opacity(.18); }
  #world-stage .ci.next { width: 54px; height: 54px; }
  #world-stage .ci.next::before { content: ''; position: absolute; inset: -8px; border-radius: 50%;
    background: radial-gradient(circle, rgba(255,255,255,.85), rgba(255,255,255,0) 68%); animation: nextGlow 2.4s ease-in-out infinite; }
  #world-stage .ci.next > img:first-child { position: relative; filter: brightness(0) opacity(.32); }
  #world-stage .ci .cstar { position: absolute; width: 22px; height: 22px; right: -7px; top: -5px; filter: drop-shadow(0 2px 2px rgba(150,90,0,.4)); }
  #world-stage .coll-tag { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); padding: 2px 9px; border-radius: 8px;
    background: #fff; font: 700 11px var(--display, 'Fredoka'), system-ui, sans-serif; color: var(--shelf-ink); box-shadow: 0 2px 0 rgba(120,80,40,.25); white-space: nowrap; }
  @keyframes collPop { from { opacity: 0; transform: translateY(8px) scale(.6); } }
  @keyframes nextGlow { 50% { opacity: .4; transform: scale(.9); } }

  /* ── the hero jar ── */
  #world-stage .shelf-hero { flex: 1 1 auto; min-height: 0; width: 100%; display: flex; align-items: flex-end; justify-content: center; }
  #world-stage .shelf-jar { --w: clamp(160px, min(62vw, calc((100dvh - 440px) / 1.3)), 290px);
    position: relative; width: var(--w); margin-bottom: -4px; transform-origin: 50% 100%; }
  #world-stage .jar-lid { display: block; position: relative; z-index: 2; margin: 0 auto -4px; width: 78%; height: calc(var(--w) * .11);
    border-radius: 12px 12px 6px 6px;
    background: repeating-linear-gradient(90deg, rgba(255,255,255,0) 0 7px, rgba(255,255,255,.22) 7px 9px, rgba(0,0,0,.06) 9px 11px),
                linear-gradient(180deg, var(--lid1, #8fd3ff), var(--lid2, #4b93e6));
    box-shadow: inset 0 2px 0 rgba(255,255,255,.55), inset 0 -3px 0 rgba(0,0,0,.14), 0 4px 6px -2px rgba(60,20,50,.35); }
  #world-stage .jar-glass { position: relative; aspect-ratio: 420 / 500; border-radius: 13% 13% 11% 11% / 10% 10% 8% 8%;
    background: radial-gradient(ellipse 30% 38% at 18% 26%, rgba(255,255,255,.7), rgba(255,255,255,0) 70%),
      linear-gradient(90deg, rgba(255,255,255,.55) 0%, rgba(255,255,255,.12) 16%, rgba(255,255,255,.05) 50%, rgba(255,255,255,.14) 84%, rgba(255,255,255,.5) 100%),
      linear-gradient(180deg, rgba(255,255,255,.34), rgba(255,255,255,.22));
    border: 2px solid rgba(255,255,255,.85);
    box-shadow: inset 0 0 0 1px rgba(255,255,255,.35), inset 0 -18px 30px -12px rgba(255,255,255,.55), 0 20px 30px -18px rgba(40,20,60,.35);
    display: grid; place-items: center; overflow: hidden; }
  #world-stage .jar-glass::after { content: ''; position: absolute; left: 9%; top: 8%; width: 7%; height: 62%; border-radius: 99px;
    background: linear-gradient(180deg, rgba(255,255,255,.9), rgba(255,255,255,0)); }
  #world-stage #world-hero-img { width: 70% !important; height: auto !important; max-width: none; transform: translateY(-12%);
    filter: drop-shadow(0 10px 12px rgba(40,20,60,.25)) !important; margin: 0 !important; position: static !important; }
  #world-stage .jar-label { position: absolute; left: 50%; top: 66%; z-index: 3; transform: translateX(-50%) rotate(-4deg);
    padding: 7px 16px 9px; border-radius: 10px; text-align: center; white-space: nowrap;
    background: linear-gradient(180deg, var(--paper-1), var(--paper-2)); color: var(--wood-ink);
    box-shadow: 0 6px 12px -4px rgba(80,45,20,.45), inset 0 0 0 1.5px rgba(160,110,60,.25), inset 0 0 0 4px rgba(255,255,255,.35); }
  #world-stage .jar-num { display: block; font: 700 10px var(--body, 'Plus Jakarta Sans'), system-ui, sans-serif; letter-spacing: .16em; text-transform: uppercase; opacity: .7; }
  #world-stage .jar-label .world-name { margin: 1px 0 0 !important; font: 700 clamp(21px, 5.6vw, 25px)/1.05 'Fredoka', system-ui, sans-serif !important;
    color: var(--wood-ink) !important; letter-spacing: -.01em; text-shadow: none !important; }
  #world-stage .jar-label .world-stars { display: flex !important; justify-content: center; gap: 3px; margin: 5px 0 0 !important; }
  #world-stage .jar-label .world-stars img { width: 20px; height: 20px; filter: drop-shadow(0 2px 2px rgba(150,90,0,.35)); }
  #world-stage .jar-label .world-stars img.off { filter: grayscale(1) brightness(.78) opacity(.55); }
  #world-stage .plank-front { margin: 0; }

  /* ── goal strip, note, CTA ── */
  #world-stage .world-goal { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 6px; margin: 16px 16px 0 !important; min-height: 40px;
    padding: 6px 16px !important; border-radius: 99px; background: #fff; color: var(--shelf-ink) !important;
    font: 600 15px 'Fredoka', system-ui, sans-serif !important; text-shadow: none !important;
    box-shadow: 0 3px 0 var(--lip), 0 10px 20px -10px rgba(50,28,80,.4); }
  #world-stage .world-goal:empty { visibility: hidden; }
  #world-stage .world-goal b { color: var(--shelf-ink); }
  #world-stage .goal-emoji { width: 32px; height: 32px; margin: -6px 0; filter: drop-shadow(0 2px 2px rgba(90,50,20,.3)); }
  #world-stage .goal-star { width: 22px; height: 22px; margin: -4px 0; filter: drop-shadow(0 2px 2px rgba(150,90,0,.35)); }
  #world-stage .world-note { flex: 0 0 auto; min-height: 0; margin: 8px 0 0 !important; font: 700 13px 'Fredoka', system-ui, sans-serif; color: #fff; text-shadow: 0 1px 3px rgba(40,20,60,.5); }
  #world-stage .world-note:empty { display: none; }
  #world-stage .cta-row { flex: 0 0 auto; display: flex; align-items: center; gap: 12px; width: 100%; max-width: 440px; margin: 16px 0 0 !important; padding: 0 16px; }
  #world-stage #world-play { flex: 1 1 auto; height: 58px !important; min-width: 0; border: 0; border-radius: 18px !important;
    background: #ff6b35 !important; color: #fff !important; font: 700 23px 'Fredoka', system-ui, sans-serif !important; letter-spacing: .02em; text-shadow: none !important;
    box-shadow: 0 4px 0 #d9501f, 0 14px 24px -10px rgba(220,80,30,.6) !important; display: inline-flex; align-items: center; justify-content: center; gap: 10px; }
  #world-stage #world-play::before, #world-stage #world-play::after { display: none !important; }
  #world-stage #world-play:active { transform: translateY(3px); box-shadow: 0 1px 0 #d9501f !important; }
  #world-stage #world-play .cta-price { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px 3px 4px; border-radius: 99px;
    background: rgba(120,40,10,.22); font-size: 20px; position: static !important; }
  #world-stage #world-play .cta-price::before { content: ''; width: 26px; height: 26px; background: url('../assets/fluentui-emoji/Coin/3D/coin_3d.webp') center / contain no-repeat; }
  #world-stage #world-play.is-locked { background: #40b456 !important; box-shadow: 0 4px 0 #2a8a3c !important; }
  #world-stage .unlock { flex: 0 0 auto; width: min(260px, 70%); margin: 12px 0 0 !important; text-align: center; }
  #world-stage .unlock-line { color: #fff; font: 700 12.5px 'Fredoka', system-ui, sans-serif; text-shadow: 0 1px 3px rgba(40,20,60,.5); margin: 6px 0 0; }

  /* ── world change: the jar slides in from the side you travelled to ── */
  #world-stage.swap-l .shelf-jar { animation: jarInL .42s cubic-bezier(.22,1,.36,1) both; }
  #world-stage.swap-r .shelf-jar { animation: jarInR .42s cubic-bezier(.22,1,.36,1) both; }
  @keyframes jarInL { from { opacity: 0; transform: translateX(40%) rotate(4deg) scale(.92); } }
  @keyframes jarInR { from { opacity: 0; transform: translateX(-40%) rotate(-4deg) scale(.92); } }
  /* a tap on the jar gives it a little squish */
  #world-stage .shelf-jar:active { animation: jarSquish .35s ease-out; }
  @keyframes jarSquish { 40% { transform: scale(1.04, .95); } 70% { transform: scale(.98, 1.02); } }

  /* ── top bar on home: settings left, Shop + coins right ── */
  body.home-active #settings-btn.floating-icon-btn { position: fixed; z-index: 100; left: 16px; right: auto;
    top: calc(env(safe-area-inset-top, 0px) + 12px); width: 44px; height: 44px; border-radius: 50%; opacity: 1; border: 0;
    background: #fff; color: var(--shelf-ink, #3b2340); box-shadow: 0 3px 0 #e3dbee, 0 10px 20px -10px rgba(50,28,80,.4); backdrop-filter: none; -webkit-backdrop-filter: none; }
  body.home-active #settings-btn svg { width: 21px; height: 21px; fill: #3b2340; }
  body.home-active .coin-chip-home { position: fixed; z-index: 100; right: 16px; left: auto; top: calc(env(safe-area-inset-top, 0px) + 12px);
    height: 44px; min-width: 104px; padding: 0 6px 0 8px; gap: 6px; border-radius: 16px; background: #fff; border: 0; cursor: pointer;
    box-shadow: 0 3px 0 #e3dbee, 0 10px 20px -10px rgba(50,28,80,.4); backdrop-filter: none; -webkit-backdrop-filter: none; }
  body.home-active .coin-chip-home .coin-icon { width: 28px; height: 28px; }
  body.home-active .coin-chip-home .coin-value { color: #3b2340; font: 700 18px 'Fredoka', system-ui, sans-serif; flex: 1; }
  .coin-plus { display: none; }
  body.home-active .coin-chip-home .coin-plus { display: grid; place-items: center; width: 26px; height: 26px; border-radius: 9px;
    background: #e0527f; color: #fff; font: 800 18px/1 'Fredoka', system-ui, sans-serif; }
  body.home-active #shop-btn { position: fixed; z-index: 100; left: auto; right: calc(16px + 104px + 8px); top: calc(env(safe-area-inset-top, 0px) + 12px);
    height: 44px; min-width: 0; padding: 0 13px 0 8px; gap: 6px; border-radius: 16px; display: inline-flex; flex-direction: row-reverse; align-items: center;
    background: #fff; background-image: none; border: 0; box-shadow: 0 3px 0 #e3dbee, 0 10px 20px -10px rgba(50,28,80,.4); backdrop-filter: none; -webkit-backdrop-filter: none; }
  body.home-active #shop-btn .label { font: 600 16px 'Fredoka', system-ui, sans-serif; color: #3b2340; letter-spacing: 0; text-transform: none; margin: 0; }
  body.home-active #shop-btn .action-icon { width: 28px; height: 28px; margin: 0; }

  @media (prefers-reduced-motion: reduce) {
    #world-stage .ci, #world-stage .ci.next::before,
    #world-stage.swap-l .shelf-jar, #world-stage.swap-r .shelf-jar, #world-stage .shelf-jar:active { animation: none !important; }
  }
  @media (max-height: 640px) {
    body.home-active #world-stage { padding-top: calc(env(safe-area-inset-top, 0px) + 62px); }
    #world-stage .coll { height: 52px; }
    #world-stage .ci { width: 36px; height: 36px; } #world-stage .ci.next { width: 44px; height: 44px; }
  }

  /* Jar Shelf home — the shop chip wins over the HUD's action-panel styling,
     which is more specific and was repainting it as a small grey "SHOP". */
  html body.home-active #shop-btn.action-panel {
    height: 44px !important; min-width: 0 !important; padding: 0 13px 0 8px !important; border-radius: 16px !important;
    background: #fff !important; border: 0 !important; opacity: 1 !important; filter: none !important;
    box-shadow: 0 3px 0 #e3dbee, 0 10px 20px -10px rgba(50,28,80,.4) !important;
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
    display: inline-flex !important; flex-direction: row-reverse !important; align-items: center !important; gap: 6px !important; }
  html body.home-active #shop-btn.action-panel .label { font: 600 16px 'Fredoka', system-ui, sans-serif !important; color: #3b2340 !important;
    letter-spacing: 0 !important; text-transform: none !important; opacity: 1 !important; margin: 0 !important; }
  html body.home-active #shop-btn.action-panel .action-icon { width: 28px !important; height: 28px !important; margin: 0 !important; }
  html body.home-active #shop-btn.action-panel::before, html body.home-active #shop-btn.action-panel::after { display: none !important; }
  /* the jar takes more of a tall screen */
  #world-stage .shelf-jar { --w: clamp(160px, min(68vw, calc((100dvh - 430px) / 1.25)), 300px) !important; }

  /* On home a modal covers the whole screen, top bar included — it lives inside
     #stage, whose box on home is only the middle of the screen. */
  body.home-active #stage .overlay:not(.hidden) { position: fixed !important; inset: 0 !important; z-index: 150 !important; border-radius: 0 !important; }

  /* Jar Shelf home: the wash is a rich backdrop now, not an airy tint — the old
     16–50% white scrim was built for the road and washed every world out. */
  body.home-active .wash-layer::after {
    background: linear-gradient(180deg, rgba(255,255,255,.10) 0%, rgba(255,255,255,.03) 45%, rgba(255,255,255,.06) 100%) !important; }
  body.home-active #world-stage::before {
    background: repeating-linear-gradient(90deg, rgba(255,255,255,.07) 0 22px, rgba(255,255,255,0) 22px 44px) !important; }

  /* Jar Shelf home — the next silhouette stands plain, no glow disc behind it */
  #world-stage .ci.next::before { display: none !important; }
  /* the note is a small chip with its icon, readable on any wall */
  #world-stage .world-note { display: inline-flex !important; align-items: center; gap: 5px; margin: 10px 0 0 !important; padding: 3px 11px 3px 7px;
    border-radius: 99px; background: rgba(40,20,60,.42); color: #fff !important; font: 700 13px 'Fredoka', system-ui, sans-serif !important; text-shadow: none !important; }
  #world-stage .world-note .note-icon { width: 20px; height: 20px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.25)); }
  #world-stage .world-note.is-fresh { background: #fff; color: #c2410c !important; padding-left: 11px; animation: none; }

  /* ═══ Shelf chrome v2 ═══
     Buttons stay white (liked), but their lip is the shelf's wood and they
     carry a top gloss, so they read as part of the same furniture. */
  #world-stage .world-note:empty { display: none !important; }
  body.home-active #settings-btn.floating-icon-btn,
  html body.home-active #shop-btn.action-panel,
  body.home-active .coin-chip-home,
  #world-stage .cta-row .nav {
    background: linear-gradient(180deg, #ffffff 0%, #fbf6ef 100%) !important;
    box-shadow: 0 4px 0 #d9b48a, 0 10px 18px -8px rgba(110,60,20,.45), inset 0 2px 0 #fff, inset 0 0 0 1px rgba(170,120,70,.18) !important; }
  #world-stage .cta-row .nav:active, body.home-active #settings-btn.floating-icon-btn:active, html body.home-active #shop-btn.action-panel:active {
    transform: translateY(3px); box-shadow: 0 1px 0 #d9b48a, inset 0 0 0 1px rgba(170,120,70,.18) !important; }
  body.home-active .coin-chip-home .coin-plus { background: linear-gradient(180deg, #8ee07f, #45b554) !important; box-shadow: 0 2px 0 #2f8a3e, inset 0 1px 0 rgba(255,255,255,.55); }
  /* "or" — the free way above, the paid way below */
  #world-stage .world-or { display: flex; align-items: center; gap: 10px; width: min(220px, 60%); margin: 10px 0 -4px; color: #fff;
    font: 700 13px 'Fredoka', system-ui, sans-serif; text-transform: uppercase; letter-spacing: .12em; text-shadow: 0 1px 3px rgba(40,20,60,.5); }
  #world-stage .world-or[hidden] { display: none; }
  #world-stage .world-or::before, #world-stage .world-or::after { content: ''; flex: 1; height: 2px; border-radius: 2px; background: rgba(255,255,255,.55); }
  /* the ad button: green, and it says AD */
  #world-stage #world-play.is-ad { background: #3fb655 !important; box-shadow: 0 4px 0 #2a8a3c, 0 14px 24px -10px rgba(30,120,50,.55) !important; }
  #world-stage #world-play .cta-label { display: inline-flex; align-items: center; gap: 8px; }
  #world-stage #world-play .ad-tag { display: inline-flex; align-items: center; gap: 3px; padding: 3px 8px 3px 6px; border-radius: 8px;
    background: #fff; color: #2a8a3c; font: 800 13px/1 'Fredoka', system-ui, sans-serif; letter-spacing: .06em; }
  #world-stage #world-play .ad-tag::before { content: ''; border-left: 8px solid #2a8a3c; border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
  /* coins toward the price, with the coin */
  #world-stage .unlock-line { display: inline-flex; align-items: center; gap: 5px; font-size: 14px !important; }
  #world-stage .unlock-coin { width: 20px; height: 20px; filter: drop-shadow(0 1px 1px rgba(0,0,0,.25)); }
  #world-stage .unlock-bar { height: 8px; border-radius: 99px; background: rgba(255,255,255,.45); overflow: hidden; }
  #world-stage .unlock-bar i { display: block; height: 100%; border-radius: 99px; background: linear-gradient(90deg, #ffd166, #f5a311); }

  /* ═══ Shelf chrome v3 — glossy plum ═══
     Chosen after rendering white, lid-coloured, glass and plum on real screens:
     plum reads on every wall, sits with the label's dark ink, and leaves orange
     to the Play button alone. */
  body.home-active #settings-btn.floating-icon-btn, html body.home-active #shop-btn.action-panel, body.home-active .coin-chip-home, #world-stage .cta-row .nav {
    background: linear-gradient(180deg, #7b56b6 0%, #5c3a98 55%, #4e2f86 100%) !important;
    box-shadow: 0 4px 0 #34205e, 0 10px 18px -8px rgba(40,20,70,.55), inset 0 2px 0 rgba(255,255,255,.35), inset 0 0 0 1.5px rgba(255,255,255,.12) !important;
    color: #fff !important; }
  body.home-active #settings-btn svg { fill: #fff !important; }
  html body.home-active #shop-btn.action-panel .label, body.home-active .coin-chip-home .coin-value { color: #fff !important; }
  #world-stage .cta-row .nav:active, body.home-active #settings-btn.floating-icon-btn:active, html body.home-active #shop-btn.action-panel:active, body.home-active .coin-chip-home:active {
    transform: translateY(3px); box-shadow: 0 1px 0 #34205e, inset 0 2px 0 rgba(255,255,255,.35) !important; }

  /* ═══ Shelf chrome v4 — 3D candy ═══
     A thick bevel under every button, a glossy highlight across its top, a
     darker belly and a thin white rim: the buttons read as pressable objects,
     the same family as the glossy jar lid. */
  body.home-active #settings-btn.floating-icon-btn, html body.home-active #shop-btn.action-panel, body.home-active .coin-chip-home, #world-stage .cta-row .nav {
    position: fixed;
    background: linear-gradient(180deg, #9a74d6 0%, #7450b8 45%, #5a3998 100%) !important;
    box-shadow:
      0 0 0 2px rgba(255,255,255,.85),
      0 6px 0 2px #3a2268,
      0 14px 20px -6px rgba(40,20,70,.55),
      inset 0 -5px 8px rgba(40,15,80,.35),
      inset 0 2px 0 rgba(255,255,255,.45) !important;
    overflow: hidden; }
  body.home-active #settings-btn.floating-icon-btn::after, html body.home-active #shop-btn.action-panel::after,
  body.home-active .coin-chip-home::after, #world-stage .cta-row .nav::after {
    content: '' !important; display: block !important; position: absolute; left: 14%; right: 14%; top: 3px; height: 38%;
    border-radius: 99px; background: linear-gradient(180deg, rgba(255,255,255,.6), rgba(255,255,255,0)); pointer-events: none; }
  body.home-active .coin-chip-home > *, html body.home-active #shop-btn.action-panel > *, #world-stage .cta-row .nav > * { position: relative; z-index: 1; }
  body.home-active .coin-chip-home .coin-value, html body.home-active #shop-btn.action-panel .label { text-shadow: 0 2px 0 rgba(40,15,80,.45) !important; }
  body.home-active #settings-btn svg { filter: drop-shadow(0 2px 0 rgba(40,15,80,.45)); }

  #world-stage #world-play {
    background: linear-gradient(180deg, #ffa25a 0%, #ff7a3a 45%, #ee5a22 100%) !important;
    box-shadow:
      0 0 0 2.5px rgba(255,255,255,.9),
      0 7px 0 2.5px #b8411a,
      0 18px 26px -8px rgba(170,60,20,.55),
      inset 0 -6px 10px rgba(160,40,0,.3),
      inset 0 2px 0 rgba(255,255,255,.55) !important;
    text-shadow: 0 2px 0 rgba(150,45,10,.5) !important; position: relative; overflow: hidden; }
  #world-stage #world-play::before { content: '' !important; display: block !important; position: absolute; left: 8%; right: 8%; top: 4px; height: 40%;
    border-radius: 99px; background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,0)); pointer-events: none; }
  #world-stage #world-play .cta-label { position: relative; z-index: 1; }
  #world-stage #world-play.is-ad {
    background: linear-gradient(180deg, #7fe07a 0%, #4cc15b 45%, #2fa045 100%) !important;
    box-shadow: 0 0 0 2.5px rgba(255,255,255,.9), 0 7px 0 2.5px #1f7a33, 0 18px 26px -8px rgba(20,100,40,.5),
      inset 0 -6px 10px rgba(0,80,20,.3), inset 0 2px 0 rgba(255,255,255,.55) !important;
    text-shadow: 0 2px 0 rgba(20,90,35,.5) !important; }
  #world-stage #world-play:active, #world-stage .cta-row .nav:active,
  body.home-active #settings-btn.floating-icon-btn:active, html body.home-active #shop-btn.action-panel:active, body.home-active .coin-chip-home:active {
    transform: translateY(5px) !important; }
  #world-stage #world-play:active { box-shadow: 0 0 0 2.5px rgba(255,255,255,.9), 0 2px 0 2.5px #b8411a, inset 0 -6px 10px rgba(160,40,0,.3) !important; }
  /* the + : a small green candy */
  body.home-active .coin-chip-home .coin-plus { background: linear-gradient(180deg, #8ee07f, #3fb04f) !important;
    box-shadow: 0 0 0 1.5px rgba(255,255,255,.85), 0 2px 0 1.5px #2a7f38, inset 0 1px 0 rgba(255,255,255,.55) !important; }

  /* ═══ Shelf chrome v5 — cream candy on wood ═══
     Plum was wrong for a scene of paper, wood and glass. The controls are
     cream candy — glossy top, soft belly — sitting on a thick bevel in the
     shelf's own wood, with the world's colour in their icons. */
  body.home-active #settings-btn.floating-icon-btn, html body.home-active #shop-btn.action-panel, body.home-active .coin-chip-home, #world-stage .cta-row .nav {
    background: linear-gradient(180deg, #fffdf8 0%, #fbf3e6 55%, #f1e2cb 100%) !important;
    box-shadow:
      0 0 0 1.5px rgba(194,146,95,.55),
      0 6px 0 1.5px #c2925f,
      0 14px 20px -6px rgba(110,60,20,.45),
      inset 0 -5px 8px rgba(180,130,80,.22),
      inset 0 2px 0 #fff !important;
    color: #5a3a1f !important; }
  body.home-active #settings-btn svg { fill: #6b4524 !important; filter: none !important; }
  html body.home-active #shop-btn.action-panel .label, body.home-active .coin-chip-home .coin-value { color: #5a3a1f !important; text-shadow: 0 1px 0 #fff !important; }
  body.home-active #settings-btn.floating-icon-btn::after, html body.home-active #shop-btn.action-panel::after,
  body.home-active .coin-chip-home::after, #world-stage .cta-row .nav::after {
    background: linear-gradient(180deg, rgba(255,255,255,.95), rgba(255,255,255,0)) !important; }
  body.home-active #settings-btn.floating-icon-btn:active, html body.home-active #shop-btn.action-panel:active,
  body.home-active .coin-chip-home:active, #world-stage .cta-row .nav:active {
    box-shadow: 0 0 0 1.5px rgba(194,146,95,.55), 0 1px 0 1.5px #c2925f, inset 0 -5px 8px rgba(180,130,80,.22) !important; }
  /* old carousel rules left top/left/right offsets on .nav; they pushed the arrows below Play */
  /* one line, always */
  #world-stage #world-play .cta-label { white-space: nowrap; }
  #world-stage #world-play.is-ad { font-size: 19px !important; }

  /* ═══ Shelf chrome v6 — make-interfaces-feel-better rules ═══
     Depth from layered shadows, not stacked rings: a 1px hairline outline, a
     tight contact shadow and a soft ambient one. A faint top highlight gives
     the surface a curve. Press is scale(0.96) on a named transition. */
  body.home-active #settings-btn.floating-icon-btn, html body.home-active #shop-btn.action-panel, body.home-active .coin-chip-home, #world-stage .cta-row .nav {
    background: linear-gradient(180deg, #ffffff 0%, #f7f4fb 100%) !important;
    box-shadow:
      0 0 0 1px oklch(0 0 0 / 0.06),
      0 1px 2px oklch(0 0 0 / 0.08),
      0 6px 16px -4px rgba(60,35,90,.22),
      inset 0 1px 0 #fff !important;
    color: #3b2340 !important;
    transition-property: transform, box-shadow; transition-duration: .12s; transition-timing-function: cubic-bezier(.2,0,0,1); }
  body.home-active #settings-btn.floating-icon-btn::after, html body.home-active #shop-btn.action-panel::after,
  body.home-active .coin-chip-home::after, #world-stage .cta-row .nav::after { display: none !important; }
  body.home-active #settings-btn svg { fill: #3b2340 !important; filter: none !important; }
  html body.home-active #shop-btn.action-panel .label, body.home-active .coin-chip-home .coin-value { color: #3b2340 !important; text-shadow: none !important; }
  body.home-active .coin-chip-home .coin-value { font-variant-numeric: tabular-nums; }
  body.home-active .coin-chip-home .coin-plus { background: #34b554 !important; box-shadow: inset 0 1px 0 rgba(255,255,255,.35), 0 1px 2px oklch(0 0 0 / 0.15) !important; }

  #world-stage #world-play {
    background: linear-gradient(180deg, #ff8a4c 0%, #f4622a 100%) !important;
    box-shadow:
      0 0 0 1px oklch(0 0 0 / 0.06),
      0 2px 3px rgba(190,70,20,.35),
      0 12px 24px -8px rgba(230,90,30,.55),
      inset 0 1px 0 rgba(255,255,255,.4),
      inset 0 -2px 0 rgba(150,40,0,.18) !important;
    text-shadow: 0 1px 1px rgba(140,40,0,.35) !important;
    transition-property: transform, box-shadow; transition-duration: .12s; transition-timing-function: cubic-bezier(.2,0,0,1); }
  #world-stage #world-play::before { display: none !important; }
  #world-stage #world-play.is-ad {
    background: linear-gradient(180deg, #4fcf68 0%, #2ea84b 100%) !important;
    box-shadow: 0 0 0 1px oklch(0 0 0 / 0.06), 0 2px 3px rgba(20,110,40,.35), 0 12px 24px -8px rgba(40,160,70,.5),
      inset 0 1px 0 rgba(255,255,255,.4), inset 0 -2px 0 rgba(0,80,20,.18) !important;
    text-shadow: 0 1px 1px rgba(0,80,20,.35) !important; }
  #world-stage #world-play .ad-tag { box-shadow: none; }

  #world-stage #world-play:active, #world-stage .cta-row .nav:active,
  body.home-active #settings-btn.floating-icon-btn:active, html body.home-active #shop-btn.action-panel:active, body.home-active .coin-chip-home:active {
    transform: scale(0.96) !important; }
  #world-stage #world-play:active { box-shadow: 0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px rgba(190,70,20,.35), inset 0 1px 0 rgba(255,255,255,.4) !important; }
  @media (prefers-reduced-motion: reduce) {
    #world-stage #world-play, #world-stage .cta-row .nav, body.home-active #settings-btn.floating-icon-btn,
    html body.home-active #shop-btn.action-panel, body.home-active .coin-chip-home { transition: none !important; }
  }

  /* unlock progress: one compact pill under the button, not a loose bar jammed against it */
  #world-stage .unlock { width: auto !important; margin: 16px 0 0 !important; }
  #world-stage .unlock-bar { display: none !important; }
  #world-stage .unlock-line { display: inline-flex !important; align-items: center; gap: 8px; margin: 0 !important; padding: 5px 12px 5px 7px; border-radius: 99px;
    background: rgba(40,20,60,.38); color: #fff; font: 700 13.5px 'Fredoka', system-ui, sans-serif !important; text-shadow: none !important; }
  #world-stage .unlock-mini { position: relative; width: 72px; height: 7px; border-radius: 99px; background: rgba(255,255,255,.3); overflow: hidden; }
  #world-stage .unlock-mini i { position: absolute; left: 0; top: 0; bottom: 0; border-radius: 99px; background: linear-gradient(90deg, #ffe08a, #f5a311); }
  #world-stage .unlock-num { font-variant-numeric: tabular-nums; }

  /* ═══ Top bar layout — settings + Shop left, coins right ═══
     The coin chip grows with the balance; Shop pinned at a fixed offset from the
     right was covered once the number reached four digits. */
  html body.home-active #shop-btn.action-panel { left: calc(16px + 44px + 10px) !important; right: auto !important;
    top: calc(env(safe-area-inset-top, 0px) + 12px) !important; flex-direction: row !important; padding: 0 14px 0 8px !important; }
  body.home-active .coin-chip-home { max-width: calc(100vw - 16px - 44px - 10px - 110px - 16px - 10px); }

  /* ═══ Home corner: ⓘ (About / licences), styled as the settings button was ═══ */
  body.home-active #info-btn.floating-icon-btn { position: fixed !important; z-index: 100; left: 16px !important; right: auto !important;
    top: calc(env(safe-area-inset-top, 0px) + 12px) !important; width: 44px !important; height: 44px !important; border-radius: 50% !important; opacity: 1 !important; border: 0 !important;
    display: grid !important; place-items: center; padding: 0 !important; color: #3b2340 !important;
    background: linear-gradient(180deg, #ffffff 0%, #f7f4fb 100%) !important;
    box-shadow: 0 0 0 1px oklch(0 0 0 / 0.06), 0 1px 2px oklch(0 0 0 / 0.08), 0 6px 16px -4px rgba(60,35,90,.22), inset 0 1px 0 #fff !important;
    -webkit-backdrop-filter: none !important; backdrop-filter: none !important;
    transition-property: transform, box-shadow; transition-duration: .12s; transition-timing-function: cubic-bezier(.2,0,0,1); }
  body.home-active #info-btn.floating-icon-btn svg { width: 22px !important; height: 22px !important; fill: #3b2340 !important; }
  body.home-active #info-btn.floating-icon-btn:active { transform: scale(0.96) !important; }

  /* #world-stage is full-screen and sat over the top bar, swallowing real taps
     on ⓘ, Shop and coins. The layer passes taps through; its content takes them. */
  body.home-active #world-stage { pointer-events: none; }
  body.home-active #world-stage > * { pointer-events: auto; }
  /* #title-screen is also full-screen on home and sat over the fixed top bar */
  body.home-active #title-screen { pointer-events: none; }
  body.home-active #title-screen #world-stage > * { pointer-events: auto; }

  /* ═══ Home idle life ═══
     One thing moves at a time: the hero breathes continuously and quietly, the
     shelf sends one hop wave every ~5s, the next item peeks at its end, and
     Play takes a breath now and then. transform/opacity only; home only;
     nothing at all for reduced motion. */
  body.home-active #world-stage img#world-hero-img {
    transform-origin: 50% 90%; will-change: transform;
    animation: heroFloat 7s ease-in-out infinite; }
  /* two breaths, then a little wiggle — one loop, so the moves compose */
  @keyframes heroFloat {
    0%, 50%, 100% { transform: translateY(-12%) scale(1) rotate(0); }
    25%, 75%      { transform: translateY(-15%) scale(1.025) rotate(0); }
    84%           { transform: translateY(-13%) scale(1.01) rotate(-5deg); }
    88%           { transform: translateY(-13%) scale(1.01) rotate(4deg); }
    92%           { transform: translateY(-12.5%) scale(1) rotate(-2deg); }
  }

  /* the wave: each found item hops, squashes on landing, then rests */
  body.home-active #world-stage .ci.got {
    transform-origin: 50% 100%;
    animation: collPop .45s cubic-bezier(.34,1.56,.64,1) both, shelfHop 5s cubic-bezier(.3,.6,.4,1) infinite;
    animation-delay: calc(var(--i, 0) * 45ms), calc(1s + var(--i, 0) * 0.16s); }
  @keyframes shelfHop {
    0%        { transform: translateY(0) scale(1, 1); }
    4%        { transform: translateY(0) scale(1.08, .9); }
    9%        { transform: translateY(-13px) scale(.95, 1.06); }
    14%       { transform: translateY(0) scale(1.1, .88); }
    18%       { transform: translateY(0) scale(.98, 1.02); }
    22%, 100% { transform: translateY(0) scale(1, 1); }
  }
  /* the next silhouette leans in when the wave reaches it */
  body.home-active #world-stage .ci.next {
    transform-origin: 50% 100%;
    animation: collPop .45s cubic-bezier(.34,1.56,.64,1) both, nextPeek 5s ease-in-out infinite;
    animation-delay: calc(var(--i, 0) * 45ms), calc(1s + var(--i, 0) * 0.16s); }
  @keyframes nextPeek {
    0%, 6%, 30%, 100% { transform: rotate(0) translateY(0); }
    12%               { transform: rotate(-7deg) translateY(-3px); }
    19%               { transform: rotate(6deg) translateY(-2px); }
    25%               { transform: rotate(-2deg) translateY(0); }
  }
  body.home-active #world-stage .ci .cstar { animation: starTwinkle 2.6s ease-in-out infinite; }
  @keyframes starTwinkle {
    0%, 100% { transform: rotate(0) scale(1); }
    50%      { transform: rotate(14deg) scale(1.15); }
  }
  /* a tapped item hops right away */
  body.home-active #world-stage .ci.tap-hop { animation: tapHop .5s cubic-bezier(.3,.6,.4,1) both !important; }
  @keyframes tapHop {
    0%   { transform: translateY(0) scale(1, 1); }
    20%  { transform: translateY(0) scale(1.1, .88); }
    50%  { transform: translateY(-18px) scale(.94, 1.08); }
    80%  { transform: translateY(0) scale(1.08, .92); }
    100% { transform: translateY(0) scale(1, 1); }
  }
  /* Play takes a breath every few seconds */
  body.home-active #world-stage #world-play { animation: playBreath 4s ease-in-out 1.5s infinite; }
  @keyframes playBreath {
    0%, 80%, 100% { transform: scale(1); }
    88%           { transform: scale(1.04); }
    94%           { transform: scale(.99); }
  }
  body.home-active #world-stage #world-play:active { animation: none; }

  @media (prefers-reduced-motion: reduce) {
    body.home-active #world-stage img#world-hero-img, body.home-active #world-stage .ci.got, body.home-active #world-stage .ci.next,
    body.home-active #world-stage .ci .cstar, body.home-active #world-stage .ci.tap-hop, body.home-active #world-stage #world-play { animation: none !important; }
  }

  /* Share card at any width. The next-world image was sized only inside the
     ≤400px queries; wider screens showed it at 256px, the button grew to ~272px
     and the column crushed the jar snapshot into an 83px strip. */
  #share-modal .share-world-art { width: 44px; height: 44px; flex: 0 0 auto; object-fit: contain; }
  #share-modal .share-jar-card { flex-shrink: 0; min-height: min(38vh, 300px); }

  /* ═══ Tier rail markers ═══
     No ring at the end of the rail — the ★ on the last emoji marks the goal.
     "You are here" is a solid white diamond sitting on the fill. */
  #tier-strip .tier-dot.is-goal-tier:not(.next) { opacity: 0 !important; }
  #tier-strip .tier-dot.next { width: 10px !important; height: 10px !important; margin: -5px 0 0 -5px !important;
    background: #fff !important; transform: rotate(45deg); border-radius: 2px !important;
    box-shadow: 0 1px 3px rgba(60,35,90,.35) !important; }


  /* ═══ Power-ups up top, deeper jar (2026-09-27) ═══
     Order: HUD, tier strip, power-ups, jar. The power-up row is compact —
     icons only, no labels — and the HUD is a size smaller, so the jar can run
     to the bottom of the screen at its new depth (H 570). */
  #powerup-bar { order: 3 !important; margin: 4px auto 6px !important; }
  #stage { order: 4 !important; }
  #powerup-bar .powerup-label { display: none !important; }
  #powerup-bar .powerup-btn { gap: 0 !important; }
  #powerup-bar .powerup-btn .powerup-icon { width: clamp(42px, 11vw, 48px) !important; height: clamp(42px, 11vw, 48px) !important; }
  #powerup-bar .powerup-btn .powerup-count { min-width: 18px !important; height: 18px !important; font-size: 10px !important; top: -5px !important; right: -5px !important; }
  body:not(.home-active) #hud { zoom: .86; }
  #tier-strip { margin-bottom: 0 !important; }

  /* ═══ Continue card — same language as home ═══ */
  #continue-modal { background: rgba(40,22,60,.34) !important; -webkit-backdrop-filter: blur(6px) !important; backdrop-filter: blur(6px) !important; }
  #continue-modal .cont-card { background: #fff !important; border: 0 !important; border-radius: 28px !important; padding: 26px 22px 20px !important;
    width: min(320px, 86vw) !important;
    box-shadow: 0 0 0 1px oklch(0 0 0 / 0.06), 0 2px 4px oklch(0 0 0 / 0.06), 0 24px 48px -16px rgba(50,25,80,.45) !important; }
  #continue-modal .continue-title { font: 700 26px/1.1 'Fredoka', system-ui, sans-serif !important; color: #3b2340 !important; letter-spacing: -.01em; text-shadow: none !important; }
  #continue-modal .continue-desc { font: 600 14px 'Fredoka', system-ui, sans-serif !important; color: #7a6282 !important; margin-top: 4px !important; }
  #continue-modal .cont-reward { background: #f6f1fb !important; border: 0 !important; border-radius: 20px !important; box-shadow: none !important; margin: 18px 0 !important; padding: 12px 16px !important; }
  #continue-modal .cont-pct { color: #3b2340 !important; font: 700 34px/1 'Fredoka', system-ui, sans-serif !important; text-shadow: none !important; background: none !important; -webkit-text-fill-color: #3b2340 !important; }
  #continue-modal .cont-of { color: #7a6282 !important; font: 600 13.5px 'Fredoka', system-ui, sans-serif !important; }
  #continue-modal .jar-clears { fill: #ffd9c7 !important; } #continue-modal .jar-stays { fill: #f1e6f7 !important; }
  #continue-modal .jar-edge { stroke: #b49ac4 !important; } #continue-modal .jar-line { stroke: #ff6b35 !important; }
  #continue-watch-btn { width: 100% !important; height: 56px !important; border: 0 !important; border-radius: 18px !important;
    display: inline-flex !important; align-items: center; justify-content: center; gap: 10px;
    background: linear-gradient(180deg, #4fcf68 0%, #2ea84b 100%) !important; color: #fff !important;
    font: 700 21px 'Fredoka', system-ui, sans-serif !important; letter-spacing: .02em; text-shadow: 0 1px 1px rgba(0,80,20,.35) !important;
    box-shadow: 0 0 0 1px oklch(0 0 0 / 0.06), 0 2px 3px rgba(20,110,40,.35), 0 12px 24px -8px rgba(40,160,70,.5),
      inset 0 1px 0 rgba(255,255,255,.4), inset 0 -2px 0 rgba(0,80,20,.18) !important;
    transition-property: transform, box-shadow; transition-duration: .12s; transition-timing-function: cubic-bezier(.2,0,0,1); }
  #continue-watch-btn::before, #continue-watch-btn::after { display: none !important; }
  #continue-watch-btn:active { transform: scale(0.96); }
  #continue-watch-btn .ad-tag { display: inline-flex; align-items: center; gap: 3px; padding: 3px 8px 3px 6px; border-radius: 8px;
    background: #fff; color: #2a8a3c; font: 800 13px/1 'Fredoka', system-ui, sans-serif; letter-spacing: .06em; text-shadow: none; }
  #continue-watch-btn .ad-tag::before { content: ''; border-left: 8px solid #2a8a3c; border-top: 5px solid transparent; border-bottom: 5px solid transparent; }
  #continue-modal .cont-foot { margin-top: 14px !important; display: flex; align-items: center; justify-content: center; gap: 10px; }
  #continue-modal .timer-fill { stroke: #ff6b35 !important; } #continue-modal .timer-bg { stroke: #efe8f5 !important; }
  #continue-modal .timer-count { color: #3b2340 !important; font-family: 'Fredoka', system-ui, sans-serif !important; }
  #continue-skip-btn { color: #7a6282 !important; font: 600 15px 'Fredoka', system-ui, sans-serif !important; background: none !important; box-shadow: none !important; border: 0 !important; }


  /* ═══ Jar arrows ═══
     Solid white chevrons either side of the jar — the thing they change — with
     no disc behind them. A soft shadow keeps them readable on pale walls; the
     button box stays 48px so the thumb target is generous. */
  #world-stage .shelf-hero { align-items: center; gap: clamp(4px, 3vw, 18px); }
  #world-stage .shelf-hero .shelf-jar { align-self: flex-end; }
  #world-stage .jar-arrow { flex: 0 0 auto; width: 48px; height: 64px; border: 0; padding: 0; display: grid; place-items: center;
    background: none; cursor: pointer; margin-bottom: 8%;
    transition-property: transform, opacity; transition-duration: .12s; transition-timing-function: cubic-bezier(.2,0,0,1); }
  #world-stage .jar-arrow svg { stroke: #fff; filter: drop-shadow(0 2px 3px rgba(40,20,60,.35)) drop-shadow(0 0 1px rgba(40,20,60,.25)); }
  #world-stage .jar-arrow:active { transform: scale(0.96); }
  #world-stage .jar-arrow:disabled { opacity: .3; }
  /* Resume takes the whole row now */
  #world-stage .cta-row #world-play { max-width: 320px; margin: 0 auto; }
  @media (prefers-reduced-motion: reduce) { #world-stage .jar-arrow { transition: none; } }

  /* ═══ Jar arrows v2 ═══
     Big solid white arrows at the jar's middle, one each side. They live inside
     .shelf-jar so "the middle" is the jar's middle on every screen. Classes are
     .jar-arrow.prev/.next (not .nav-*) so no carousel rule can reach them. */
  #world-stage .shelf-jar { position: relative; }
  #world-stage .shelf-jar .jar-arrow { position: absolute; top: 52%; transform: translateY(-50%); z-index: 4;
    width: 56px; height: 72px; border: 0; padding: 0; margin: 0; display: grid; place-items: center; background: none !important; box-shadow: none !important; cursor: pointer;
    transition-property: transform, opacity; transition-duration: .12s; transition-timing-function: cubic-bezier(.2,0,0,1); }
  #world-stage .shelf-jar .jar-arrow.prev { left: -64px; }
  #world-stage .shelf-jar .jar-arrow.next { right: -64px; }
  #world-stage .shelf-jar .jar-arrow svg { width: 40px; height: 40px; filter: drop-shadow(0 3px 4px rgba(40,20,60,.35)) drop-shadow(0 0 1px rgba(40,20,60,.3)); }
  #world-stage .shelf-jar .jar-arrow:active { transform: translateY(-50%) scale(0.96); }
  #world-stage .shelf-jar .jar-arrow:disabled { opacity: .3; }
  @media (max-width: 360px) { #world-stage .shelf-jar .jar-arrow.prev { left: -52px; } #world-stage .shelf-jar .jar-arrow.next { right: -52px; } }
  /* bigger arrows */
  #world-stage .shelf-jar .jar-arrow { width: 60px; height: 80px; }
  #world-stage .shelf-jar .jar-arrow svg { width: 54px; height: 54px; }
  #world-stage .shelf-jar .jar-arrow.prev { left: -66px; }
  #world-stage .shelf-jar .jar-arrow.next { right: -66px; }
  @media (max-width: 360px) { #world-stage .shelf-jar .jar-arrow svg { width: 46px; height: 46px; }
    #world-stage .shelf-jar .jar-arrow.prev { left: -56px; } #world-stage .shelf-jar .jar-arrow.next { right: -56px; } }

  /* ═══ Run HUD: names on the power-ups, no tier caption (2026-09-27) ═══
     The caption under the rail ("ALMOST THERE …", "ONE MORE MERGE!") was one
     more line of text to read mid-run; the rail and its diamond already say it.
     Its type moves to the power-ups, which had no names at all. */
  #powerup-bar .powerup-btn { gap: 4px !important; }
  #powerup-bar .powerup-label { display: block !important;
    font-family: 'Fredoka', -apple-system, system-ui, sans-serif;
    font-size: clamp(9px, 1.15vh, 10.5px); font-weight: 600; letter-spacing: 0.5px;
    text-transform: uppercase; line-height: 1; color: #fff;
    text-shadow:
      0 1px 0 rgba(54, 36, 74, 0.50), 0 -1px 0 rgba(54, 36, 74, 0.34),
      1px 0 0 rgba(54, 36, 74, 0.34), -1px 0 0 rgba(54, 36, 74, 0.34),
      0 2px 7px rgba(54, 36, 74, 0.46); }

  /* ═══ Power-ups as soap bubbles (2026-09-27) ═══
     A see-through sphere instead of a white tile: it takes the world's colour
     through it, so four of them stop reading as four heavy boxes. Longhands
     only — the icon art lives on child elements, never the tile's background. */
  #powerup-bar .powerup-btn .powerup-icon {
    border-radius: 50% !important;
    border: 0 !important;
    background-color: rgba(255, 255, 255, 0.16) !important;
    background-image: radial-gradient(circle at 32% 26%,
      rgba(255, 255, 255, 0.95) 0 7%, rgba(255, 255, 255, 0.42) 20%,
      rgba(255, 255, 255, 0.14) 58%, rgba(255, 255, 255, 0.55) 100%) !important;
    background-size: 100% 100% !important;
    box-shadow:
      inset 0 0 0 1.5px rgba(255, 255, 255, 0.92),
      inset -6px -8px 14px rgba(150, 110, 220, 0.22),
      0 8px 16px -8px rgba(60, 35, 100, 0.40) !important;
  }

  /* ═══ Rail without a marker (2026-09-27) ═══
     No diamond, no ring: where the white fill ends is where you are. */
  /* ×2 sits on the bubble's rim, clear of the name below it. */
  #powerup-bar .powerup-btn.twin .powerup-icon::after { bottom: 2px !important; right: -8px !important; }

  /* ═══ Fill icon = this world's smallest pieces + the count ═══ */
  #powerup-bar .fill-art { position: relative; width: 74%; height: 74%; display: block; pointer-events: none; }
  #powerup-bar .fill-piece {
    position: absolute; display: grid; place-items: center;
    font-style: normal; font-size: 13px; line-height: 1;
    background-size: contain; background-repeat: no-repeat; background-position: center;
    filter: drop-shadow(0 1.5px 2px rgba(60, 35, 90, 0.30));
  }
  #powerup-bar .fill-piece.fp0 { width: 46%; height: 46%; left: 0;   top: 2%;  transform: rotate(-18deg); }
  #powerup-bar .fill-piece.fp1 { width: 48%; height: 48%; right: 0;  top: 10%; transform: rotate(12deg); }
  #powerup-bar .fill-piece.fp2 { width: 50%; height: 50%; left: 20%; bottom: 0; transform: rotate(8deg); }
  #powerup-bar .fill-n {
    position: absolute; right: -10%; bottom: -8%;
    font: 700 11px/1 'Fredoka', -apple-system, system-ui, sans-serif; color: #fff;
    text-shadow: 0 1px 0 rgba(54,36,74,.55), 0 -1px 0 rgba(54,36,74,.35), 1px 0 0 rgba(54,36,74,.35), -1px 0 0 rgba(54,36,74,.35);
  }

  /* ═══ Run screen order (2026-09-27) ═══
     HUD, power-ups, jar, and the collection rail under the jar. */
  #hud { order: 1 !important; }
  #powerup-bar { order: 2 !important; margin: 2px auto 6px !important; }
  /* margin-top was parking tall-screen spare height between the HUD and the
     jar; with the power-ups right above the jar that read as a hole. */
  #stage { order: 3 !important; margin-top: 0 !important; }
  #tier-strip { order: 4 !important; margin: 0 !important; }

  /* ═══ Themes + Settings: same glossy glass as the power-up bubbles ═══ */
  #hud #home-btn {
    border: 0 !important;
    background-color: rgba(255, 255, 255, 0.16) !important;
    background-image: radial-gradient(circle at 32% 26%,
      rgba(255, 255, 255, 0.85) 0 5%, rgba(255, 255, 255, 0.36) 22%,
      rgba(255, 255, 255, 0.12) 60%, rgba(255, 255, 255, 0.45) 100%) !important;
    box-shadow:
      inset 0 0 0 1.5px rgba(255, 255, 255, 0.92),
      inset -6px -8px 14px rgba(150, 110, 220, 0.22),
      0 8px 16px -8px rgba(60, 35, 100, 0.40) !important;
    backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
  }
  #settings-btn {
    border: 0 !important;
    border-radius: 50% !important;
    background-color: rgba(255, 255, 255, 0.16) !important;
    background-image: radial-gradient(circle at 32% 26%,
      rgba(255, 255, 255, 0.95) 0 7%, rgba(255, 255, 255, 0.42) 20%,
      rgba(255, 255, 255, 0.14) 58%, rgba(255, 255, 255, 0.55) 100%) !important;
    box-shadow:
      inset 0 0 0 1.5px rgba(255, 255, 255, 0.92),
      inset -4px -6px 10px rgba(150, 110, 220, 0.22),
      0 6px 12px -6px rgba(60, 35, 100, 0.40) !important;
    backdrop-filter: none !important; -webkit-backdrop-filter: none !important;
  }

  /* ═══ L2 run layout (2026-09-27) ═══
     The tier rail leaves the run screen; a goal ring in the HUD takes its job.
     The gear joins the HUD row instead of owning a row of its own, and every
     pixel saved goes to the jar (world H 740). --vert-reserved is everything
     on the run screen that is not the jar, so short phones size the column
     right. */
  body:not(.home-active) { --vert-reserved: 240px;
    padding-top: calc(env(safe-area-inset-top, 0px) + 8px) !important;
    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 14px) !important; }
  body:not(.home-active) #wrap { gap: 10px !important; }
  /* The HUD takes the phone's width, not the jar's: on a short phone the jar
     column narrows and would squeeze the goal ring off the screen. */
  body:not(.home-active) #hud { padding-left: 40px !important; align-items: center !important;
    align-self: center; box-sizing: border-box; gap: 6px !important;
    /* #hud carries zoom .86 on this screen; the width is divided by it so the
       rendered box is the phone's width. */
    width: min(calc((100vw - 16px) / 0.86), calc(420px / 0.86)); }
  body:not(.home-active) #hud > .score-panel { min-width: 104px !important; }
  body:not(.home-active) #settings-btn { top: calc(env(safe-area-inset-top, 0px) + 22px) !important; left: 8px !important; }
  body.home-active #goal-panel { display: none; }

  #goal-panel { position: relative; flex: 0 0 auto; align-self: center;
    display: flex; flex-direction: column; align-items: center; gap: 2px; }
  #goal-panel .goal-ring { position: relative; width: 54px; height: 54px; }
  #goal-panel .goal-svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
  #goal-panel .goal-track { fill: none; stroke: rgba(255, 255, 255, 0.32); stroke-width: 4; }
  #goal-panel .goal-fill { fill: none; stroke: #fff; stroke-width: 4; stroke-linecap: round;
    stroke-dasharray: 100; stroke-dashoffset: 100;
    filter: drop-shadow(0 1px 1px rgba(54, 36, 74, 0.35));
    transition: stroke-dashoffset 0.5s cubic-bezier(.2, 0, 0, 1); }
  /* Same soap bubble as the power-ups. */
  #goal-panel .goal-bubble { position: absolute; inset: 7px; border-radius: 50%; display: grid; place-items: center;
    background-color: rgba(255, 255, 255, 0.16);
    background-image: radial-gradient(circle at 32% 26%,
      rgba(255, 255, 255, 0.95) 0 7%, rgba(255, 255, 255, 0.42) 20%,
      rgba(255, 255, 255, 0.14) 58%, rgba(255, 255, 255, 0.55) 100%);
    box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, 0.92), inset -4px -6px 10px rgba(150, 110, 220, 0.22),
      0 6px 12px -6px rgba(60, 35, 100, 0.40); }
  #goal-panel .goal-piece { grid-area: 1 / 1; width: 64%; height: 64%; display: grid; place-items: center;
    font-style: normal; font-size: 18px; line-height: 1;
    background-size: contain; background-repeat: no-repeat; background-position: center;
    /* A silhouette: the shape is the clue, not the answer. */
    filter: brightness(0) invert(1) opacity(0.6) drop-shadow(0 0 1px rgba(54, 36, 74, 0.55));
    animation: goalBob 1.8s ease-in-out infinite; }
  #goal-panel .goal-q { grid-area: 1 / 1; font: 700 15px/1 'Fredoka', -apple-system, system-ui, sans-serif; color: #fff;
    text-shadow: 0 1px 3px rgba(54, 36, 74, 0.65); pointer-events: none; }
  @keyframes goalBob {
    50% { transform: translateY(-2px) scale(1.05); }
  }
  #goal-panel .goal-stars i { position: absolute; width: 13px; height: 13px; margin: -6.5px 0 0 -6.5px;
    background: url(../assets/fluentui-emoji/Star/3D/star_3d.webp) center / contain no-repeat;
    filter: grayscale(1) brightness(1.5) opacity(0.7); }
  #goal-panel .goal-stars i.on { filter: drop-shadow(0 1px 1px rgba(54, 36, 74, 0.4)); }
  #goal-panel .goal-hint { display: flex; align-items: center; gap: 1px; height: 14px; }
  #goal-panel .goal-hint i { width: 14px; height: 14px; display: grid; place-items: center; font-style: normal; font-size: 11px;
    background-size: contain; background-repeat: no-repeat; background-position: center;
    filter: drop-shadow(0 1px 1px rgba(54, 36, 74, 0.35)); }
  #goal-panel .goal-hint b { font: 700 10px/1 'Fredoka', -apple-system, system-ui, sans-serif; color: #fff;
    text-shadow: 0 1px 2px rgba(54, 36, 74, 0.6); }
  #goal-panel.no-recipe .goal-hint { visibility: hidden; }
  /* One merge away: the ring warms and breathes. No words needed. */
  #goal-panel.is-imminent .goal-fill { stroke: #ffd24a; }
  /* The glow is its own layer and only its opacity moves — compositor-only. */
  #goal-panel .goal-ring::after { content: ''; position: absolute; inset: 2px; border-radius: 50%; pointer-events: none;
    box-shadow: 0 0 0 4px rgba(255, 210, 74, 0.55), 0 0 14px 4px rgba(255, 210, 74, 0.45); opacity: 0; }
  #goal-panel.is-imminent .goal-ring::after { animation: goalHot 0.9s ease-in-out infinite; }
  @keyframes goalHot {
    50% { opacity: 1; }
  }
  /* Done: the world's biggest piece, in colour. */
  #goal-panel.is-done .goal-piece { filter: drop-shadow(0 2px 2px rgba(54, 36, 74, 0.35)); animation: none; }
  #goal-panel.is-done .goal-q, #goal-panel.is-done .goal-hint { visibility: hidden; }
  @media (prefers-reduced-motion: reduce) {
    #goal-panel .goal-piece { animation: none; }
    #goal-panel.is-imminent .goal-ring::after { animation: none; opacity: 1; }
  }

  /* ═══ Run HUD row (2026-09-27) ═══
     left | score | right, with the two side columns equal so the score sits
     dead centre whatever is either side of it. Left: gear (absolute) + home.
     Right: NEXT + goal ring. Home is a small round bubble, white house only. */
  .home-glyph { display: none; }
  body:not(.home-active) #hud {
    display: grid !important; grid-template-columns: 1fr auto 1fr; align-items: center !important;
    padding-left: 0 !important; column-gap: 6px; }
  body:not(.home-active) #hud > * { grid-row: 1; margin-top: 0 !important; align-self: center !important; }
  /* 56 css px = 48 rendered (zoom .86): the gear (40) + 8 gap, so the gear's
     left edge lands on the jar's, mirroring the goal ring on the right. */
  body:not(.home-active) #hud > #home-btn { grid-column: 1; justify-self: start; margin-left: 56px !important; }
  body:not(.home-active) #hud > .score-panel { grid-column: 2; }
  body:not(.home-active) #hud > .next-panel { grid-column: 3; justify-self: end; margin-right: 0 !important; }
  body:not(.home-active) #hud > #goal-panel { grid-column: 3; justify-self: end; }
  body:not(.home-active) #hud #home-btn {
    width: 40px !important; height: 40px !important; min-width: 0 !important; padding: 0 !important;
    border-radius: 50% !important; display: grid !important; place-items: center; }
  body:not(.home-active) #hud #home-btn .action-icon,
  body:not(.home-active) #hud #home-btn .hud-badge,
  body:not(.home-active) #hud #home-btn .label { display: none !important; }
  body:not(.home-active) #hud #home-btn .home-glyph { display: block; width: 20px; height: 20px; fill: #fff;
    filter: drop-shadow(0 1px 1.5px rgba(54, 36, 74, 0.45)); }

  /* The run-row grid above is `display: grid !important` at the same
     specificity as the share screen's hide rule, so the hide is re-stated
     after it — the share card must not have the HUD painted over its jar. */
  body.share-active #hud,
  body.share-active .floating-icon-btn { display: none !important; }

  /* ═══ Gear matches home (2026-09-27) ═══
     (A cloud-less "minimal score" was tried here and reverted on review.) */
  body:not(.home-active) #settings-btn { width: 40px !important; height: 40px !important; }
  body:not(.home-active) #settings-btn svg { width: 18px !important; height: 18px !important; fill: #fff !important; }

  /* The jar is deeper now (fitted H), so the share card is capped lower or it
     pushes the Share button off a 390x844 screen. */
  #share-modal .share-jar-card,
  #share-modal .share-jar-img { max-height: min(50vh, 470px) !important; }
  /* #hud is zoomed .86 on this screen: 46px renders as 40, the gear's size. */
  body:not(.home-active) #hud #home-btn { width: 46px !important; height: 46px !important; }
  body:not(.home-active) #hud #home-btn .home-glyph { width: 22px; height: 22px; }

  /* ═══ Rail back, ring parked (2026-09-28) ═══
     The tier rail with its caption and diamond returns under the jar; the goal
     ring is hidden (kept in the markup so it can come back). */
  body:not(.home-active) #goal-panel { display: none !important; }

  /* ═══ Arrows kept on screen (QA 2026-09-28) ═══
     The jar is centred, so (100% - 100vw) / 2 is minus the gap between the
     jar and the screen edge: the arrow sits 66px out, or 4px from the edge,
     whichever is further in. Replaces the per-width media queries above. */
  #world-stage .shelf-jar .jar-arrow.prev { left: max(-66px, calc((100% - 100vw) / 2 + 4px)) !important; }
  #world-stage .shelf-jar .jar-arrow.next { right: max(-66px, calc((100% - 100vw) / 2 + 4px)) !important; }

  /* ═══ Overlays never crop (QA 2026-09-28) ═══
     In-run modals are inset:0 inside the jar, and on a small phone the jar is
     small (320x568 -> 271x335). Content that does not fit scrolls inside the
     overlay instead of being cut, and `safe center` keeps its top reachable. */
  .overlay { overflow-y: auto; overscroll-behavior: contain; justify-content: safe center; }
  @media (max-height: 700px) {
    #settings-modal { padding: 12px 16px; }
    #settings-modal .settings-icon { font-size: 30px; margin-bottom: 2px; }
    #settings-modal .settings-title { font-size: 20px; margin-bottom: 10px; }
    #settings-modal .settings-rows { gap: 6px; margin-bottom: 10px; }
    #settings-modal .settings-row { padding: 6px 12px; }
  }

  /* ═══ Keep-playing card fits (QA 2026-09-28) ═══
     On a 320x568 phone the card was taller than the jar it sits in, which put
     "End run" under the fold. The decline must always be on screen. */
  @media (max-height: 700px) {
    #continue-modal { padding: 10px !important; }
    #continue-modal .cont-card { padding: 16px 16px 12px !important; }
    #continue-modal .continue-title { font-size: 22px !important; }
    #continue-modal .cont-reward { margin: 10px 0 !important; padding: 8px 12px !important; }
    #continue-modal .cont-pct { font-size: 28px !important; }
    #continue-modal .cont-jar-svg { width: 34px; height: 44px; }
    #continue-watch-btn { height: 50px !important; }
  }

  /* ═══ Scene plates (2026-09-28) ═══
     js/scenes.js draws the world behind the jar on the run screen; on those
     worlds the palette gradient's blur orbs step aside and the jar's glass
     thins so the world shows through. Home keeps its own wash. */
  #scene { position: fixed; inset: 0; z-index: -1; pointer-events: none; overflow: hidden; display: none; }
  html.has-scene #scene { display: block; }
  body.home-active #scene { display: none; }
  #scene svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
  #scene::after { content: ''; position: absolute; inset: 0; opacity: 0.9;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)' opacity='0.05'/%3E%3C/svg%3E") repeat;
    background-size: 160px 160px; }
  html.has-scene:not(.home-active) body::before,
  html.has-scene:not(.home-active) body::after { display: none; }
  .sc-p { position: absolute; border-radius: 50%; opacity: 0; will-change: transform, opacity;
    animation: scFall var(--d, 12s) linear var(--dl, 0s) infinite; }
  .sc-p.snow { background: #fff; box-shadow: 0 0 2px rgba(120, 150, 190, 0.5); }
  @keyframes scFall {
    0%   { transform: translate3d(0, -10px, 0); opacity: 0; }
    12%  { opacity: 0.95; }
    85%  { opacity: 0.85; }
    100% { transform: translate3d(16px, var(--fall, 200px), 0); opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) { .sc-p { animation: none; display: none; } }

  /* Ambient kinds (paintScene). Transform/opacity only, slow, edges only. */
  .sc-p.bubble { border: 1.5px solid rgba(255, 255, 255, 0.85); background: rgba(255, 255, 255, 0.12);
    animation-name: scRise; }
  @keyframes scRise {
    0%   { transform: translate3d(0, 0, 0); opacity: 0; }
    15%  { opacity: 0.85; }
    100% { transform: translate3d(0, calc(-1 * var(--fall, 200px)), 0); opacity: 0; }
  }
  .sc-p.fish { left: -40px; width: 26px; height: 12px; border-radius: 0; opacity: 0.28;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 26 12'%3E%3Cpath d='M0 6 Q8 0 18 6 Q8 12 0 6Z M18 6 L26 1 L26 11Z' fill='%231e6e78'/%3E%3C/svg%3E") center / contain no-repeat;
    animation: scSwim var(--d, 30s) linear var(--dl, 0s) infinite; }
  @keyframes scSwim {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(var(--span, 460px), 0, 0); }
  }
  .sc-p.firefly { width: 5px; height: 5px; background: #fff6b0; box-shadow: 0 0 6px 2px rgba(255, 240, 150, 0.6);
    animation: scGlow var(--d, 2.8s) ease-in-out var(--dl, 0s) infinite; }
  @keyframes scGlow {
    0%, 100% { opacity: 0.15; }
    50% { opacity: 0.95; }
  }
  .sc-p.butterfly { width: 16px; height: 12px; border-radius: 0; opacity: 0.9;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 12'%3E%3Cg fill='%23ffd36e'%3E%3Cellipse cx='4' cy='4' rx='4' ry='4'/%3E%3Cellipse cx='12' cy='4' rx='4' ry='4'/%3E%3Cellipse cx='5' cy='9' rx='3' ry='3'/%3E%3Cellipse cx='11' cy='9' rx='3' ry='3'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
    animation: scFlit var(--d, 6s) ease-in-out infinite; }
  .sc-p.butterfly.b2 { filter: hue-rotate(0deg) brightness(1.4) saturate(0); }
  @keyframes scFlit {
    0%, 100% { transform: translate3d(0, 0, 0); }
    25% { transform: translate3d(14px, -10px, 0); }
    50% { transform: translate3d(4px, -22px, 0); }
    75% { transform: translate3d(-10px, -8px, 0); }
  }
  .sc-p.shoot { width: 60px; height: 2px; border-radius: 1px; background: rgba(255, 255, 255, 0.85);
    animation: scShoot 9s linear infinite; }
  @keyframes scShoot {
    0%, 86% { transform: translate3d(0, 0, 0); opacity: 0; }
    88% { opacity: 1; }
    100% { transform: translate3d(-160px, 90px, 0); opacity: 0; }
  }
  #scene .sc-tw { animation: scTwinkle 4s ease-in-out infinite; }
  @keyframes scTwinkle {
    50% { opacity: 0.25; }
  }
  @media (prefers-reduced-motion: reduce) { #scene .sc-tw { animation: none; } }

  /* More ambient kinds for the kit plates. Same rules: transform/opacity only. */
  .sc-p.sparkle { width: 10px; height: 10px; border-radius: 0; background: none;
    animation: scGlow var(--d, 2.8s) ease-in-out var(--dl, 0s) infinite; }
  .sc-p.sparkle::before { content: ''; position: absolute; inset: 0; background: #fff;
    clip-path: polygon(50% 0, 60% 40%, 100% 50%, 60% 60%, 50% 100%, 40% 60%, 0 50%, 40% 40%); }
  .sc-p.heart { width: 11px; height: 10px; border-radius: 0; background: none;
    animation: scRise calc(var(--d, 3s) * 4) linear var(--dl, 0s) infinite; }
  .sc-p.heart::before { content: ''; position: absolute; inset: 0; background: #fff;
    clip-path: path('M5.5 10 C-2 4 1.5 -2 5.5 2 C9.5 -2 13 4 5.5 10Z'); }
  .sc-p.leaf { width: 10px; height: 6px; border-radius: 0 80% 0 80%; background: #e8a04a;
    animation: scFall var(--d, 14s) linear var(--dl, 0s) infinite; }
  .sc-p.petal { width: 8px; height: 6px; border-radius: 60% 40%; background: #ffc2d4;
    animation: scFall var(--d, 14s) linear var(--dl, 0s) infinite; }
  .sc-p.steam { width: 14px; height: 14px; background: rgba(255, 255, 255, 0.7);
    animation: scSteam var(--d, 5s) ease-out var(--dl, 0s) infinite; }
  @keyframes scSteam {
    0%   { transform: translate3d(0, 0, 0) scale(0.8); opacity: 0; }
    25%  { opacity: 0.5; }
    100% { transform: translate3d(8px, -90px, 0) scale(1.4); opacity: 0; }
  }
  .sc-p.bird, .sc-p.bat { left: -40px; width: 18px; height: 8px; border-radius: 0; opacity: 0.5;
    background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 18 8'%3E%3Cpath d='M0 6 q4 -6 9 0 q5 -6 9 0' stroke='%233c3c5a' stroke-width='2' fill='none'/%3E%3C/svg%3E") center / contain no-repeat;
    animation: scSwim var(--d, 26s) linear var(--dl, 0s) infinite; }
  .sc-p.bat { width: 20px; height: 9px; opacity: 0.6;
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 9'%3E%3Cpath d='M0 3 q5 -3 7 2 q3 -4 6 0 q2 -5 7 -2 q-4 1 -6 5 q-4 -2 -8 0 q-2 -4 -6 -5z' fill='%23140a28'/%3E%3C/svg%3E"); }




  .sc-p.confetti { width: 7px; height: 4px; border-radius: 1px; background: #ff9f8a;
    animation: scFall var(--d, 13s) linear var(--dl, 0s) infinite; }
  .sc-p.confetti:nth-of-type(2n) { background: #ffd37a; }
  .sc-p.confetti:nth-of-type(3n) { background: #8fc4f0; }
  .sc-p.dust { width: 3px; height: 3px; background: rgba(255, 255, 255, 0.85);
    animation: scGlow var(--d, 4s) ease-in-out var(--dl, 0s) infinite; }

  /* ═══ Thinner frost over scenes (phone review #4) ═══
     Same jar art (shine, stripe, rim are untouched); the frost behind the
     pile is halved so the jar reads as clear glass over a scene, not cloud. */
  html.has-scene:not(.home-active) #stage {
    background: linear-gradient(180deg,
      rgba(255,255,255,0.30) 0%,
      rgba(255,255,255,0.14) 14%,
      rgba(255,255,255,0.10) 50%,
      rgba(255,255,255,0.10) 100%) !important;
  }

  .sc-p.rain { width: 1.5px; height: 12px; border-radius: 1px; background: rgba(255, 255, 255, 0.75);
    animation: scRain var(--d, 1.5s) linear var(--dl, 0s) infinite; }
  @keyframes scRain {
    0%   { transform: translate3d(0, -20px, 0) rotate(12deg); opacity: 0; }
    10%  { opacity: 0.7; }
    100% { transform: translate3d(-16px, var(--fall, 240px), 0) rotate(12deg); opacity: 0; }
  }

  /* ═══ Smoky HUD on scenes ═══
     White soap-bubbles vanished into light scene skies. On scene worlds the
     round buttons sit on smoky dark glass (white rim + highlight kept), which
     reads on light and dark scenes alike; the score cloud goes near-solid. */
  html.has-scene:not(.home-active) #powerup-bar .powerup-btn .powerup-icon,
  html.has-scene:not(.home-active) #settings-btn,
  html.has-scene:not(.home-active) #hud #home-btn {
    background-color: rgba(28, 20, 48, 0.32) !important;
    background-image: radial-gradient(circle at 32% 24%,
      rgba(255, 255, 255, 0.55) 0 6%, rgba(255, 255, 255, 0.12) 22%, rgba(255, 255, 255, 0) 55%) !important;
    box-shadow:
      inset 0 0 0 1.5px rgba(255, 255, 255, 0.75),
      inset 0 -6px 12px rgba(0, 0, 0, 0.12),
      0 6px 14px -6px rgba(20, 10, 40, 0.45) !important;
  }
  html.has-scene:not(.home-active) #hud .score-panel { background: rgba(255, 255, 255, 0.92); }

  /* Share screen on scene worlds: the scene-glass rule outranked the share
     flatten, leaving a ghost jar behind the card. */
  html.has-scene body.share-active #stage {
    background: transparent !important; box-shadow: none !important; border: none !important;
  }
  body.share-active #scene .sc-contact { display: none; }

  /* Rail caption on scene worlds: ink chosen against the scene's ground. */
  html.has-scene .tier-progress .cap-pre,
  html.has-scene .tier-progress .cap-name,
  html.has-scene .tier-progress .cap-pre.solo {
    color: var(--cap-ink) !important; text-shadow: var(--cap-halo) !important; }

  /* ═══ Tighter top (S23 Ultra review) ═══
     8px of body padding plus the score cloud's own bump room left ~40px of
     nothing above the HUD. The column is fitted from measured chrome, so the
     jar takes the freed height automatically. */
  body:not(.home-active) { padding-top: calc(env(safe-area-inset-top, 0px) + 2px) !important; }
  body:not(.home-active) #wrap > #hud { margin-top: -10px; }

  /* ═══ Juicy glass over scenes (phone review #5) ═══
     The frost read milky. Clear glass instead: almost no fill, light that
     hugs the rim (negative spread keeps it at the edge), a floor thickness,
     a small crisp highlight top-left and the reflection stripe. */
  html.has-scene:not(.home-active) #stage {
    background: linear-gradient(180deg,
      rgba(255,255,255,0.12) 0%,
      rgba(255,255,255,0.04) 12%,
      rgba(255,255,255,0.03) 100%) !important;
    border: 1.5px solid rgba(255,255,255,0.85) !important;
    box-shadow:
      inset 0 2px 0 rgba(255,255,255,0.95),
      inset 6px 0 8px -6px rgba(255,255,255,0.85),
      inset -6px 0 8px -6px rgba(255,255,255,0.7),
      inset 0 -10px 12px -8px rgba(255,255,255,0.65),
      0 22px 40px -18px rgba(40,50,90,0.35),
      0 6px 12px rgba(40,50,90,0.08) !important;
  }
  /* The top-left shine blob read as a cloudy patch — gone on scene worlds. */
  html.has-scene:not(.home-active) #stage::before { display: none; }
  html.has-scene:not(.home-active) #stage::after { opacity: 1; filter: blur(1.5px); }

  /* ═══ Scene actors (living backgrounds) ═══
     Moving props drawn by paintScene as their own small elements. Transform
     and opacity only, so they run on the compositor. */
  .sc-a { position: absolute; pointer-events: none; will-change: transform; }
  .sc-a svg { display: block; overflow: visible; }
  .sc-a.drift { animation: scaDrift var(--d, 40s) ease-in-out var(--dl, 0s) infinite alternate; }
  .sc-a.bob   { animation: scaBob var(--d, 4s) ease-in-out var(--dl, 0s) infinite alternate; }
  .sc-a.spin  { animation: scaSpin var(--d, 36s) linear var(--dl, 0s) infinite; }
  .sc-a.sway  { transform-origin: 50% 100%; animation: scaSway var(--d, 5s) ease-in-out var(--dl, 0s) infinite alternate; }
  .sc-a.hop   { transform-origin: 50% 100%; animation: scaHop var(--d, 5s) ease-in-out var(--dl, 0s) infinite; }
  .sc-a.swim  { animation: scaSwim var(--d, 24s) linear var(--dl, 0s) infinite; }
  .sc-a.swing { transform-origin: 50% 0; animation: scaSwing var(--d, 3.6s) ease-in-out var(--dl, 0s) infinite alternate; }
  .sc-a.blink { animation: scaBlink var(--d, 1.6s) ease-in-out var(--dl, 0s) infinite alternate; }
  .sc-a.flit  { animation: scaFlit var(--d, 7s) ease-in-out var(--dl, 0s) infinite; }
  .sc-a.steam { animation: scaSteam var(--d, 3.5s) ease-out var(--dl, 0s) infinite; }
  @keyframes scaDrift {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(var(--dx, 24px), 0, 0); }
  }
  @keyframes scaBob {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(0, -9px, 0); }
  }
  @keyframes scaSpin {
    from { transform: rotate(0deg); }
    to   { transform: rotate(360deg); }
  }
  @keyframes scaSway {
    from { transform: rotate(-3.5deg); }
    to   { transform: rotate(3.5deg); }
  }
  @keyframes scaHop {
    0%, 70%, 100% { transform: translate3d(0, 0, 0) rotate(0deg); }
    76% { transform: translate3d(0, -6px, 0) rotate(-8deg); }
    82% { transform: translate3d(0, 0, 0) rotate(0deg); }
    88% { transform: translate3d(0, 0, 0) rotate(7deg); }
    94% { transform: translate3d(0, 0, 0) rotate(0deg); }
  }
  @keyframes scaSwim {
    from { transform: translate3d(0, 0, 0); }
    to   { transform: translate3d(var(--span, 460px), 0, 0); }
  }
  @keyframes scaSwing {
    from { transform: rotate(-4deg); }
    to   { transform: rotate(4deg); }
  }
  @keyframes scaBlink {
    from { opacity: 0.3; }
    to   { opacity: 1; }
  }
  @keyframes scaFlit {
    0%, 100% { transform: translate3d(0, 0, 0); }
    25% { transform: translate3d(40px, -18px, 0); }
    50% { transform: translate3d(70px, 6px, 0); }
    75% { transform: translate3d(24px, 16px, 0); }
  }
  @keyframes scaSteam {
    0%   { transform: translate3d(0, 0, 0) scale(0.8); opacity: 0; }
    30%  { opacity: 0.8; }
    100% { transform: translate3d(6px, -40px, 0) scale(1.5); opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) { .sc-a { animation: none !important; } }

  /* ═══ Home V1 (2026-09-28, "Home screens V1" / home5.html, all worlds) ═══
     The home is a room: the viewed world's own scene fills the screen, its
     floor line on the jar's base, so no wooden shelves and no wallpaper. White
     surfaces carry text (collection card, label, goal pill); round buttons are
     smoky glass; PLAY is the one coral pill. */
  body.home-active #scene { display: block; }
  body.home-active #scene .sc-contact { display: none; }
  body.home-active .wash-layer { opacity: 0 !important; }
  body.home-active #world-stage::before { display: none !important; }
  body.home-active #world-stage { padding-top: calc(env(safe-area-inset-top, 0px) + 66px) !important; }

  /* header: name, collection card, count */
  #world-stage .shelf-back { display: flex; flex-direction: column; align-items: center; }
  #world-stage .shelf-back::before { content: 'Squishy Merge'; text-transform: uppercase; letter-spacing: .32em;
    font: 700 12px/1 'Fredoka', system-ui, sans-serif; color: var(--home-ink, #3b2340); margin: 4px 0 10px; }
  #world-stage .coll { background: rgba(255, 255, 255, 0.82); border-radius: 24px; height: 68px !important; width: min(88vw, 360px);
    padding: 0 12px !important; margin: 0 !important; align-items: center !important;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .9) inset, 0 14px 30px -18px rgba(40, 25, 60, .45); }
  #world-stage .ci { width: 40px !important; height: 40px !important; place-items: center !important; }
  #world-stage .ci.next { width: 46px !important; height: 46px !important; }
  #world-stage .ci.unk img { filter: brightness(0) opacity(.14) !important; }
  #world-stage .shelf-back .plank { height: auto !important; background: none !important; box-shadow: none !important; display: flex; justify-content: center; }
  #world-stage .coll-tag { position: static !important; transform: none !important; margin-top: 8px; padding: 0 !important; background: none !important; box-shadow: none !important;
    font: 700 10.5px 'Fredoka', system-ui, sans-serif !important; letter-spacing: .16em; text-transform: uppercase; color: var(--home-ink, #3b2340) !important; opacity: .7; }

  /* hero: clear jar on the floor line, flat pastel lid, white label */
  #world-stage .plank-front { display: none !important; }
  #world-stage .shelf-jar { margin-bottom: 0 !important; }
  #world-stage .shelf-jar::after { content: ''; position: absolute; left: 6%; right: 6%; bottom: -10px; height: 20px; z-index: -1; border-radius: 50%;
    background: radial-gradient(ellipse at center, rgba(30, 20, 50, .28), rgba(30, 20, 50, 0) 70%); }
  #world-stage .jar-lid { width: 70% !important; border-radius: 12px 12px 8px 8px !important;
    background: linear-gradient(180deg, var(--lid1, #bfe3f5), var(--lid1, #bfe3f5)) !important;
    box-shadow: inset 0 2px 0 rgba(255, 255, 255, .6), inset 0 -3px 0 rgba(0, 0, 0, .06), 0 3px 6px -3px rgba(40, 25, 60, .3) !important; }
  #world-stage .jar-glass { background: linear-gradient(90deg, rgba(255,255,255,.5) 0%, rgba(255,255,255,.14) 14%, rgba(255,255,255,.1) 50%, rgba(255,255,255,.16) 86%, rgba(255,255,255,.45) 100%),
      rgba(255, 255, 255, .22) !important;
    border: 2px solid rgba(255, 255, 255, .9) !important; }
  #world-stage .jar-label { transform: translateX(-50%) !important; border-radius: 14px !important; padding: 7px 18px 8px !important;
    background: rgba(255, 255, 255, .94) !important; color: var(--score-ink, #3b2340) !important;
    box-shadow: 0 10px 20px -12px rgba(40, 25, 60, .45) !important; }
  #world-stage .jar-label .world-name { color: var(--score-ink, #3b2340) !important; }   /* white label: always the deep ink */
  #world-stage .jar-num { letter-spacing: .18em !important; }

  /* smoky round arrows at the jar's sides */
  #world-stage .shelf-jar .jar-arrow { width: 44px !important; height: 44px !important; border-radius: 50%; background: rgba(28, 20, 48, 0.3) !important;
    box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .8), 0 6px 14px -6px rgba(20, 10, 40, .45) !important; }
  #world-stage .shelf-jar .jar-arrow.prev { left: -54px !important; }
  #world-stage .shelf-jar .jar-arrow.next { right: -54px !important; }
  #world-stage .shelf-jar .jar-arrow svg { display: none; }
  #world-stage .shelf-jar .jar-arrow::before { content: ''; width: 10px; height: 10px; border: solid #fff; border-width: 0 0 3px 3px; border-radius: 2px; }
  #world-stage .shelf-jar .jar-arrow.prev::before { transform: translateX(2px) rotate(45deg); }
  #world-stage .shelf-jar .jar-arrow.next::before { transform: translateX(-2px) rotate(-135deg); }

  /* bottom: goal pill, coral PLAY, pager */
  #world-stage .world-goal { margin-top: 22px !important; min-height: 36px !important; padding: 4px 14px !important;
    background: rgba(255, 255, 255, .94) !important; font: 600 14px 'Fredoka', system-ui, sans-serif !important;
    box-shadow: 0 8px 18px -10px rgba(40, 25, 60, .4) !important; }
  #world-stage .cta-row #world-play { border-radius: 999px !important; text-transform: uppercase; letter-spacing: .06em;
    height: 56px !important; max-width: 280px !important; font-size: 22px !important; }
  #world-stage .world-pager { display: flex; gap: 6px; justify-content: center; margin-top: 14px; }
  #world-stage .world-pager:empty { display: none; }
  #world-stage .world-pager i { width: 6px; height: 6px; border-radius: 99px; background: var(--home-ink, #3b2340); opacity: .25;
    transition-property: width, opacity; transition-duration: .2s; }
  #world-stage .world-pager i.on { width: 18px; opacity: .6; }

  /* top bar: smoky ⓘ, white pills */
  body.home-active #info-btn.floating-icon-btn { background: rgba(28, 20, 48, 0.3) !important; color: #fff !important;
    box-shadow: inset 0 0 0 1.5px rgba(255, 255, 255, .8), 0 6px 14px -6px rgba(20, 10, 40, .45) !important; }
  body.home-active #info-btn.floating-icon-btn svg { fill: currentColor !important; }
  html body.home-active #shop-btn.action-panel, body.home-active .coin-chip-home { border-radius: 999px !important; background: rgba(255, 255, 255, .94) !important; }

  @media (max-height: 700px) {
    #world-stage .shelf-back::before { margin: 0 0 6px; }
    #world-stage .coll { height: 56px !important; }
    #world-stage .world-goal { margin-top: 14px !important; }
    #world-stage .world-pager { margin-top: 8px; }
  }
  @media (prefers-reduced-motion: reduce) { #world-stage .world-pager i { transition: none; } }

  /* ═══ Tier rail card (2026-09-28, the home collection card in the run) ═══
     The ladder sits in the same white card as home's collection: pieces,
     progress line and caption on one surface, so it reads on every scene and
     the caption takes the white surface's deep ink. */
  #tier-strip { background: rgba(255, 255, 255, 0.82) !important; border-radius: 24px !important;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, .9) inset, 0 14px 30px -18px rgba(40, 25, 60, .45) !important;
    padding: 11px 12px 7px !important; margin: 6px 0 0 !important; }
  #tier-strip .tier-rail-track { background: rgba(40, 25, 60, .10) !important; }
  html.has-scene #tier-strip .tier-progress .cap-pre,
  html.has-scene #tier-strip .tier-progress .cap-name,
  html.has-scene #tier-strip .tier-progress .cap-pre.solo,
  #tier-strip .tier-progress .cap-pre, #tier-strip .tier-progress .cap-name {
    color: var(--score-ink, #3b2340) !important; text-shadow: none !important; }
  #tier-strip .tier-progress .cap-pre { opacity: .7; }


  /* ═══ Rail shadows (2026-09-28) ═══
     A piece not yet made this run is its shadow, not a faded grey copy: the
     silhouette says "something is here" without giving the picture away, and
     it reads on the white card. The art is its own layer (.ti-art), so the
     star badge stays lit. Made pieces keep the existing pop into colour. */
  #tier-strip .tier-img .ti-art { position: absolute; inset: 0; background: center / contain no-repeat; font-style: normal;
    display: grid; place-items: center; transition-property: filter; transition-duration: .35s; }
  #tier-strip .tier-img:not(.made) { opacity: 1; filter: none; transform: none; }
  #tier-strip .tier-img:not(.made) .ti-art { filter: brightness(0) opacity(.2); }
  #tier-strip .tier-img.next-target:not(.made) .ti-art { filter: brightness(0) opacity(.34); }
  #tier-strip .tier-img.is-goal-tier:not(.made) { filter: none; }
  #tier-strip .tier-img.made { opacity: 1; filter: none; }
  /* Just the pieces: no progress line under them (2026-09-28). */
  #tier-strip .tier-rail { display: none !important; }
  /* Home's idle life in the run: the hop wave travels along the made pieces
     every ~5s and the next shadow leans in when it arrives (keyframes shared
     with home). The art layer moves, so the slot's own unlock pop is free. */
  #tier-strip .tier-img.made .ti-art { transform-origin: 50% 100%;
    animation: shelfHop 5s cubic-bezier(.3, .6, .4, 1) infinite; animation-delay: calc(1s + var(--i, 0) * 0.16s); }
  #tier-strip .tier-img.is-next:not(.made) .ti-art { transform-origin: 50% 100%;
    animation: nextPeek 5s ease-in-out infinite; animation-delay: calc(1s + var(--i, 0) * 0.16s); }
  @media (prefers-reduced-motion: reduce) { #tier-strip .tier-img .ti-art { transition: none; animation: none !important; } }

  /* ═══ Power-ups on white (2026-09-28) ═══
     Circles that hold pieces are white, like the rail card and the score cloud;
     the emoji colour blooms on white where dark glass muddied it. Glyph-only
     utility buttons (gear, home) stay smoky. The count badge flips to ink so it
     survives on white; the ad offer is the coral of Play. */
  #powerup-bar .powerup-btn .powerup-icon,
  html.has-scene:not(.home-active) #powerup-bar .powerup-btn .powerup-icon {
    background-color: rgba(255, 255, 255, 0.92) !important;
    background-image: radial-gradient(circle at 34% 24%, rgba(255, 255, 255, 1) 0 12%, rgba(255, 255, 255, 0) 46%),
      linear-gradient(180deg, rgba(255, 255, 255, 0) 55%, rgba(60, 35, 100, 0.06) 100%) !important;
    box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.95), 0 10px 22px -12px rgba(40, 25, 60, 0.5), 0 2px 4px rgba(40, 25, 60, 0.08) !important;
  }
  #powerup-bar .powerup-btn .powerup-count { background: #45264f; color: #fff;
    box-shadow: 0 0 0 2px #fff, 0 2px 4px rgba(40, 25, 60, 0.3); }
  #powerup-bar .powerup-btn .powerup-count.zero { background: rgba(69, 38, 79, 0.4); color: #fff; }
  #powerup-bar .powerup-btn .powerup-count.ad-ready { background: #f4623a; color: transparent;
    box-shadow: 0 0 0 2px #fff, 0 2px 4px rgba(200, 70, 30, 0.35); }
  #powerup-bar .powerup-btn .powerup-count.ad-ready::after { border-left-color: #fff; }
  #powerup-bar .powerup-btn:disabled .powerup-icon { opacity: 1 !important; filter: none !important; }
  #powerup-bar .powerup-btn:disabled .powerup-icon > * { opacity: 0.5; filter: saturate(0.35); }
  #powerup-bar .powerup-btn:disabled .powerup-count { background: #a99fbb; }

  /* V4 polish: the world a hair quieter than the pieces — no more, or the
     3D world washes out (2026-09-28). */
  /* No CSS filter on the scene: Android Chrome rasterised the full-screen
     filtered layer below device resolution and the world read blurry
     (2026-09-29). Colour lives in the SVG itself. */
  /* V4: ambient specks under the same light — a soft shadow down-right and a
     highlight up-left, so they are small objects, not flat dots. */
  #scene .sc-p:not(.rain):not(.shoot):not(.snow):not(.dust) { filter: drop-shadow(1px 2px 1.5px rgba(40, 30, 70, 0.18)); }
  #scene .sc-p.leaf, #scene .sc-p.petal, #scene .sc-p.confetti {
    background-image: radial-gradient(circle at 30% 28%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 62%); }

  /* ═══ Arrow burst (home arrows, 2026-09-29) ═══
     A thin white ring and five tiny glints drifting the way the world moves.
     The fixed layer never transforms; its children do. */
  .arrow-burst { position: fixed; width: 0; height: 0; z-index: 120; pointer-events: none; }
  .arrow-burst i { position: absolute; left: 0; top: 0; display: block; }
  .arrow-burst .ab-ring { width: 44px; height: 44px; margin: -22px 0 0 -22px; border-radius: 50%;
    box-shadow: inset 0 0 0 2.5px rgba(255, 255, 255, 0.95), 0 0 8px rgba(255, 255, 255, 0.55);
    animation: abRing 0.55s ease-out both; }
  .arrow-burst .ab-glint { width: 6px; height: 6px; margin: -3px 0 0 -3px; background: #fff; border-radius: 1px;
    filter: drop-shadow(0 1px 1.5px rgba(40, 30, 70, 0.25));
    animation: abGlint 0.55s ease-out var(--dl, 0ms) both; }
  @keyframes abRing {
    0%   { transform: scale(0.6); opacity: 0.95; }
    45%  { opacity: 0.7; }
    100% { transform: scale(1.6); opacity: 0; }
  }
  @keyframes abGlint {
    0%   { transform: translate(0, 0) rotate(45deg) scale(0.2); opacity: 0; }
    25%  { opacity: 1; }
    70%  { opacity: 0.85; }
    100% { transform: translate(var(--dx), var(--dy)) rotate(45deg) scale(0.9); opacity: 0; }
  }
  @media (prefers-reduced-motion: reduce) { .arrow-burst { display: none; } }

  /* ═══ NEXT backing (2026-09-29) ═══
     The next piece sits on the same smoky round glass as the gear and home, so
     the three top corners read as one set. Its own element, behind the piece:
     the piece art is the next-img's background (set from JS) and stays on top. */
  .next-panel .next-back { display: none; }
  body:not(.home-active) .next-panel .next-back {
    display: block; grid-area: icon; align-self: center; justify-self: center;
    width: 46px; height: 46px; border-radius: 50%;
    background-color: rgba(28, 20, 48, 0.32);
    background-image: radial-gradient(circle at 32% 24%,
      rgba(255, 255, 255, 0.55) 0 6%, rgba(255, 255, 255, 0.12) 22%, rgba(255, 255, 255, 0) 55%);
    box-shadow:
      inset 0 0 0 1.5px rgba(255, 255, 255, 0.75),
      inset 0 -6px 12px rgba(0, 0, 0, 0.12),
      0 6px 14px -6px rgba(20, 10, 40, 0.45); }
  body:not(.home-active) .next-panel .next-img { width: 34px !important; height: 34px !important; margin: 0 !important; z-index: 1; position: relative; }

  /* ═══ Home arrows on white (2026-09-29) ═══
     Smoky glass sank into the 3D scenes. White like every other home surface,
     an ink chevron, a soft shadow; disabled fades but stays readable. */
  #world-stage .shelf-jar .jar-arrow { width: 48px !important; height: 48px !important;
    border-radius: 50%; background: rgba(255, 255, 255, 0.92) !important;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 8px 18px -8px rgba(40, 25, 60, 0.5), 0 2px 4px rgba(40, 25, 60, 0.12) !important; }
  #world-stage .shelf-jar .jar-arrow::before { border-color: var(--score-ink, #3b2340) !important; border-width: 0 0 3px 3px; }
  #world-stage .shelf-jar .jar-arrow:disabled { opacity: 0.45 !important; }
  body:not(.home-active) .next-panel .label { margin-top: 3px; }

  /* ═══ Run coins pill (2026-09-29) ═══
     The payout sits in the home coin chip's white pill so it reads on every
     theme: deep ink, tabular digits, a bigger coin with a soft gold glow. */
  .run-coin-row { background: rgba(255, 255, 255, 0.92); color: #3b2340 !important;
    padding: 6px 16px 6px 8px; border-radius: 999px; gap: 8px;
    font: 700 clamp(20px, 5.4vw, 24px)/1 'Fredoka', -apple-system, system-ui, sans-serif;
    font-variant-numeric: tabular-nums; letter-spacing: 0.01em;
    box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9) inset, 0 10px 22px -12px rgba(40, 25, 60, 0.5), 0 2px 4px rgba(40, 25, 60, 0.08); }
  .run-coin-row .coin-icon { width: 30px; height: 30px; filter: drop-shadow(0 1px 3px rgba(245, 163, 17, 0.45)); }
  .run-coin-row #run-coins { color: #3b2340; }
  .run-double { background: linear-gradient(180deg, #ff8a4c 0%, #f4622a 100%) !important; border: 0 !important;
    font-weight: 700 !important; box-shadow: 0 2px 3px rgba(190, 70, 20, 0.3), 0 10px 20px -8px rgba(230, 90, 30, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.4) !important; }

  /* ═══ Home header text (2026-09-29) ═══
     The per-scene ink sank into its own scene (green on green). White with a
     soft dark lift reads on light and dark worlds alike. */
  #world-stage .shelf-back::before { color: #fff !important; opacity: 1 !important;
    text-shadow: 0 1px 2px rgba(40, 25, 60, 0.45), 0 0 10px rgba(40, 25, 60, 0.28) !important; }
  #world-stage .coll-tag { color: #fff !important; opacity: 1 !important;
    text-shadow: 0 1px 2px rgba(40, 25, 60, 0.45), 0 0 8px rgba(40, 25, 60, 0.25) !important; }
  #world-stage .world-pager i { background: #fff !important; opacity: 0.55 !important;
    box-shadow: 0 1px 2px rgba(40, 25, 60, 0.3); }
  #world-stage .world-pager i.on { opacity: 1 !important; }

  /* ═══ Perf: fewer full-screen layers (2026-09-30) ═══
     Every world shows a scene, so the old wash (already opacity 0) and the
     floating orbs (hidden behind the scene's opaque sky) could never be seen —
     but they still animated and forced every layer above them onto the GPU.
     The grain now paints inside the scene SVG (see Scenes FINISH.grain). */
  html.has-scene .wash-layer { display: none !important; }
  html.has-scene body::before, html.has-scene body::after { display: none !important; animation: none !important; }
  #scene::after { display: none !important; }

  /* ═══ Glassier jar (2026-09-30) ═══
     Glass needs cues, not fog: brighter, faintly cool walls at the sides where
     the glass is thick, a clear middle, a thin inner line for depth, and two
     reflection streaks. All static gradients: no animation, no blur, no layer. */
  html.has-scene:not(.home-active) #stage {
    background:
      linear-gradient(90deg,
        rgba(255, 255, 255, 0.34) 0%, rgba(235, 246, 255, 0.14) 3.5%, rgba(255, 255, 255, 0) 12%,
        rgba(255, 255, 255, 0) 88%, rgba(235, 246, 255, 0.12) 96.5%, rgba(255, 255, 255, 0.3) 100%),
      linear-gradient(180deg,
        rgba(255, 255, 255, 0.22) 0%, rgba(255, 255, 255, 0.04) 7%, rgba(255, 255, 255, 0) 30%,
        rgba(255, 255, 255, 0) 88%, rgba(255, 255, 255, 0.16) 100%) !important;
    border: 1.5px solid rgba(255, 255, 255, 0.92) !important;
    box-shadow:
      inset 0 0 0 1px rgba(30, 50, 90, 0.10),
      inset 0 3px 0 rgba(255, 255, 255, 0.9),
      inset 0 -14px 14px -10px rgba(255, 255, 255, 0.7),
      0 22px 40px -18px rgba(40, 50, 90, 0.35),
      0 6px 12px rgba(40, 50, 90, 0.08) !important;
  }
  /* No reflection strip on the left: it read as a patch (2026-09-30). */
  html.has-scene:not(.home-active) #stage::before { display: none !important; }

  /* Bubble mode never shows a ghost jar behind the share card (2026-09-30). */
  html.bubble-mode.has-scene body.share-active #stage {
    background: transparent !important; box-shadow: none !important; border: none !important; }

