@import url("/symbols.css");
  :root{
    --ink:#0E1F1A; --ink-2:#16352C; --panel-deep:#0B1A15; --paper:#F7F3E3;
    --gold:#F9C85E; --cta:#F9C85E; --cta-dark:#C9973F; --toast-bg:var(--gold); --toast-fg:#1A140E;
    --lime:#AEBE37; --teal:#0D7464; --deep:#1D3A3E;
    --mint:#AEBE37; --muted:#8FA89A; --line:#2E5A48; --on-accent:#143029;
    --display:'Montserrat',sans-serif; --body:'Lato',sans-serif; --pixel:'Lato',sans-serif; --script:'Dancing Script',cursive;
  }
  *{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}
  html,body{height:100%}
  body{font-family:var(--body);background:var(--ink);color:var(--paper);overflow:hidden}
  button{font-family:inherit;cursor:pointer;border:none}
  .screen{position:fixed;inset:0;display:none;overflow-y:auto}
  .screen.active{display:block}
  /* top-right admin access */
  .admin-corner{position:fixed;top:14px;right:14px;z-index:70;
    background:rgba(8,26,20,.88);border:1px solid #2E5A48;color:#8FA89A;
    border-radius:11px;padding:9px 14px;font-size:12.5px;font-weight:700;backdrop-filter:blur(6px)}
  .admin-corner:hover{color:var(--gold);border-color:var(--gold)}
  .admin-tag{position:fixed;top:16px;right:150px;z-index:70;font-family:var(--pixel);
    font-size:10px;letter-spacing:2px;color:var(--gold);background:rgba(249,200,94,.10);
    border:1px solid rgba(249,200,94,.4);border-radius:8px;padding:8px 10px;display:none}

  /* Registration screens (#landing/#creator) inherit the org theme from :root —
     no scoped color override. The SME purple palette now lives as the "Amethyst"
     theme preset (applied via organizations.theme), so applyTheme() reaches these
     surfaces like every other. This is only a size-emphasis, no theming: */
  #landing .lw-big{font-size:18px;letter-spacing:.14em}

  /* ---------- LANDING ---------- */
  #landing{background:radial-gradient(1200px 600px at 50% -10%, #1E4A38 0%, transparent 60%), var(--ink)}
  .land-wrap{min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;position:relative}
  .marquee-lights{display:flex;gap:10px;margin-bottom:26px}
  .bulb{width:9px;height:9px;border-radius:50%;background:var(--gold);opacity:.25;animation:bulb 1.6s infinite}
  .bulb:nth-child(2n){animation-delay:.4s}.bulb:nth-child(3n){animation-delay:.8s}
  @keyframes bulb{0%,100%{opacity:.25}50%{opacity:1;box-shadow:0 0 12px var(--gold)}}
  .eyebrow{font-family:var(--pixel);font-size:11px;letter-spacing:3px;color:var(--gold);text-transform:uppercase;margin-bottom:18px}
  h1.land-h{font-family:var(--display);font-weight:700;font-size:clamp(30px,4.8vw,54px);line-height:1.1;letter-spacing:-.02em;max-width:900px;text-wrap:balance}
  @media(max-width:520px){h1.land-h{font-size:23px}}
  h1.land-h em{font-style:normal;color:var(--gold)}
  .land-sub{margin-top:20px;max-width:560px;color:var(--muted);font-size:17px;line-height:1.55}
  .land-sub strong{color:var(--paper)}
  .optin{margin-top:34px;display:flex;gap:10px;width:100%;max-width:480px}
  .optin input{flex:1;padding:16px 18px;border-radius:12px;border:1px solid var(--line);background:var(--ink-2);color:var(--paper);font-size:16px;outline:none;min-width:0}
  .optin input:focus{border-color:var(--gold)}
  .optin input::placeholder{color:#5A7A6A}
  .btn-cta{background:var(--cta);color:var(--on-accent);font-weight:700;font-size:16px;padding:16px 26px;border-radius:12px;box-shadow:0 6px 0 var(--cta-dark);transition:transform .08s, box-shadow .08s;white-space:nowrap}
  .btn-cta:active{transform:translateY(4px);box-shadow:0 2px 0 var(--cta-dark)}
  .optin-err{margin-top:10px;font-size:13px;color:var(--cta);height:16px}
  .cre-err{margin-top:10px;font-size:17px;font-weight:700;line-height:1.35;color:var(--cta)}
  .land-proof{margin-top:40px;display:flex;gap:26px;flex-wrap:wrap;justify-content:center;font-family:var(--pixel);font-size:11px;color:var(--muted);letter-spacing:1px}
  .land-proof b{color:var(--gold);font-weight:400}
  .land-note{margin-top:48px;text-align:center;font-size:12px;color:#41604F}
  .admin-login-link{text-align:center;margin-top:10px}
  .admin-login-link a{font-size:11px;color:#41604F;text-decoration:underline;opacity:.65;letter-spacing:.04em}
  .admin-login-link a:hover{opacity:1}
  .leaf{position:absolute;width:min(360px,44vw);height:min(360px,44vw);background:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 320'%3E%3Cg opacity='0.9'%3E%3Cellipse cx='70' cy='60' rx='84' ry='30' fill='%23569241' transform='rotate(38 70 60)'/%3E%3Cellipse cx='150' cy='36' rx='70' ry='24' fill='%23AEBE37' transform='rotate(64 150 36)'/%3E%3Cellipse cx='36' cy='150' rx='88' ry='28' fill='%231D3A3E' transform='rotate(14 36 150)'/%3E%3Cellipse cx='110' cy='120' rx='60' ry='20' fill='%230D7464' transform='rotate(48 110 120)'/%3E%3Cellipse cx='40' cy='250' rx='66' ry='22' fill='%23569241' transform='rotate(-12 40 250)'/%3E%3Cpath d='M10 310 Q120 240 90 110' stroke='%23464B37' stroke-width='5' fill='none' stroke-linecap='round'/%3E%3C/g%3E%3C/svg%3E") no-repeat center/contain;opacity:.5;pointer-events:none;z-index:0}
  .leaf.tl{top:-46px;left:-46px}
  .leaf.br{bottom:-46px;right:-46px;transform:rotate(180deg)}
  /* ⚠️ TENANT BRANDING — the leaf art is JUNGLE foliage with hex fills baked into a
     data-URI SVG. CSS custom properties cannot reach inside a data URI (it is a separate
     document), so these can never follow a theme the way the canvas world does. And
     recolouring would be the wrong fix anyway: tinting jungle leaves magenta still gives a
     cyberpunk tenant foliage — the SHAPE is the brand, not just the colour.
     So: show them only on the green presets and hide them everywhere else. Per-preset
     landing ornament is a real design task, not a colour hack. The attribute is set by the
     FOUC bootstrap in index.html (cached, so it does not flash). Unknown/absent preset
     hides them, which is the safe default. */
  .leaf{display:none}
  html[data-ww-preset="jungle"] .leaf,
  html[data-ww-preset="bloom"] .leaf{display:block}
  .land-wrap>*:not(.leaf){position:relative;z-index:1}
  .logo-wrap{display:flex;flex-direction:column;align-items:center;gap:10px;margin-bottom:22px}
  .logo-word{font-family:var(--display);font-weight:800;letter-spacing:.14em;font-size:15px;line-height:1.25;text-align:center}
  .logo-word .lw-top{color:var(--gold);font-size:12px;letter-spacing:.34em;display:block}
  .logo-word .lw-big{color:#F7F3E3;font-size:22px;letter-spacing:.2em;display:block}
  .script-lead{font-family:var(--script);font-size:clamp(22px,3.4vw,32px);color:var(--lime);margin-bottom:6px;transform:rotate(-2deg)}
  #landCd{color:#F7F3E3;font-weight:700}

  /* ---------- CREATOR ---------- */
  #creator{background:var(--ink)}
  .cre-wrap{min-height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:40px 24px;text-align:center}
  .cre-step{font-family:var(--pixel);font-size:11px;letter-spacing:3px;color:var(--gold);margin-bottom:14px}
  h2.cre-h{font-family:var(--display);font-weight:700;font-size:clamp(26px,4.5vw,40px)}
  .cre-sub{color:var(--muted);margin-top:10px;font-size:15px}
  #avatarPreview{margin:30px auto 8px;image-rendering:pixelated;filter:drop-shadow(0 10px 24px rgba(0,0,0,.5))}
  .swatches{display:flex;gap:12px;justify-content:center;margin-top:18px;flex-wrap:wrap}
  .swatch{width:44px;height:44px;border-radius:12px;border:3px solid transparent;transition:transform .1s}
  .swatch.sel{border-color:var(--gold);transform:scale(1.12)}
  .cre-q{margin-top:16px;font-family:var(--display);font-weight:700;font-size:17px;color:var(--gold)}
  .cre-wrap{position:relative;overflow:hidden}
  .cre-wrap>*:not(.leaf){position:relative;z-index:1}
  .gender-row{display:flex;gap:10px;justify-content:center;margin-top:20px}
  .gbtn{background:var(--ink-2);border:1px solid var(--line);color:var(--paper);border-radius:12px;padding:11px 22px;font-size:15px;font-weight:700}
  .gbtn.sel{background:var(--gold);border-color:var(--gold);color:var(--on-accent)}
  .cre-name{margin-top:14px;width:100%;max-width:380px}
  .cre-name input{width:100%;padding:18px 20px;border-radius:14px;border:2px solid var(--gold);background:#0F2B22;color:var(--paper);font-size:19px;font-weight:700;text-align:center;outline:none;box-shadow:0 0 0 5px rgba(249,200,94,.10),0 8px 24px rgba(0,0,0,.35);transition:box-shadow .2s}
  .cre-name input:focus{box-shadow:0 0 0 6px rgba(249,200,94,.24),0 8px 24px rgba(0,0,0,.35)}
  .cre-name input::placeholder{color:#7A8F80;font-weight:400}
  #enterBtn{margin-top:26px}

  /* ---------- GAME ---------- */
  #game{background:#04100C;overflow:hidden !important}
  /* ⛔⛔ `pinch-zoom`, NOT `none` — AN ATTENDEE WHO ZOOMED IN COULD NOT ZOOM BACK OUT. Matthew, on a
     phone: "I had to hit it multiple times which zoomed in my screen and I was unable to zoom out."
     ⭐ THE TRAP HAD TWO HALVES AND ONLY ONE WAS OBVIOUS. Getting IN: iOS Safari has IGNORED
     `user-scalable=no` since iOS 10, so double-tap-to-zoom fires regardless of the viewport meta —
     and repeated fast taps in one spot is exactly what the old tap gate produced (see game.js).
     Getting OUT: `touch-action:none` tells the browser to handle NO gesture on this element, so a
     two-finger pinch over the canvas — which is the whole screen — did nothing. Zoom was therefore
     ENTERABLE AND NOT EXITABLE, and an attendee who tripped it was stuck for the rest of the event.
     ⭐⭐ `pinch-zoom` KEEPS PANNING DISABLED — which is the property the joystick actually needs, so
     single-finger drags still reach our pointer handlers untouched — while handing MULTI-finger
     pinch back to the browser. That makes the zoom symmetric rather than one-way. It is not a
     licence to zoom; it is the way back.
     ⚠️ NOT a fix for the tap gate, and not a substitute for it: `touch-action` is why they could not
     RECOVER, never why they got there. Both halves ship together for that reason. */
  #gameCanvas{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:pinch-zoom}
  .hud{position:absolute;left:0;right:0;pointer-events:none}
  .hud-top{top:0;padding:12px 14px;display:flex;justify-content:space-between;align-items:flex-start;gap:10px}
  .hud-chip{pointer-events:auto;background:rgba(8,26,20,.86);backdrop-filter:blur(6px);border:1px solid #2E5A48;border-radius:12px;padding:9px 14px;font-size:13px;line-height:1.35}
  .hud-chip .t{font-family:var(--pixel);font-size:10px;letter-spacing:2px;color:var(--gold);display:block;margin-bottom:2px}
  #countdown{font-family:var(--pixel);font-size:15px;color:#fff}
  /* ⛔ THE CHIP MUST NOT SURVIVE AS AN EMPTY PILL. With the headcount gone the container holds
     only #liveTag and #countdown — and endEvent() hides the tag WITHOUT showing the countdown, so
     in the `ended` phase both children are hidden and this rendered as a blank rounded box. The
     number used to mask that state; deleting it exposed it.
     ⭐ Keyed on a CLASS, never on inline `display`, because the confirmation corridor stores and
     restores this element's INLINE display when it hides the event HUD. A second inline writer
     would fight it; a class composes with it. */
  #attChip.empty{display:none}
  /* ── THE EVENT PILL, TOP-CENTRE (JT's mockup, zone 3 of the desktop nav redesign) ──────────
     It used to sit in the bottom-right HUD strip beside the chat button. The event's status is the
     one thing every attendee should be able to find without looking for it, and the bottom-right
     corner is now the chat corner.
     ⛔ `position:fixed`, NOT absolute: the element stays where it is in the DOM (inside
     `.hud-bottom`), and an absolutely-positioned child would be laid out against THAT container's
     box, which sits at the bottom of the screen — `top:14px` would resolve near the floor.
     Leaving the markup alone is deliberate: #attChip's inline `display` is stored and restored by
     the confirmation corridor, and its `.empty` class is keyed on the same element.
     ⚠️ DESKTOP ONLY. A phone has its own bar and its own spacing (`#game.mobile #attChip` below),
     and the redesign JT scoped is desktop. */
  /* ⛔ TOP-LEFT, NOT CENTRED — CORRECTED 2026-09-06, FROM A SCREENSHOT. Centred was the mockup and
     it is wrong in the room: the stage screen is centred too, so the pill landed ON the video
     whenever an attendee stood near the stage. JT: "that spot sometimes will overlay with the
     actual screen." ⭐ The top-left corner is empty in every phase and it stacks with the left
     rail below it, so the world's chrome reads as one column down that edge. */
  #game:not(.mobile) #attChip{position:fixed;left:18px;transform:none;top:14px;z-index:12;
    display:inline-flex;align-items:center;gap:8px;padding:9px 16px;border-radius:999px;
    background:color-mix(in srgb, var(--panel-deep,#0B1A15) 92%, transparent);
    border:1px solid color-mix(in srgb, var(--gold) 22%, transparent);
    box-shadow:0 6px 20px rgba(0,0,0,.45)}
  /* The live line reads as a sentence now, not a label: "● LIVE" was chrome, "The presentation is
     live" is the thing the attendee wants to know. The blink moves to the DOT — a pulsing dot is
     the convention for "on air", and blinking a whole sentence is hard to read. */
  #game:not(.mobile) #liveTag{font-family:var(--display);font-size:13px;letter-spacing:.01em;
    animation:none;display:none;align-items:center;gap:8px}
  #game:not(.mobile) #attChip #liveTag{margin-left:0}
  #game:not(.mobile) #liveTag .lt-dot{width:8px;height:8px;border-radius:999px;
    background:var(--danger,#C6362B);animation:blink 1.2s infinite;flex:none}
  /* ⛔ `#liveTag` is shown with an INLINE `display:inline` from JS (game.js), which would beat any
     `display:flex` here and drop the gap between dot and text. The dot is floated back into line
     with a vertical-align instead of fighting the inline writer — the JS owns visibility, the
     stylesheet owns looks, and neither has to know about the other. */
  #game:not(.mobile) #liveTag .lt-dot{display:inline-block;vertical-align:middle;margin-right:7px}
  #attChip #liveTag{margin-left:8px}
  #liveTag{display:none;color:#C6362B;font-family:var(--pixel);font-size:14px;animation:blink 1.2s infinite}
  @keyframes blink{50%{opacity:.35}}

/* ⛔⛔ THE FIRST-PAINT HOLDING OVERLAY — see the block above #wwHolding in index.html for why this
   is an overlay rather than a screen, and why it must not brand itself.
   ⭐ EVERY VALUE HERE IS A LITERAL ON PURPOSE. Theme tokens are not resolved at first paint (that is
   what `theme-pending` exists for) and the Google font may not have loaded, so a token or a webfont
   here would either resolve to nothing or repaint under the attendee. `#0d0d12` is the same backdrop
   `html.theme-pending` already paints, so the two agree instead of flashing against each other.
   ⚠️ THE TIMER IS A NO-JS FALLBACK, NOT THE MECHANISM. In the normal path `html.ww-ready` removes
   this the moment the route is decided — usually in a few ms. The animation only matters when
   game.js never runs at all, and it fails toward SHOWING THE PAGE, which is the safe direction: a
   stranded attendee is worse than the flash this replaces.
   ⚠️ 8s, not 1s: it must never fire while a slow-but-working session refresh is still in flight, or
   it reintroduces the flash on exactly the connection that can least afford it. */
#wwHolding{position:fixed;inset:0;z-index:200;display:flex;align-items:center;justify-content:center;
  background:#0d0d12;
  animation:wwHoldingBail 240ms ease 8s forwards}
#wwHolding .msg{opacity:0;
  font:600 18px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  color:#9FB3AB;letter-spacing:.01em;text-align:center;padding:0 24px;
  animation:wwHoldingMsg 200ms ease 450ms forwards}
/* The normal exit. Display:none rather than opacity so nothing of it can intercept a tap. */
html.ww-ready #wwHolding{display:none}
@keyframes wwHoldingBail{to{opacity:0;visibility:hidden}}
@keyframes wwHoldingMsg{to{opacity:1}}
/* ⚠️ Reduced motion still needs the bail-out — it is a timeout, not decoration — so only the
   message's fade is dropped. */
@media (prefers-reduced-motion:reduce){
  #wwHolding{animation-duration:0s}
  #wwHolding .msg{animation:wwHoldingMsg 0s linear 450ms forwards}
}
  #mudChip{position:absolute;left:50%;top:64px;transform:translateX(-50%);pointer-events:none}
  /* ⛔⛔ `left:50%` WITH NO WIDTH CAPS AN ELEMENT AT HALF THE SCREEN, AND `max-width` READS AS IF IT
     IS IN CHARGE WHEN IT IS INERT (measured on the LIVE build, mobile 375px, 2026-08-22).
     An absolutely-positioned box with `left:50%` and `width:auto` shrink-to-fits into the space its
     containing block leaves, which is `100% - 50%`. `translateX(-50%)` then moves it back so it
     LOOKS correctly centred — the visual is right and the layout width was already halved.
     ⭐ Measured, both squeezed to exactly 188px of a 375px screen:
  /* ⛔⛔ AND THE WIDTH HAS A CEILING ON MOBILE, BECAUSE RELEASING THE CAP CREATED A NEW COLLISION —
     found by HIT-TESTING the live build, not by looking at it. At its natural width the card ran to
     x349 while `#mobRail` starts at x313, and `document.elementFromPoint()` at the MAP button's
     centre returned `soundAsk`: the card is z44 and the rail is z12, so it silently took the tap.
     ⭐ A fix that unwraps a button and steals a navigation control is not a fix. Derived from the
     rail's own strip plus the shared `--ww-mob-gap` ("clear air between two tappable things"), so
     moving the rail moves this too — never a second hardcoded number, which is the exact drift
     `suite-mobile-stacking` exists to stop.
     ⚠️ Costs 25px of the height back (199 → 224 measured, from the title re-wrapping) and that is
     the right trade: 224px that cannot steal a tap beats 199px that can.
     ⚠️ MOBILE ONLY — there is no rail on desktop, so the base `max-width` still stands there. */
  #mudChip .step{display:block;font-size:13px}
  #mudChip .step.dim{opacity:.45}
  #mudChip .step.done{color:var(--mint)}
  .hud-bottom{bottom:0;padding:14px;display:flex;justify-content:flex-start;align-items:flex-end;gap:10px;transition:bottom .3s ease}
  .hud-btn{pointer-events:auto;background:rgba(8,26,20,.9);border:1px solid #2E5A48;color:var(--paper);border-radius:12px;padding:11px 15px;font-size:13px;font-weight:600}
  .hud-btn.gold{background:var(--gold);border-color:var(--gold);color:#241A05}
  /* ── the collapsed CHAT BAR (JT's mockup, 2026-09-06) ─────────────────────────────────────
     It is the same #chatToggle it always was — the button IS the collapsed state, so the panel
     opens directly above it and neither one moves. Desktop only: `#game.mobile #chatToggle` is
     already display:none, and mobile reaches chat through its own rail. */
  #chatToggle.chat-bar{display:inline-flex;align-items:center;gap:8px;padding:11px 14px;border-radius:14px}
  #chatToggle.chat-bar .cb-l{font-weight:700}
  #chatToggle.chat-bar .cb-v{color:var(--muted);font-size:11px;margin-left:2px}
  /* The unread badge. ⛔ A COUNT IS ONLY EVER SHOWN WHILE THE PANEL IS SHUT — an unread badge over
     an open panel is a number describing messages the attendee is currently looking at. */
  #chatToggle.chat-bar .cb-n{display:inline-flex;align-items:center;justify-content:center;
    min-width:20px;height:20px;padding:0 6px;border-radius:999px;
    background:var(--danger,#C6362B);color:#fff;font-size:11px;font-weight:800;line-height:1}
  #chatToggle.chat-bar .cb-n[hidden]{display:none}
  #chatToggle.chat-bar.has-unread{border-color:color-mix(in srgb, var(--gold) 45%, transparent)}
  #chatToggle.chat-bar.folded{display:none}
  /* The header's collapse control is a chevron now, not a ✕ — the panel folds back into its bar
     rather than being dismissed, so an X overstated what it does. */
  .ch-head button{line-height:1}

  /* lower-third presentation bar */
  #stageBar{position:absolute;left:0;right:0;bottom:0;z-index:12;pointer-events:none;
    background:linear-gradient(180deg,rgba(3,10,7,0) 0%,rgba(2,8,6,.94) 26%);
    padding:34px 18px 18px;display:flex;align-items:center;justify-content:center;gap:16px;flex-wrap:wrap;
    transform:translateY(110%);transition:transform .32s cubic-bezier(.3,1.2,.5,1)}
  #stageBar.show{transform:translateY(0)}
  /* ⛔ THE PILL DOCKED INTO THE BAR — one bar, not two stacked (JT: "ALL ON ONE BAR").
     `#stageAudio` is `position:fixed` on its own, which cannot join a flex row, so docking flips it
     to static and lets the bar lay it out beside the watch buttons.
     ⚠️ `pointer-events:auto` is required, not decorative: `#stageBar` is pointer-events:none and
     re-enables only `button` descendants — so the volume RANGE input would be dead inside the bar
     without this, and a slider that cannot be dragged is worse than one that is not there. */
  /* ⛔⛔ AND IT DROPS ITS OWN CAPSULE WHEN DOCKED, OR IT READS AS A BUTTON ON A BUTTON. JT: "it
     looks like it's a button on top of another button. There's almost a highlight underneath it."
     ⭐ THAT IS EXACTLY WHAT IT WAS. `#stageAudio` is a dark pill in its own right — background,
     1px border, 999px radius, 6/10 padding — built for FLOATING over the world, where it has to
     be legible against whatever is behind it. Inside the controls card there is nothing to be
     legible against: the card is the background, and `#stageAudioTap` is already a full gold
     pill. So the container's chrome was a second, larger, darker capsule showing as a rim around
     the button — which is why it looked like a highlight rather than like a border.
     ⚠️ `.in-bar` restated POSITION but never appearance, and that is the easy half to miss: the
     dock had to fix `position:fixed` (a fixed element cannot join a flex row) or it would not
     have laid out at all, so the loud failure got solved and the quiet one shipped. */
  #stageAudio.in-bar{position:static;left:auto;bottom:auto;transform:none;pointer-events:auto;
    margin:0;box-shadow:none;background:none;border:0;padding:0;border-radius:0}
  #stageBar .sb-label{font-family:var(--display);font-weight:800;font-size:13px;letter-spacing:.22em;color:var(--gold)}
  #stageBar .sb-dot{width:9px;height:9px;border-radius:50%;background:#C6362B;animation:blink 1.2s infinite}
  #stageBar button{pointer-events:auto;color:var(--on-accent);font-family:var(--display);font-weight:800;font-size:16px;letter-spacing:.04em;border-radius:12px;padding:15px 26px}
  #fullBtn{background:var(--cta);box-shadow:0 5px 0 var(--cta-dark)}
  #fullBtn:active{transform:translateY(4px);box-shadow:0 1px 0 var(--cta-dark)}
  #theaterBtn{background:var(--teal);box-shadow:0 5px 0 var(--deep);color:var(--paper) !important}
  #theaterBtn:active{transform:translateY(4px);box-shadow:0 1px 0 var(--deep)}
  /* ⛔⛔ THE PRESENTATION CONTROLS ARE A CENTRED CARD ON DESKTOP, NOT A FULL-WIDTH BAND (JT's
     mockup, 2026-09-06). The band spanned the whole floor, so everything anchored to the bottom
     had to be pushed out of its way — the HUD strip, the chat panel and the left rail each carried
     a `.stagebar` lift, and the whole screen jumped when an attendee walked near the stage.
     ⭐ A CARD REACHES NO CORNER, so those three lifts are DELETED rather than re-tuned: the fix is
     that nothing needs to move, not that everything moves by a better number.
     ⚠️ `.show` MUST RESTATE translateX(-50%). The base rule slides the bar in with
     `transform:translateY(…)`, and `transform` is one property — a `.show` that sets only
     `translateY(0)` silently drops the centring and the card jumps to the left edge at the end of
     its animation. That is the whole reason both rules below carry both functions. */
  #game:not(.mobile) #stageBar{left:50%;right:auto;bottom:22px;width:max-content;
    max-width:min(760px,92vw);padding:14px 18px;gap:14px;flex-wrap:nowrap;border-radius:20px;
    background:color-mix(in srgb, var(--panel-deep,#0B1A15) 94%, transparent);
    border:1px solid color-mix(in srgb, var(--gold) 22%, transparent);
    box-shadow:0 14px 40px rgba(0,0,0,.55);
    transform:translateX(-50%) translateY(160%)}
  #game:not(.mobile) #stageBar.show{transform:translateX(-50%) translateY(0)}
  /* The status words come OFF the card: #attChip carries them now, top-left, and the same sentence
     twice on one screen is not two pieces of information. ⚠️ Desktop only — on a phone the `.mini`
     row has no pill above it, and its own comment explains that the words are what make it a
     NOTIFICATION rather than two unexplained buttons. */
  #game:not(.mobile) #stageBar .sb-label,
  #game:not(.mobile) #stageBar .sb-sub,
  #game:not(.mobile) #stageBar .sb-dot{display:none}
  .p-input{width:100%;margin-top:9px;padding:11px 12px;border-radius:10px;border:1px solid #DDD2B0;background:#FBF7EA;color:#241E10;font-size:14px;font-family:var(--body);outline:none}
  .p-input:focus{border-color:var(--gold)}
  .p-row-label{font-size:11px;font-weight:700;letter-spacing:.12em;color:#8A7B58;margin-top:12px;text-transform:uppercase}
  .mini-swatch{width:30px;height:30px;border-radius:8px;border:2px solid transparent;margin:6px 6px 0 0}
  .mini-swatch.sel{border-color:#143029;transform:scale(1.1)}
  .chip-btn{display:inline-block;margin:8px 8px 0 0;padding:9px 13px;border-radius:9px;background:#EFE9D2;border:1px solid #DDD2B0;font-size:13px;font-weight:700;color:#3A3020}
  .chip-btn.sel{background:var(--gold);border-color:var(--gold)}
  .cal-row{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:10px;padding:10px 12px;background:#EFE9D2;border-radius:10px}
  .cal-row .c-day{font-size:10px;font-weight:800;letter-spacing:.1em;color:#8A7B58}
  .cal-row .c-name{font-size:14px;font-weight:700}
  .cal-row .c-time{font-size:12px;color:#5B5340}
  /* live chat */
  /* Persistent LEFT dock (Part 1 §1.2 states 1+2): mirrors the old right-side geometry.
     Always visible by default on desktop (JS adds .show once); the ✕ / #chatToggle collapse it.
     Mobile is overridden below to the bottom sheet. */
  /* ⛔⛔ BOTTOM-RIGHT, AND IT USED TO BE BOTTOM-LEFT WHILE ITS BUTTON WAS BOTTOM-RIGHT (JT, from a
     screenshot, 2026-09-06). The panel opened on the OPPOSITE side of the screen from the control
     that opened it, so the eye had to cross the whole world to find what it had just asked for —
     and after the left rail landed it opened ON TOP of the rail as well.
     ⭐ IT ALSO REMOVES A JUMP NOBODY HAD NAMED: `.sidechat` (desktop webinar/pitch) already docks
     chat to the RIGHT, so the panel used to slide left→right when the video opened and back when
     it closed. Same side in both states now, so that transition is a resize rather than a leap. */
  #chatPanel{position:absolute;right:14px;bottom:64px;width:302px;height:340px;display:flex;flex-direction:column;
    background:var(--panel-deep);border:1px solid var(--line);border-radius:14px;overflow:hidden;z-index:11;
    opacity:0;pointer-events:none;transform:translateY(10px) scale(.98);transform-origin:bottom right;transition:.25s}
  #chatPanel.show{opacity:1;pointer-events:auto;transform:none}
  /* Scroll-to-latest pill: the persistent panel's "unread" signal — new messages arrived while you
     were scrolled up reading history (postChat no longer force-scrolls in that case). */
  #chatJump{position:absolute;left:50%;transform:translateX(-50%);bottom:52px;z-index:2;
    background:var(--cta);color:var(--on-accent);border-radius:999px;padding:5px 13px;font-size:11.5px;
    font-weight:700;box-shadow:0 4px 12px rgba(0,0,0,.45);cursor:pointer;white-space:nowrap}
  #chatJump[hidden]{display:none}
  /* (the `.stagebar` chat lift is gone — the controls are a centred card and reach no corner) */
  /* ---- Desktop webinar/pitch: chat BESIDE the stage (Zoom/Meet layout) ----
     `.sidechat` is set by layoutDesktopWatch() only when: desktop, video open, chat open, not the
     walk-mode PiP, and phase is webinar or pitch. So these rules cannot reach the lobby, a phone,
     or a collapsed dock — the JS predicate is the gate and this is only what it renders as.
     ⛔ DELIBERATELY NOT A `display:none` ANYWHERE NEAR #chatPanel. suite-chat-video-states C4
     asserts that the tutorial room's `mudmode` hide is the ONLY display:none on this element,
     because a hidden-by-CSS panel is invisible to the z-index contract the other four states
     rest on. This layout MOVES the panel; it never hides it.
     ⚠️ Placed AFTER #game.stagebar #chatPanel: same specificity (2 ids + 1 class), so source
     order is what makes `bottom:0` beat that rule's `bottom:150px`. Moving this block up breaks
     the layout only while the stage bar is up, which is most of the event. */
  #game.sidechat{--wwchatw:clamp(300px,26vw,400px)}
  /* The stage gives up the right-hand strip; the video re-letterboxes into what is left rather
     than playing behind chat. #videoOverlay is inset:0, so only `right` needs overriding. */
  #game.sidechat #videoOverlay{right:var(--wwchatw)}
  /* Full-height flush column. transform:none because the base rule's collapsed transform would
     otherwise slide the column; opacity/pointer-events still come from .show, which is always on
     here (the class requires chatOpen), so the panel stays a normal member of the z-80 stack. */
  #game.sidechat #chatPanel{left:auto;right:0;top:0;bottom:0;width:var(--wwchatw);height:auto;
    border-radius:0;border:none;border-left:1px solid var(--line);transform:none;z-index:80}
  #game.sidechat #chatJump{bottom:56px}
  .ch-head{display:flex;align-items:center;justify-content:space-between;padding:10px 14px;border-bottom:1px solid var(--line)}
  .ch-head b{font-family:var(--display);font-size:11px;letter-spacing:.2em;color:var(--gold)}
  .ch-head button{background:none;color:var(--muted);font-size:16px}
  #chatMsgs{flex:1;overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:8px;font-size:12.5px}
  .cm{line-height:1.4;color:var(--paper)}
  .cm .nm{font-weight:700;color:var(--gold);margin-right:4px}
  .cm .bd{font-size:11px;margin-right:2px}
  .cm .vipb{display:inline-block;filter:hue-rotate(215deg) saturate(1.8) brightness(1.1)}
  /* 2 Oct 2026 — the host's mark carries a word; a message can answer another (docs/host-tools-2-oct.md) */
  .cm .staffb{display:inline-block;padding:0 5px;border-radius:5px;background:rgba(61,214,140,.16);color:#9BEBC0;font-size:10px;font-weight:800;letter-spacing:.02em;white-space:nowrap}
  .cm .cm-q{display:block;max-width:100%;margin:0 0 2px;padding:2px 7px;border:0;border-left:2px solid var(--gold);border-radius:0 6px 6px 0;background:rgba(255,255,255,.07);color:rgba(247,243,227,.72);font:500 11px/1.35 inherit;font-family:inherit;text-align:left;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
  .cm .cm-q b{color:var(--gold);font-weight:700;margin-right:4px}
  .cm .cm-r{margin-left:6px;padding:0 5px;border:0;border-radius:6px;background:none;color:rgba(247,243,227,.5);font-size:12px;line-height:1.3;cursor:pointer;opacity:0;transition:opacity .12s}
  .cm:hover .cm-r,.cm .cm-r:focus-visible{opacity:1}
  #game.mobile .cm .cm-r{opacity:.55;padding:0 4px;font-size:11px}
  .cm.cm-hit{background:rgba(249,200,94,.20);border-radius:6px;box-shadow:0 0 0 3px rgba(249,200,94,.20)}
  #chatReply{display:flex;align-items:center;gap:6px;padding:5px 10px;border-top:1px solid rgba(255,255,255,.12);background:rgba(249,200,94,.10);font-size:11.5px;line-height:1.3;color:var(--paper)}
  #chatReply[hidden]{display:none}
  #chatReply .cr-t{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  #chatReply .cr-t b{color:var(--gold);margin-right:4px}
  #chatReply .cr-x{flex:none;border:0;background:none;color:inherit;opacity:.7;cursor:pointer;font-size:12px;padding:2px 6px}
  .ch-input{display:flex;gap:8px;padding:10px;border-top:1px solid var(--line)}
  .ch-input input{flex:1;min-width:0;background:var(--panel-deep);border:1px solid var(--line);border-radius:9px;color:var(--paper);padding:9px 11px;font-size:13px;outline:none}
  .ch-input button{background:var(--cta);color:var(--on-accent);font-weight:800;border-radius:9px;padding:9px 13px;font-size:13px}
  .lb-sub{margin-top:16px;font-size:10px;font-weight:800;letter-spacing:.18em;color:#8A7B58}
  .lb-ind{display:flex;align-items:center;gap:8px;margin-top:8px;padding:7px 10px;background:#EFE9D2;border-radius:9px;font-size:12.5px}
  /* The pale "me" strip has no 1:1 token — it is a light TINT of the accent over the paper
     surface, and the token set has no tint. `color-mix` derives it. Declared twice on
     purpose: a browser without color-mix support ignores the second declaration and keeps
     the old lavender, so this can only improve, never blank out. */
  .lb-ind.me{background:linear-gradient(90deg,#E7DEFF,#F1EBFF);border:1px solid #C9B6FF}
  .lb-ind.me{background:color-mix(in srgb,var(--cta,#8A5CF6) 14%,var(--paper,#F7F3E3));
    border-color:color-mix(in srgb,var(--cta,#8A5CF6) 42%,var(--paper,#F7F3E3))}
  .lb-ind .li-r{font-weight:800;color:#8A7B58;width:16px}
  .lb-ind .li-e{width:18px;text-align:center}
  .lb-ind .li-n{font-weight:700;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* ⚠️ `color:#fff` WAS A LATENT CONTRAST BUG, not just a theming one. The moment the
     background follows `--cta`, a hardcoded white label lands on jungle's #F9C85E and lux's
     #E8C874 — white-on-yellow, which CLAUDE.md §5 says is "impossible by construction"
     precisely because `--on-accent` is DERIVED by wcagOnAccent rather than picked. Using the
     token is what makes theming this safe; changing the background alone would have shipped
     an unreadable badge to two presets. Amethyst's on-accent is #FFFFFF, so it is unchanged
     there. */
  .lb-ind .li-v{font-size:9px;font-weight:800;background:var(--cta,#8A5CF6);color:var(--on-accent,#fff);border-radius:5px;padding:2px 5px}
  .lb-ind .li-l{font-weight:800;color:#5B5340}

  #lockBanner{position:absolute;left:50%;top:46%;transform:translate(-50%,-50%);display:none;pointer-events:none;background:rgba(8,26,20,.94);border:1px solid #F9C85E;border-radius:14px;padding:16px 22px;text-align:center;max-width:300px;z-index:10}
  #lockBanner .lb-lock{font-size:26px;margin-bottom:6px}
  #lockBanner .lb-t{font-family:var(--display);font-weight:700;font-size:17px}
  #lockBanner .lb-s{font-size:13px;color:var(--muted);margin-top:4px}
  #vipBuyBtn{pointer-events:auto;margin-top:12px;background:var(--cta);color:var(--on-accent);font-weight:700;font-size:14px;padding:10px 20px;border-radius:10px;box-shadow:0 5px 0 var(--cta-dark);border:none;cursor:pointer}
  #vipBuyBtn:active{transform:translateY(3px);box-shadow:0 2px 0 var(--cta-dark)}
  /* The corridor's offer, second door in (#718). ⛔ `pointer-events:auto` is NOT decoration:
     #lockBanner is pointer-events:none, so without it this button renders and cannot be
     pressed — a defect no screenshot shows. #vipBuyBtn carries the same line for the same
     reason. ⭐ Deliberately GHOST rather than gold: the banner already has one primary action
     and two CTAs of equal weight is a banner with no primary action. */
  #vipUpsellBtn{pointer-events:auto;display:block;margin:10px auto 0;background:transparent;color:#F9C85E;
    font-weight:700;font-size:14px;padding:9px 18px;border-radius:10px;border:2px solid rgba(249,200,94,.55);cursor:pointer}
  #vipUpsellBtn:active{transform:translateY(2px);background:rgba(249,200,94,.14)}

  #hint{position:absolute;left:0;right:0;margin-inline:auto;width:fit-content;bottom:96px;background:rgba(8,26,20,.9);border:1px solid #2E5A48;border-radius:12px;padding:10px 16px;font-size:13px;color:var(--muted);pointer-events:none;transition:opacity .5s;text-align:center;max-width:88vw}
  /* ⛔⛔ ABOVE THE STREAM, BECAUSE AT z-20 EVERY TOAST WAS INVISIBLE FOR THE WHOLE EVENT.
   Measured on a rendered page at main, phase `webinar`, stream auto-presented: `#toast` reported
   `display:block; visibility:visible; opacity:1` at (14,586,320x58) and was nowhere on screen.
   Both it and `#videoOverlay` are children of `#game`, so their z-indexes compare DIRECTLY, and
   the overlay's box (0,0,947x720) fully CONTAINS the toast's. Hiding the overlay and changing
   nothing else made the identical toast plainly visible — the control that separates "occluded"
   from "never rendered".
   ⛔ ALL VIEWPORTS, NOT JUST DESKTOP. The only non-occluding state is MOBILE WITH CHAT OPEN, where
   the overlay is a `--vidh` band rather than full height — and that is not the default:
   `maybeAutoPresentVideo` opens 'screen' with chat CLOSED. The band is the exception.
   ⭐ AND THIS IS NOT AN ANNOUNCEMENT FIX. `#toast` has ONE writer (`nextToast`) and 45 call sites
   in game.js: scarcity counts, bot applications, phase changes, door refusals. Every one of them
   has been invisible during the stream for as long as this has been. The announcement is simply
   what somebody happened to be testing.
   ⚠️ 90 IS BRACKETED, NOT ARBITRARY — it must clear `#videoOverlay` (60) and `#chatPanel` (80) so
   it is legible while watching, and stay under `#buyModal` (120) so it can never paint over
   checkout. ⛔ ADMIN's announcement banner is 95 and must stay ABOVE this: two elements at one z
   means DOM order decides and nobody chose it.
   ⚠️⚠️ AND WHEN THAT BANNER SHIPS, THE ANNOUNCEMENT MUST STOP GOING THROUGH `toast()` — otherwise
   the same text renders twice, once here and once there. That hand-off is agreed with ADMIN, not
   assumed by either side. */
  #toast{position:absolute;left:50%;transform:translateX(-50%);bottom:150px;background:var(--toast-bg);color:var(--toast-fg);font-weight:800;font-size:13.5px;border-radius:12px;padding:11px 16px;opacity:0;transition:opacity .3s;pointer-events:none;max-width:min(360px,80vw);text-align:center;z-index:var(--z-toast);box-shadow:0 10px 28px rgba(0,0,0,.45);border:1px solid rgba(0,0,0,.22)}
  /* ⛔⛔ THE ANNOUNCEMENT BANNER — z-index 95, AND THE NUMBER IS A DECISION, NOT A DEFAULT.
     The z-space here is a shared namespace with no declaration: buyModal 120, toast 90 (raised
     from 20 by #802, which is why the comment above exists), chatPanel 80, videoOverlay 60.
     ⛔ 90 WAS THE NUMBER HANDED TO BOTH LANES AND IT IS TAKEN. Two elements at one z-index fall
     back to DOM order, which neither lane would be choosing — so this is 95: above chat (so it
     survives the webinar side-layout),
     below buyModal (so it never covers checkout), and unambiguous against that toast.
     ⚠️ NEIGHBOURS IN THE BAND, so the next lane does not have to go looking: #adminScreen #toast
     is 99 — deliberately above this, because a toast raised on the ADMIN screen is addressed to
     the operator standing in front of it, not to an attendee. #chatPanel is declared five times
     (11 on the base rule, 80 on four state rules); the one that matters is 80, and a check that
     reads the FIRST match compares against 11 and passes at 12.
     ⚠️ If you take another number in this range, write it here. Two lanes picking colliding
     values in files that never touch is invisible to every check we own — it happened on this
     very element, the same day, on a number handed to both lanes.
     ⭐ TOP-CENTRE AND LARGE because this is the loudest thing in the product: it is how one
     person moves two hundred, and a notice they miss is an instruction that did not happen. */
  #wwBanner{position:fixed;top:0;left:50%;transform:translateX(-50%) translateY(-120%);
    z-index:95;max-width:min(760px,94vw);width:max-content;
    display:flex;align-items:center;gap:14px;
    padding:18px 22px;margin-top:14px;border-radius:16px;
    background:var(--toast-bg);color:var(--toast-fg);
    border:1px solid rgba(255,255,255,.18);box-shadow:0 14px 40px rgba(0,0,0,.45);
    font-family:var(--display,inherit);font-size:clamp(19px,2.2vw,28px);line-height:1.25;
    font-weight:700;text-align:left;
    transition:transform .28s cubic-bezier(.2,.9,.3,1.2),opacity .2s;opacity:0;pointer-events:none}
  #wwBanner.show{transform:translateX(-50%) translateY(0);opacity:1;pointer-events:auto}
  #wwBanner .ann-x{flex:none;cursor:pointer;border:0;background:rgba(0,0,0,.18);color:inherit;
    font-size:20px;line-height:1;padding:10px 13px;border-radius:10px;font-family:inherit}
  #wwBanner .ann-x:hover{background:rgba(0,0,0,.30)}
  /* Phones: a full-width strip. Most of the audience is here, and a centred pill wastes the width. */
  @media (max-width:760px){
    #wwBanner{left:8px;right:8px;transform:translateY(-120%);max-width:none;width:auto;
      font-size:clamp(17px,4.4vw,22px);padding:15px 16px;gap:10px}
    #wwBanner.show{transform:translateY(0)}
  }
  #toast.world{left:14px;transform:none;bottom:76px;text-align:left;max-width:min(320px,70vw)}
  /* ⛔ …AND ON DESKTOP IT MUST CLEAR THE LEFT RAIL, which now stands exactly where it landed.
     Measured at 1440×900 before this rule: the "👋 Welcome, <name>" world toast covered #drCrowd
     completely. ⚠️ Invisible to a hit-test — the toast is `pointer-events:none`, so
     `elementsFromPoint` reported the button reachable while it sat under a yellow card; only
     comparing RECTANGLES found it. The rail is 56 wide at left:18, so 86 is its right edge + 12.
     ⚠️ Desktop-scoped on purpose: a phone has no left rail and 86px there is just off-centre. */
  #game:not(.mobile) #toast.world{left:86px}
  #toast.world.raised{bottom:158px}
  /* mud-room minimalism: orientation + welcome only, both enlarged */
  #game.mudmode #attChip{display:none}
  #game.mudmode #chatToggle,#game.mudmode #ambBtn,#game.mudmode #muteBtn,#game.mudmode #chatPanel{display:none}
  #game.mudmode #mudChip{transform:translateX(-50%) scale(1.4);transform-origin:top center;top:44px}
  #game.mudmode #toast{font-size:18px;padding:17px 26px;bottom:154px;border-width:2px}

  /* room-transition loader */
  #roomLoad{position:fixed;inset:0;background:var(--ink);display:none;flex-direction:column;align-items:center;justify-content:center;z-index:50;transition:opacity .35s}
  #roomLoad .rl-spin{width:44px;height:44px;border-radius:50%;border:4px solid var(--line);border-top-color:var(--gold);animation:spin .8s linear infinite}
  @keyframes spin{to{transform:rotate(360deg)}}
  #roomLoad .rl-t{margin-top:18px;font-family:var(--pixel);font-size:12px;letter-spacing:3px;color:var(--gold)}
  /* ⛔ NO SPINNER FOR A ROOM YOU WALKED INTO. `#roomLoad` is also the fast-travel cover, where a
     spinner is right because the world is being rebuilt around a jump. The corridor exit borrows
     only the fade — a 220ms flash of a progress indicator would be a new jarring thing in place
     of the one it replaces. */
  /* ⛔⛔ A COVER MUST OUTRANK WHAT IT COVERS, AND THIS ONE DID NOT. `#roomLoad` is z-index 50 and
     the corridor card is `calc(var(--z-toast) + 1)` = 91, so raising the cover over the corridor
     exit hid the ROOM and left the CARD painted on top of it — measured, and visible in the 60ms
     capture as a fully legible offer card over a dark screen.
     ⭐ Scoped to `.plain`, which is the corridor-exit use only. Fast travel keeps the layering it
     has always had: nothing here measured that path, and changing another lane's transition on the
     way past is how a fix becomes two. ⚠️ It is a real lead though — during a fast travel the toast
     (90) and chat (80) also paint over that cover, and nobody has looked at whether they should.
     ⚠️ +3 puts it above the card (+1) and the action button (+2) and below the admin toast (99),
     the mwatch controls (100) and checkout (120) — a cover for a room change has no business
     hiding a checkout. */
  #roomLoad.plain{z-index:calc(var(--z-toast) + 3)}
  #roomLoad.plain .rl-spin,#roomLoad.plain .rl-t{display:none}

  /* proximity panels (non-blocking, auto-close on walk-away) */
  .panel{position:absolute;left:50%;bottom:var(--panel-anchor,110px);transform:translateX(-50%) translateY(16px);width:min(390px,90vw);background:var(--paper);color:#241E10;border-radius:16px;padding:20px 22px;opacity:0;pointer-events:none;transition:all .32s cubic-bezier(.3,1.35,.55,1);box-shadow:0 20px 60px rgba(0,0,0,.55)}
  .panel.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:auto}

  /* ⛔⛔ THE BOOKING PANEL GROWS UPWARD OFF THE TOP OF THE SCREEN AND TAKES ITS OWN ✕ WITH IT.
     Mo, walking the real funnel: "half of it is cut off, I can't scroll up, I can't do anything…
     the only way out is by clicking away from it."
     ⭐ ONE BUG, NOT TWO. #778 shipped the ✕ and he still could not see it, because `.panel` is
     `position:absolute; bottom:110px` with NO max-height: the box is anchored at the BOTTOM, so
     extra content extends the TOP past the viewport, and `.p-close` at `top:6px` rides off with
     it. Nothing scrolls because an absolutely-positioned panel is not in any scroll container.
     ⚠️ The GHL iframe carries a FIXED inline height on purpose (renderBookPanel's no-`message`
     rule — their embed script would size it, and we refuse to listen to the frame), so the panel
     cannot size itself to its content. The cap has to come from the viewport instead.
     ➜ Same derivation as #confirmPanel.checkout: the 110px anchor plus 24px of breathing room.
     The FRAME scrolls, never the panel, so the ✕ and the title stay put — a control that scrolls
     out of reach is the bug being fixed, not a resolution of it. */
  #bookPanel{max-height:calc(100vh - 110px - 24px);display:flex;flex-direction:column}
  /* ⛔⛔ ONE SCROLLBAR, AND IT IS THE EMBED'S. Matthew: "There's 2 scroll bars on the popup for the
     book a call... Either the embed for the calendar needs to be scrollable or the popup itself,
     NOT BOTH." MEASURED: this wrapper carried `overflow-y:auto` (mine, #810) while the GHL iframe
     is built at a FIXED height (`IS_MOBILE?430:560`) and scrolls internally — so whenever the
     available space was under that height, both scrolled. Two bars, one list.
     ⭐ THE EMBED'S SCROLL IS THE ONE THAT CANNOT GO. The fixed height exists because we refuse to
     listen for `highlevel.setHeight` — the same script writes contact fingerprints to localStorage,
     and a resize listener is one edit from a listener that trusts the frame. So the iframe fills
     this box and scrolls itself; this box does not.
     ⚠️ `min-height:0` stays and is load-bearing: without it a flex child refuses to shrink below its
     content and the panel grows instead of the frame scrolling. */
  /* A COLUMN, so the iframe can flex and its siblings — the email hint and the fallback link —
     keep their own height instead of being pushed out under `overflow:hidden`. */
  #bookFrame{flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column}
  /* ⛔⛔ ON A LAPTOP THE CALENDAR WAS A LETTERBOX. Mo photographed the desktop pitch: "defs needs to
     be easier to use and clearer." MEASURED on 1440×900 — panel 390×266 (27% of the width, 30% of
     the height) with the IFRAME AT 346×150. A booking calendar in a 150px-tall box is a calendar
     nobody can see the slots in, which is why his content was clipped.
     ⭐ The 390px comes from `.panel{width:min(390px,90vw)}`, a phone-shaped default inherited by a
     surface that is not a phone. Height was content-driven, so the flex column never grew.
     ⛔ AND THE z LIFT FIXES A SECOND, SEPARATE STATE: with the video FULL SCREEN (z60) the panel sat
     UNDERNEATH it at 89px — the same thing Mo reported on a phone and #916 fixed there only.
     Mobile bands the video via --vidh; desktop has no band, so the panel goes ON TOP. It is the
     conversion, and nothing should cover it.
     ⚠️ Mobile is untouched: `#game.mwatch.booking #bookPanel` still owns the phone, and these two
     selectors cannot both match. */
  /* ⛔⛔ THIS HEIGHT IS LOAD-BEARING, AND THE OBVIOUS PLACE TO PUT IT INSTEAD DOES NOTHING.
     Without a definite height HERE the whole chain collapses: #bookIframe carries height:100%,
     #bookFrame is `flex:1 1 0%` so its height comes from THIS panel, and a content-sized panel
     gives the percentage no definite ancestor — so the iframe falls back to the HTML DEFAULT OF
     150px and the panel then sizes itself around that. It is circular, and the widget renders the
     whole calendar into the 150px box, so all an attendee sees is the vendor's butterfly logo on
     white. Mo walked exactly that on the desktop pitch.
     ⭐ MEASURED AGAINST THIS RULE, on the CDN at 1440x900, iframe height:
         as shipped                             panel 766 -> frame 677 -> iframe 609   works
         this rule removed (height:auto)        panel 307 -> frame 218 -> iframe 150   collapsed
         ...plus #bookFrame{height:560px}       panel 307 -> frame 218 -> iframe 150   NO CHANGE
     ⛔ THAT THIRD ROW IS THE TRAP. Putting the height on #bookFrame is the obvious fix and it has
     ZERO effect, because `flex:1 1 0%` means the flex container decides that item's height and an
     explicit height on it is overridden. The definite height has to be on the PANEL.
     ⛔ AND DO NOT "SIMPLIFY" THIS BY GIVING THE IFRAME A FIXED PIXEL HEIGHT INSTEAD.
     #bookFrame is overflow:hidden, so a frame taller than the wrapper is clipped and unreachable —
     and the wrapper's own scrollbar was one of the two that #895 was fixing.
     ⭐⭐ THE HISTORY, BECAUSE IT IS THE CLEAREST INSTANCE OF THE ACCIDENTAL-CONSTRAINT SHAPE THIS
     FILE HAS. The iframe used to carry `height:<h>px` (560 desktop). #895 replaced it with
     height:100% to kill a genuine double scrollbar — correctly. But that fixed height was doing a
     SECOND, UNWRITTEN JOB: it was the only definite height in the chain. Removing the double
     scrollbar removed the calendar with it, on desktop only, and nothing failed — no error, no
     test, a panel that opened and looked like it was loading forever.
     ⭐ Mobile was never affected: #game.mwatch.booking #bookPanel gives the phone a definite
     height, so the percentage resolves there. A bug that is invisible on the surface most people
     test is the reason this is written at the rule rather than in a doc. */
  #game:not(.mobile) #bookPanel{
    width:min(920px,76vw);
    height:min(820px,calc(100vh - 110px - 24px));
    z-index:100}
  /* ⛔ 44px TOUCH TARGET, and the glyph is only 20px of it. CLAUDE.md §12's audience note is
     18px+ text for a non-technical, older audience; a close control they cannot hit reliably is
     the same trap as one they cannot see. `.panel` is position:absolute, so this anchors to it. */
  .panel .p-close{position:absolute;top:6px;right:8px;width:44px;height:44px;background:none;border:0;
    font-size:20px;line-height:44px;color:#A79C82;cursor:pointer;z-index:3}
  .panel .p-close:hover{color:#241E10}
  .panel .p-eyebrow{font-family:var(--pixel);font-size:10px;letter-spacing:2px;color:#A0752B;margin-bottom:6px}
  .panel h3{font-family:var(--display);font-size:20px;line-height:1.15;margin-bottom:8px}
  .panel p{font-size:14px;line-height:1.5;color:#5B5340}
  .panel .btn-cta{margin-top:14px;width:100%;font-size:15px}
  /* 3 Oct 2026 — the offer at the desk (#offerPanel, built in game.js; docs/offers.md): a sub-title, a line above two
     buttons, and the second button (Buy now) in the panel's dark ink so the two read as two different things */
  #offerPanel .of-sub{font-weight:700;color:#3A3222;margin-bottom:6px}
  #offerPanel .of-desc{white-space:pre-wrap;overflow-wrap:anywhere}
  #offerPanel .of-line{margin-top:12px;font-weight:700;color:#241E10}
  #offerPanel .of-btns{display:flex;flex-direction:column;gap:4px}
  #offerPanel a.btn-cta{display:block;text-align:center;text-decoration:none;box-sizing:border-box}
  #offerPanel .btn-cta.of-buy{background:#143029;color:var(--gold);box-shadow:0 6px 0 #0A1B17}
  #offerPanel .btn-cta.of-buy:active{box-shadow:0 2px 0 #0A1B17}
  #offerPanel .of-note{margin-top:10px;font-size:12.5px;color:#8A8169;text-align:center}
  /* 15 Oct 2026 — the tiers: two or three offers side by side at the desk (game.js openTiersPanel), the recommended one raised */
  #offerPanel.of-wide{width:min(980px,94vw);max-height:calc(100vh - 140px);overflow-y:auto}
  #offerPanel .of-tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:6px;align-items:stretch}
  #offerPanel .of-tiers .of-wrap{border:1px solid rgba(36,30,16,.16);border-radius:14px;padding:14px 14px 12px;display:flex;flex-direction:column;background:rgba(255,255,255,.35)}
  #offerPanel .of-tiers .of-wrap .of-btns{margin-top:auto}
  #offerPanel .of-tiers .of-wrap.of-star{border:2px solid var(--of-accent,#C9A227);box-shadow:0 10px 26px rgba(0,0,0,.18);background:rgba(255,255,255,.6)}
  #offerPanel .of-tiers .of-img{max-height:130px;width:100%;object-fit:cover;border-radius:10px}
  /* 6 Oct 2026 — the offer's own look: a picture at the top, one accent colour (the eyebrow, the book button, the buy button's words) */
  /* 6 Oct 2026 — the host's chat inside the world: a private line, the host's two controls on a line, the Team tab */
  #chatMsgs .cm.cm-private{background:rgba(255,215,120,.10);border-left:2px solid var(--gold);padding-left:6px;border-radius:0 6px 6px 0}
  #chatMsgs .cm-pv{display:inline-block;font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase;color:var(--gold);margin-right:6px}
  #chatMsgs .cm-pr,#chatMsgs .cm-x{float:right;background:none;border:0;color:#9a9076;cursor:pointer;font-size:12px;padding:0 4px;line-height:1.2}
  #chatMsgs .cm-x[data-armed="1"]{color:#e0a080;font-size:11px}
  #chatMsgs .cm a{color:var(--gold);text-decoration:underline;overflow-wrap:anywhere}
  #chatReply.cr-private{border-left:2px solid var(--gold)}
  #chatTabs{display:flex;gap:6px;padding:6px 8px 2px}
  #chatTabs button{background:none;border:1px solid rgba(255,255,255,.18);color:#d8d2bf;border-radius:999px;padding:3px 10px;font-size:11px;cursor:pointer}
  #chatTabs button[aria-pressed="true"]{background:var(--gold);color:#143029;border-color:var(--gold)}
  #chatMsgs[data-view="team"] .cm:not(.cm-team){display:none}   /* the tab decides which LINES show; the panel itself is never hidden by this (suite-chat-video-states C4) */
  #chatMsgs:not([data-view="team"]) .cm-team{display:none}
  #chatMsgs .cm-team{background:rgba(120,200,255,.08);border-left:2px solid #8fd3ff;padding-left:6px;border-radius:0 6px 6px 0}
  #chatMsgs .cm-team .cm-pv{color:#8fd3ff}
  #offerPanel .of-wrap{--of-accent:var(--cta);--of-ink:var(--on-accent)}   /* the defaults = the theme; an offer's own colour comes as an inline style on this element */
  #offerPanel .of-img{display:block;width:100%;max-height:170px;object-fit:cover;border-radius:12px;margin:2px 0 12px}
  #offerPanel .of-wrap[style*="--of-accent"] .p-eyebrow{color:var(--of-accent)}
  #offerPanel .of-wrap[style*="--of-accent"] [data-offer-go="book"]{background:var(--of-accent);color:var(--of-ink);box-shadow:0 6px 0 color-mix(in srgb,var(--of-accent) 55%,#000)}
  #offerPanel .of-wrap[style*="--of-accent"] .btn-cta.of-buy{color:var(--of-accent)}
  /* 3 Oct 2026 — the recording room (#recPanel, built in game.js; docs/recording-room.md): the player fills the panel's
     width at 16:9, the same dark glass and thin neon bezel as the stage screen; wider than the other panels so a
     recording is watchable, never wider than the screen */
  #recPanel{width:min(680px,calc(100vw - 32px))}
  #recPanel .rec-frame{position:relative;width:100%;aspect-ratio:16/9;margin-top:12px;background:#0B1114;border:1.5px solid rgba(0,229,255,.6);border-radius:8px;overflow:hidden}
  #recPanel .rec-frame video,#recPanel .rec-frame iframe{position:absolute;inset:0;width:100%;height:100%;border:0;background:#0B1114}
  #recPanel .btn-ghost{margin-top:12px}
  #recPanel a.rec-out{color:#241E10;font-weight:700}
  .res-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:12px;padding:10px 12px;background:#EFE5CC;border-radius:10px}
  .res-row .r-name{font-size:14px;font-weight:600}
  .res-row .r-sub{font-size:11px;color:#8A7B58}
  .res-dl{background:var(--ink);color:var(--paper);font-size:12px;font-weight:700;border-radius:8px;padding:9px 12px;white-space:nowrap}
  /* box Contents (admin-authored text/video/file) — Task 1's render targets */
  .r-text{font-size:14px;line-height:1.5;color:#5B5340;margin-top:10px;white-space:pre-wrap}
  .r-video{margin-top:12px;border-radius:10px;overflow:hidden;background:#000}
  .r-video iframe{display:block;width:100%;aspect-ratio:16/9;border-radius:10px;border:0}
  .res-dl-cfg{background:var(--ink);color:var(--paper);font-size:12px;font-weight:700;border-radius:8px;padding:9px 12px;white-space:nowrap;border:none;cursor:pointer}
  .quiz-opt{display:block;width:100%;text-align:left;margin-top:9px;padding:12px 14px;background:#EFE9D2;border:1px solid #DDD2B0;border-radius:10px;font-size:14px;font-weight:600;color:#3A3020}
  .quiz-opt:active{background:var(--gold)}
  .sched{display:flex;justify-content:space-between;font-size:13px;margin-top:9px;padding-bottom:8px;border-bottom:1px dashed #D9CBA8}
  .sched b{color:#3A3020}

  /* hall-of-fame card — floating & non-blocking; closes when you walk away */
  /* ⛔⛔ THE CARD HAD NO CEILING AND CLIPPED ITSELF AT BOTH ENDS. Third-party QA, Chrome on a
     Windows laptop: "the popup isn't fully contained in the viewport, it's getting cut off, so the
     full content isn't visible."
     ⭐ MEASURED at 834x720 with a real testimonial (hero + quote + story + result screenshot): the
     card came out 756px tall in a 720px viewport — `max-height:none` and `overflow:hidden`, centred
     on `top:50%`, so it overflowed 33px ABOVE and 3px below AND COULD NOT BE SCROLLED. Content
     length is the variable, so this gets worse the better the testimonial is.
     ⛔⛔ AND IT DEFEATED #943's ✕: that button is pinned top-right of the card, which is precisely
     the strip that goes off the top — measured `close_button_visible:false`. A close control the
     attendee cannot see is the same as not having one, so the card fix and the ✕ are one fix.
     ➜ A CEILING PLUS AN INTERNAL SCROLLER. The card becomes a flex column capped to the viewport;
     `.hof-body` takes the overflow. The hero keeps its own height so the photo or video is never
     squashed, and the ✕ stays pinned to the CARD rather than scrolling away with the text.
     ⚠️ `dvh` with a `vh` fallback: mobile browser chrome makes `vh` overshoot, which would put the
     ceiling below the viewport and reintroduce exactly this. */
  #hofCard{position:absolute;left:50%;top:50%;transform:translate(-50%,-52%) scale(.97);width:min(400px,92vw);
    max-height:calc(100vh - 32px);max-height:calc(100dvh - 32px);display:flex;flex-direction:column;
    background:var(--paper);color:#241E10;border-radius:18px;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,.6);
    opacity:0;pointer-events:none;transition:all .3s cubic-bezier(.3,1.35,.55,1);z-index:15}
  #hofCard.show{opacity:1;transform:translate(-50%,-52%) scale(1);pointer-events:auto}
  /* ⛔ min-height, NOT height — the box sizes to its media so a portrait photo or video is shown
     WHOLE instead of centre-cropped, and 170px stays as the floor for the placeholder and empty
     states so nothing has to toggle a class. */
  #hofCard .hof-hero{position:relative;min-height:170px;background:#14332C}
  /* ⛔ HIDDEN UNTIL IT HAS A SOURCE, exactly as .hof-result beside it already is. #hofImg ships
     with NO src and display:block; it is invisible today only because .hof-hero sizes it and the
     alt text has nowhere to render — one layout change from a broken-image icon with the words
     "Featured client" in a client's Hall of Fame. openTrophy() sets display:block the moment it
     assigns a src (and 'none' when a video takes over), so nothing needs to change there. */
  /* ⛔⛔ THE ROOT CAUSE WAS NEVER THIS RULE. Mo's complaint is about the VIDEO, whose
     `object-fit:cover` was an INLINE style on <video id="hofVid"> in index.html — and inline beats
     the stylesheet, so ANY FIX THAT EDITED ONLY THIS FILE WOULD HAVE BEEN INERT. The inline sizing
     had to be REMOVED, not overridden. This img rule was always correct for images; it is widened
     here so both media behave the same way.
     ⭐ WHY `width:auto` RATHER THAN PLAIN `contain` IN A FIXED BOX: `contain` alone LETTERBOXES —
     bars above and below, wasted vertical space. Letting the element size to its own aspect makes a
     portrait file render full-height and narrow, PILLARBOXED on the hero's existing #14332C, so the
     whole frame is visible with no wasted height. `object-fit:contain` stays as a no-op safety net
     for the clamped case. Same pattern `.hof-result` already uses two rules below. */
  /* ⚠️ TWO RULES, NOT ONE COMBINED SELECTOR, AND THAT IS DELIBERATE. Merging them into
     `.hof-hero img, #hofVid{…}` is tidier and breaks `suite-branding`'s hero-image guard, which
     matches `#hofCard .hof-hero img{` followed immediately by its block. The property it protects
     — something decides whether a srcless <img> renders — is unchanged either way, so the guard is
     arguably narrow; but widening a test to accommodate a stylistic preference is how a suite
     stops describing the code. Keep them separate. */
  #hofCard .hof-hero img{display:none;width:auto;max-width:100%;height:auto;
    max-height:min(52vh,380px);margin:0 auto;object-fit:contain}
  #hofCard #hofVid{width:auto;max-width:100%;height:auto;
    max-height:min(52vh,380px);margin:0 auto;object-fit:contain}
  #hofCard .hof-badge{position:absolute;top:12px;left:12px;background:var(--gold);color:#241A05;font-family:var(--pixel);font-size:10px;letter-spacing:2px;padding:6px 10px;border-radius:8px}
  #hofCard .hof-stat{position:absolute;bottom:-20px;right:18px;background:var(--cta);color:#fff;font-family:var(--display);font-weight:700;font-size:24px;padding:7px 16px;border-radius:12px;box-shadow:0 8px 24px rgba(0,0,0,.35)}
  /* ⛔ `min-height:0` IS LOAD-BEARING, NOT TIDYING. A flex child's default `min-height:auto`
     refuses to shrink below its content, so the scroller would never scroll and the card would
     overflow exactly as before — the ceiling above would be inert. */
  #hofCard .hof-body{padding:30px 22px 18px;overflow-y:auto;min-height:0;overscroll-behavior:contain}
  /* The hero keeps its intrinsic height: a portrait photo or video is shown whole (see the
     min-height note above), and it is the BODY that gives way when the card runs out of room. */
  #hofCard .hof-hero{flex:0 0 auto}
  #hofCard .hof-name{font-family:var(--display);font-weight:700;font-size:22px}
  #hofCard .hof-role{font-family:var(--pixel);font-size:10px;letter-spacing:2px;color:#A0752B;margin-top:4px}
  #hofCard .hof-quote{margin-top:10px;font-family:var(--display);font-style:italic;font-size:15px;line-height:1.4;color:#3A3020;display:none}
  #hofCard .hof-story{margin-top:12px;font-size:14px;line-height:1.55;color:#5B5340}
  #hofCard .hof-result{display:none;width:100%;max-height:200px;object-fit:contain;margin-top:12px;border-radius:10px;border:1px solid #E4D8BE;background:#0B1F1A}
  /* ⛔⛔ TWO INDEPENDENT REPORTERS ON ONE SURFACE — Matthew (QA) and ADMIN, separately: the close
     hint is tiny and low-contrast. MEASURED, not judged: #B3A47E on --paper scored 2.11-2.24 against
     WCAG AA's 4.5 for normal text (3.0 even for LARGE), at 9px, for an audience CLAUDE.md says skews
     older. The card's own body text scores 6.85 on the same background — the card could always carry
     legible text; this label had simply been styled down until it stopped being readable.
     ➜ Body colour, 11px, less tracking. And it is now the SECONDARY affordance: #hofClose is the
     obvious one, so this line no longer has to carry the whole job. */
  #hofCard .hof-walk{margin-top:12px;font-family:var(--pixel);font-size:11px;letter-spacing:1px;color:#5B5340;text-align:center}
  /* ⛔ AN ADDITION, NEVER A REPLACEMENT. Walking away is the mechanic this room is built around and
     it is untouched — see the `hit<0` branch in game.js, which still owns the close. This button
     only hides the card; it deliberately does NOT clear `curTrophy`, because the proximity loop
     re-opens any trophy whose index differs from it, so resetting would re-open the card on the very
     next frame while the attendee is still standing there.
     ⚠️ 44px is a thumb target, not a decoration: the reported problem is a control people cannot
     find or hit on a phone. Sits top-RIGHT because .hof-badge already owns top-left. */
  #hofCard #hofClose{position:absolute;top:8px;right:8px;z-index:2;width:44px;height:44px;
    display:flex;align-items:center;justify-content:center;font-size:20px;line-height:1;
    background:rgba(10,20,16,.72);color:#F7F3E3;border:1.5px solid rgba(247,243,227,.55);
    border-radius:50%;cursor:pointer;padding:0;touch-action:manipulation}
  #hofCard #hofClose:hover{background:rgba(10,20,16,.9)}

  /* modals (stage B, success) */
  .modal-bg{position:absolute;inset:0;background:rgba(5,18,13,.74);display:none;align-items:center;justify-content:center;padding:24px;z-index:30}
  .modal-bg.show{display:flex}
  .modal-bg.soft{background:rgba(5,18,13,.30);pointer-events:none}
  .modal-bg.soft .modal{pointer-events:auto}
  .stageb-with{font-family:var(--script);font-size:24px;color:#0D7464;margin:-2px 0 8px}
  .modal{background:var(--paper);color:#241E10;border-radius:18px;padding:26px;max-width:420px;width:100%;text-align:center;box-shadow:0 30px 80px rgba(0,0,0,.6);position:relative}
  .modal h3{font-family:var(--display);font-size:24px;margin-bottom:8px}
  .modal p{font-size:15px;color:#5B5340;line-height:1.5}
  .modal .btn-cta{margin-top:18px;width:100%}
  .modal .m-close{position:absolute;top:10px;right:14px;background:none;font-size:20px;color:#A79C82}
  .modal .btn-ghost{margin-top:10px;width:100%;background:#EFE5CC;color:#5B5340;font-weight:600;padding:13px;border-radius:12px;font-size:14px}
  /* ── Buy panel (Lane 2 — Stripe embedded checkout) ── additive only */
  /* Sit ABOVE the whole game HUD. #stageAudio is z-index:40 and was rendering on
     top of the checkout; the base .modal-bg is z-index:30. Also make the backdrop
     near-opaque so the live game canvas behind doesn't bleed through (cleaner + less
     perceived lag). */
  #buyModal{z-index:120;background:rgba(5,18,13,.94)}
  /* Cap the modal to the viewport and scroll INSIDE it. Stripe's embedded checkout
     grows its own iframe height (card fields, wallets, address) — we must never fix
     that; instead .buy-body scrolls so the Pay button is always reachable. dvh keeps
     it honest under mobile browser chrome; vh is the fallback. */
  .modal.buy{max-width:460px;text-align:left;display:flex;flex-direction:column;
    max-height:calc(100vh - 48px);max-height:calc(100dvh - 48px);overflow:hidden}
  .modal.buy .m-close{z-index:2}
  .buy-body{overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;min-height:0;flex:1 1 auto}
  .modal.buy .buy-body>div>h3{text-align:center}
  .buy-prod{display:flex;align-items:center;gap:12px;justify-content:space-between;padding:12px 14px;border:1px solid #E6DCC2;border-radius:12px;margin-top:10px}
  .buy-prod-main{min-width:0}
  .buy-prod-name{font-weight:800;color:#241E10;font-size:15px}
  .buy-prod-desc{font-size:12px;color:#8A7F63;margin-top:2px;line-height:1.4;text-transform:uppercase;letter-spacing:.04em;font-weight:700}
  .buy-prod-rooms{font-size:12px;color:#569241;font-weight:700;margin-top:5px}
  /* .modal.buy scope beats the generic `.modal .btn-cta{width:100%}` (0-2-0) that was
     forcing this price button full-width → overlapping the product description. */
  .modal.buy .buy-prod-buy{margin-top:0;width:auto;flex-shrink:0;align-self:center;font-size:15px;padding:11px 18px;border-radius:10px;box-shadow:0 4px 0 var(--cta-dark)}
  .modal.buy .buy-prod-buy:active{transform:translateY(3px);box-shadow:0 1px 0 var(--cta-dark)}
  #buyStripeMount{min-height:280px}
  #buyWait,#buyMsg{text-align:center;padding:6px 0}
  .buy-spinner{width:34px;height:34px;border:3px solid #E6DCC2;border-top-color:#569241;border-radius:50%;margin:8px auto 16px;animation:buySpin .8s linear infinite}
  @keyframes buySpin{to{transform:rotate(360deg)}}
  /* While the buy modal is open, hide the floating HUD controls entirely (belt-and-
     suspenders with the z-index above) — same principle as the keydown suppression.
     Reversible: the class is removed on close. */
  #game.buying #stageAudio,#game.buying #stageBar,#game.buying #tapOpenBtn,
  #game.buying #bigClose,#game.buying #mobRail,#game.buying #joyWrap{display:none!important}
  #stagebCountdown{font-family:var(--display);font-weight:700;font-size:34px;color:var(--cta-dark);margin:14px 0 4px}
  #stagebSub{font-family:var(--pixel);font-size:10px;letter-spacing:2px;color:#A0752B}

  /* fullscreen video overlay */
  #videoOverlay{position:fixed;inset:0;background-color:#06120E;background-size:cover;background-position:center;display:none;flex-direction:column;z-index:60}
  #chatPanel{z-index:80}
  #videoOverlay.show{display:flex}
  .vid-stage{flex:1;position:relative;display:flex;align-items:center;justify-content:center;overflow:hidden}
  .vid-glow{position:absolute;inset:0;background:radial-gradient(60% 55% at 50% 42%, rgba(60,150,110,.30), transparent 70%)}
  .vid-slide{position:relative;width:min(1000px,90vw);aspect-ratio:16/9;background:linear-gradient(160deg,#12302A,#0A1F1A);border:1px solid #2E5A48;border-radius:14px;padding:min(6vw,54px);display:flex;flex-direction:column;justify-content:center;box-shadow:0 30px 80px rgba(0,0,0,.6)}
  #demoVid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;border-radius:14px;background:#000;display:none;z-index:1}
  .vid-live{z-index:2}
  .vid-presenter{z-index:2}
  .vid-kicker{font-family:var(--pixel);font-size:13px;letter-spacing:3px;color:var(--gold);margin-bottom:14px}
  .vid-kicker.pitch{color:var(--cta)}
  .vid-title{font-family:var(--display);font-weight:700;font-size:clamp(22px,4vw,42px);line-height:1.1}
  .vid-bars{margin-top:26px;display:flex;flex-direction:column;gap:10px;max-width:70%}
  .vid-bar{height:9px;border-radius:5px;background:rgba(143,168,154,.28)}
  .vid-live{position:absolute;top:18px;right:20px;font-family:var(--pixel);font-size:12px;color:var(--cta);display:flex;align-items:center;gap:7px}
  .vid-live .dot{width:9px;height:9px;border-radius:50%;background:var(--cta);animation:blink 1.2s infinite}
  .vid-presenter{position:absolute;bottom:16px;left:20px;display:flex;align-items:center;gap:10px;color:var(--muted);font-size:13px}
  .vid-presenter .pfp{width:34px;height:34px;border-radius:50%;background:linear-gradient(145deg,#F9C85E,#0D7464)}
  #theaterCv{position:absolute;inset:0;width:100%;height:100%;display:none}
  .vid-controls{background:#081712;border-top:1px solid #1E4034;padding:12px 18px;display:flex;align-items:center;gap:14px}
  .vid-play{width:40px;height:40px;border-radius:50%;background:var(--paper);color:#0E1F1A;font-size:16px;display:flex;align-items:center;justify-content:center;flex:0 0 auto;cursor:pointer}
  .vid-track{flex:1;height:6px;border-radius:3px;background:#1E4034;position:relative;overflow:hidden}
  .vid-fill{position:absolute;left:0;top:0;bottom:0;width:0;background:var(--cta);border-radius:3px}
  .vid-time{font-family:var(--pixel);font-size:11px;color:var(--muted);flex:0 0 auto}
  .vid-mode,.vid-close{background:var(--ink-2);color:var(--paper);border:1px solid var(--line);border-radius:10px;padding:9px 14px;font-size:13px;font-weight:600;flex:0 0 auto}
  .vid-note{position:absolute;bottom:16px;right:20px;font-size:11px;color:#41604F}
  /* Transient chat bubbles over the video's lower edge (mobile webinar, chat closed). Twitch-style.
     pointer-events:none on the container AND each bubble → taps pass through to the video controls. */
  #chatBubbles{position:absolute;left:8px;right:8px;bottom:8px;z-index:3;display:flex;flex-direction:column;gap:5px;align-items:flex-start;pointer-events:none}
  #chatBubbles .cbub{pointer-events:none;max-width:90%;background:rgba(6,16,12,.86);border:1px solid rgba(46,90,72,.6);border-radius:12px;padding:5px 10px;font-size:12px;line-height:1.3;color:var(--paper);box-shadow:0 4px 12px rgba(0,0,0,.4);opacity:0;transform:translateY(6px);transition:opacity .2s ease,transform .2s ease}
  #chatBubbles .cbub.in{opacity:1;transform:none}
  #chatBubbles .cbub .nm{font-weight:700;color:var(--gold);margin-right:5px}

  /* ---------- Walk Mode — desktop picture-in-picture (top-right) ----------
     .walk turns the full-screen overlay into a CORNER CARD, not a full-width band: a 16:9 video in a
     thin bar is only ~390px wide, so a full-width band is ~75% black void. PiP docks the player top-right
     at a comfortable fixed size, floating over the still-walkable world — far less occlusion, no void.
     The world canvas is UNCHANGED underneath (§5.7 option a). #buyModal (z-index:120) still stacks above
     it, and .walk is NOT in the #game.buying hide rule, so the player survives checkout. Logic is
     unchanged — this is only what .walk renders as. Mobile keeps the full-bleed top band (§7, below). */
  #game.walkmode{--pip-w:480px}                            /* PiP width — comfortable to watch while walking; shared so the admin pill can clear it */
  #videoOverlay.walk{
    inset:auto;top:calc(16px + env(safe-area-inset-top,0px));right:16px;left:auto;bottom:auto;
    width:var(--pip-w,480px);height:auto;
    border-radius:14px;overflow:hidden;border:1px solid rgba(46,90,72,.55);
    box-shadow:0 18px 44px rgba(0,0,0,.55)
  }
  #videoOverlay.walk .vid-stage{flex:none;width:100%;height:calc(var(--pip-w,480px) * 9 / 16);margin:0;padding:0;border-radius:0}   /* 16:9 video, full card width — no pillarbox */
  #videoOverlay.walk .vid-slide{width:100%;height:100%;max-width:none;padding:0;border:none;border-radius:0;box-shadow:none}   /* video fills the card */
  #videoOverlay.walk .vid-controls{width:100%;margin:0;padding:8px 10px;gap:10px}   /* controls BENEATH the video, contained by the card */
  /* Even at 480px the full control row (~510px) overflows the card, so "Back to full screen" clipped.
     Drop the timestamp + volume slider — the least load-bearing and the widest — so nothing clips (mute
     stays reachable on the world HUD). .vid-mode stays one line so "Back to full screen" is fully readable.
     Kept: play/pause, scrubber, Back-to-full-screen, Chat. */
  #videoOverlay.walk #vidTime{display:none}
  #videoOverlay.walk .vid-mode{white-space:nowrap}
  /* Admin pill sits top-right (z-index:70, hidden for attendees) — shift it clear of the PiP in Walk Mode. */
  #game.walkmode .admin-corner{right:calc(var(--pip-w,480px) + 32px)}
  /* C-9 — compact the fallback slide card so kicker/title/bars don't overflow the small PiP card */
  #videoOverlay.walk .vid-note,#videoOverlay.walk .vid-presenter,#videoOverlay.walk .vid-bars{display:none}
  #videoOverlay.walk .vid-title{font-size:clamp(14px,2.2vw,20px);margin:0}
  #videoOverlay.walk .vid-kicker{font-size:11px;margin-bottom:6px}
  /* Control swap: no closing and no theater while Walk Mode is on (banded OR expanded); show the expand toggle. */
  #walkExpandBtn{display:none}
  #game.walkmode #vidClose,#game.walkmode #vidModeBtn{display:none}
  #game.walkmode #walkExpandBtn{display:inline-flex;align-items:center}
  /* The attendee's own Escape-minimise reuses the band, but NOT `.walkmode` — that hides #vidClose,
     which is right when the presenter banded you and wrong when you did it yourself. So `.vidmin`
     shows the expand control and leaves the close button alone. */
  #game.vidmin #walkExpandBtn{display:inline-flex;align-items:center}
  /* ⛔ THE CORNER CARD CAN BE HIDDEN (22 Oct 2026; JT: "anything that's unclosable … pisses me off"; Ahmed: "I can't minimise the
     pop-up"). − on the picture hides the picture only: the card folds into its control row (❚❚ · Show video · Back to full screen ·
     Chat) and the same <video> keeps playing, so the host is still heard. A phone has no corner card (its band closes already). */
  #vidHideBtn,#vidShowBtn{display:none}
  #videoOverlay.walk:not(.pip-hidden) #vidHideBtn{display:flex;position:absolute;top:8px;right:8px;z-index:6;width:30px;height:30px;padding:0;align-items:center;justify-content:center;border-radius:50%;border:1px solid rgba(255,255,255,.35);background:rgba(4,10,8,.72);color:#fff;font:700 18px/1 system-ui,sans-serif;cursor:pointer}
  #videoOverlay.walk:not(.pip-hidden) #vidHideBtn:hover{background:rgba(4,10,8,.92)}
  #videoOverlay.walk.pip-hidden{width:auto}
  #videoOverlay.walk.pip-hidden .vid-stage,#videoOverlay.walk.pip-hidden .vid-track{display:none}
  #videoOverlay.walk.pip-hidden #vidShowBtn{display:inline-flex;align-items:center}
  #videoOverlay.walk .vid-live{right:48px}                     /* clear of the − */
  #videoOverlay.walk #vidModeBtn{display:none}                 /* the card's row overflowed (Chat was cut); theater is one press away from Back to full screen */
  /* ⛔ WATCH FULL SCREEN FILLS THE SCREEN ON A COMPUTER (22 Oct 2026; Ahmed: "full screen should take the whole width"). The slide was
     capped at 1000px, so a big screen showed the stream in a box with margins, even in the browser's own full screen. In the screen
     view (not the corner card, not a phone) the slide fills the stage and the stream is letterboxed, never cropped. Theater view
     hides the slide (its canvas draws the room), so it is untouched. */
  #game:not(.mobile) #videoOverlay:not(.walk) .vid-slide{width:100%;height:100%;max-width:none;aspect-ratio:auto;border:0;border-radius:0;box-shadow:none}
  #game:not(.mobile) #videoOverlay:not(.walk) #demoVid{object-fit:contain;border-radius:0}
  /* Mobile band (§7): full-bleed video, top-docked, safe-area padding; height sized to the video
     (100vw*9/16) capped at 32vh. A TOP band collides with neither #stageBar nor #mobRail (both bottom-fixed). */
  @media (max-width:640px), (pointer:coarse) and (max-height:480px){
    /* Walk Mode band RETIRED on mobile: walk_mode governs world-walkability only; the video is the three-state
       Twitch layout (mwatch), never a band. Reset the desktop PiP card to a plain full-screen overlay here;
       while watching, the #game.mwatch rule below (higher specificity) takes over the sizing. */
    #videoOverlay.walk{inset:0;top:auto;left:auto;right:auto;bottom:auto;width:auto;height:auto;border:none;border-radius:0;box-shadow:none}
    #game.walkmode #vidClose{display:inline-flex}   /* mobile: no band → dismiss (✕) must work even in Walk Mode */
    #game.walkmode #walkExpandBtn{display:none}      /* mobile: no band → nothing to expand */
    /* ⭐ …but the ATTENDEE'S OWN band (#game.vidmin) is a different thing from the retired Walk
       Mode band, and it needs a way back to full screen. #walkExpandBtn is already wired to
       restoreVideo() when vidMinimized (see its onclick), so showing it here is the whole fix. */
    #game.vidmin #walkExpandBtn{display:inline-flex;align-items:center}
  }

  /* ---- Mobile webinar/pitch — the three states ----
     #game.mwatch = watching. --vidh (px, from visualViewport.height in layoutMobileWatch) is the split line.
     Chat CLOSED → --vidh = full → video owns the whole visible view (world hidden, bubbles over it).
     Chat OPEN  → --vidh = top-third → video band on top, chat pane fills below. Video dismissed → no mwatch → world.
     Applies in Walk Mode too (the band is retired); the reset (width/border/radius/shadow) neutralises the
     desktop PiP card, which .walk would otherwise impose. Higher specificity than #videoOverlay.walk, so it wins. */
  @media (max-width:640px), (pointer:coarse) and (max-height:480px){
    #game.mwatch #videoOverlay{inset:auto;top:0;left:0;right:0;bottom:auto;width:auto;height:var(--vidh,100%);border:none;border-radius:0;box-shadow:none;transition:height .28s ease}
    #game.mwatch.chatopen #chatPanel{top:var(--vidh,66vh);bottom:0;left:0;right:0;width:auto;height:auto;transform:none;border-radius:0;z-index:80}
/* ⭐ THE BOOKING PANEL TAKES THE SPACE UNDER THE BAND, exactly as chat does above — same split, same
   variable, so there is ONE answer to "video plus a form on a phone" rather than two that drift.
   ⚠️ THE PANEL MUST NOT SCROLL (#895): the GHL embed scrolls INTERNALLY, and a scrollbar on the
   panel as well traps people at the moment they convert. The panel is a flex column, #bookFrame
   flexes to fill it, and the frame keeps its own overflow:hidden. */
#game.mwatch.booking #bookPanel{top:var(--vidh,66vh);bottom:0;left:0;right:0;width:auto;height:auto;max-height:none;transform:none;border-radius:0;overflow:hidden;z-index:80}
    #game.mwatch.chatopen #chatJump{bottom:56px}
    /* ⛔ THE PICTURE THREW AWAY 10% OF THE PHONE'S WIDTH. JT: theater view is too small, and the
       video sits too high. Both are this one rule. `.vid-slide` is `width:min(1000px,90vw)` — a
       sensible inset for a centred slide in a DESKTOP full-screen overlay, and on a 390px phone
       it is 351px. Because the slide is `aspect-ratio:16/9`, the 39px of lost width costs 19px of
       height as well, so the picture came out 351x200 inside a stage measuring 390x222.
       ⭐ MEASURED in Chrome at 390x844, theater view reached through #railChat (the real mobile
       door — #chatToggle is display:none on mobile and #vidChatBtn only exists over the video):
         band 287px = stage 222 + controls 65   ·   picture 351x200   ·   16:9 at full width = 390x219
       ⚠️ THE BAND IS NOT THE BUG, WHICH IS WHERE I EXPECTED IT TO BE. 34% of 844 is 287, and a
       full-width 16:9 picture plus the 65px control bar needs 284 — so `--vidh` was already right
       to within 3px, and widening it would have taken room from chat to fix a problem that was
       never in the split.
       ⭐ `width:100%` is the same move `#videoOverlay.walk .vid-slide` already makes for the PiP
       card ("video fills the card"), so this is the file's existing idiom for a slide whose
       container is already the right shape, not a new mechanism. */
    #game.mwatch #videoOverlay .vid-slide{width:100%}
  }
  @media (max-width:640px) and (prefers-reduced-motion:reduce), (pointer:coarse) and (max-height:480px) and (prefers-reduced-motion:reduce){
    #game.mwatch #videoOverlay{transition:none}   /* snap, never a value that could jump mid-animate */
  }

  @media (max-width:560px){ .optin{flex-direction:column} .hud-chip{padding:7px 10px;font-size:12px} #countdown{font-size:12px} #stageBtns{flex-direction:column} }
/* ===================== EMBEDDED ADMIN CONSOLE (scoped) ===================== */
#adminScreen{background:var(--ink)}
#adminScreen{
    --ink:#0E1F1A; --ink2:#16352C; --ink3:#0B1A15; --paper:#F7F3E3;
    --gold:#F9C85E; --gold-d:#C9973F; --teal:#0D7464; --lime:#AEBE37;
    --red:#C6362B; --muted:#8FA89A; --line:#2E5A48;
    --disp:'Montserrat',sans-serif; --body:'Lato',sans-serif;
  }
#adminScreen input, #adminScreen select, #adminScreen textarea{font-family:var(--body);font-size:14px;color:var(--paper);background:var(--ink3);
    border:1px solid var(--line);border-radius:9px;padding:10px 12px;outline:none;width:100%}
#adminScreen input:focus, #adminScreen select:focus, #adminScreen textarea:focus{border-color:var(--gold)}
#adminScreen textarea{resize:vertical;min-height:70px;line-height:1.5}
#adminScreen ::placeholder{color:#5A7A6A}
#adminScreen /* ---------- shell ---------- */
  .shell{display:flex;min-height:100vh}
#adminScreen .side{width:248px;flex:0 0 248px;background:var(--ink3);border-right:1px solid #1E4034;
    display:flex;flex-direction:column;position:sticky;top:0;height:100vh;overflow-y:auto}
#adminScreen .side-head{padding:20px 18px 14px;border-bottom:1px solid #1E4034}
#adminScreen .side-head .lw-top{font-family:var(--disp);font-weight:800;font-size:10px;letter-spacing:.34em;color:var(--gold)}
#adminScreen .side-head .lw-big{font-family:var(--disp);font-weight:800;font-size:18px;letter-spacing:.16em}
#adminScreen .bulbs{display:flex;gap:7px;margin-top:10px}
#adminScreen .bulb{width:7px;height:7px;border-radius:50%;background:var(--gold);opacity:.25;animation:bulb 1.6s infinite}
#adminScreen .bulb:nth-child(2n){animation-delay:.4s}
#adminScreen .bulb:nth-child(3n){animation-delay:.8s}
#adminScreen .side-sub{margin-top:8px;font-size:10px;letter-spacing:2px;color:var(--muted)}
#adminScreen nav{padding:10px 0;flex:1}
#adminScreen .nav-group{font-family:var(--disp);font-size:9px;font-weight:800;letter-spacing:.24em;color:#41604F;
    padding:14px 18px 6px}
#adminScreen .nav-btn{display:flex;align-items:center;gap:10px;width:100%;text-align:left;background:none;
    color:var(--muted);font-size:13.5px;font-weight:700;padding:10px 18px;border-left:3px solid transparent}
#adminScreen .nav-btn:hover{color:var(--paper);background:rgba(46,90,72,.16)}
#adminScreen .nav-btn.on{color:var(--gold);border-left-color:var(--gold);background:rgba(249,200,94,.06)}
#adminScreen .led{width:8px;height:8px;border-radius:50%;flex:0 0 auto;margin-left:auto}
#adminScreen .led.local{background:var(--lime);box-shadow:0 0 6px rgba(174,190,55,.6)}
#adminScreen .led.backend{background:var(--red);box-shadow:0 0 6px rgba(198,54,43,.6)}
#adminScreen .side-foot{padding:14px 18px;border-top:1px solid #1E4034;font-size:10.5px;color:#41604F;line-height:1.6}
#adminScreen .side-foot b{color:var(--muted)}
#adminScreen .legend{display:flex;flex-direction:column;gap:5px;margin-bottom:8px}
#adminScreen .legend span{display:flex;align-items:center;gap:8px}
#adminScreen /* ---------- main ---------- */
  main{flex:1;min-width:0;display:flex;flex-direction:column}
#adminScreen .topbar{position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:14px;
    padding:14px 26px;background:rgba(14,31,26,.94);backdrop-filter:blur(6px);border-bottom:1px solid #1E4034}
#adminScreen .status-chip{display:flex;align-items:center;gap:8px;font-family:var(--disp);font-weight:800;
    font-size:10px;letter-spacing:.18em;color:var(--gold);background:rgba(249,200,94,.08);
    border:1px solid rgba(249,200,94,.35);border-radius:9px;padding:8px 12px}
#adminScreen .status-chip .dot{width:8px;height:8px;border-radius:50%;background:var(--gold);animation:bulb 1.6s infinite}
/* Connected: green and steady. The amber pulse above is the DEV/not-connected state —
   it reads as "something is provisional", which is exactly wrong once saves publish to a
   client's live event. paintModeChrome() in admin.js adds this class. */
#adminScreen .status-chip.live{color:var(--lime);background:rgba(174,190,55,.10);border-color:rgba(174,190,55,.40)}
#adminScreen .status-chip.live .dot{background:var(--lime);animation:none}
#adminScreen .saved-flash{font-size:12px;color:var(--lime);opacity:0;transition:opacity .3s}
#adminScreen .saved-flash.show{opacity:1}
#adminScreen .topbar .grow{flex:1}
#adminScreen .btn{font-family:var(--disp);font-weight:800;font-size:12px;letter-spacing:.04em;border-radius:10px;padding:10px 16px}
#adminScreen .btn-gold{background:var(--gold);color:#143029;box-shadow:0 4px 0 var(--gold-d)}
#adminScreen .btn-gold:active{transform:translateY(3px);box-shadow:0 1px 0 var(--gold-d)}
#adminScreen .btn-ghost{background:var(--ink2);color:var(--paper);border:1px solid var(--line)}
#adminScreen .btn-red{background:transparent;color:var(--red);border:1px solid var(--red)}
#adminScreen .btn-sm{font-size:11px;padding:7px 11px}
#adminScreen .content{padding:26px;max-width:1060px;width:100%}
#adminScreen .tab{display:none}
#adminScreen .tab.on{display:block;animation:fadeUp .22s ease}
@keyframes fadeUp{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
#adminScreen .page-eyebrow{font-family:var(--disp);font-weight:800;font-size:10px;letter-spacing:.28em;color:var(--gold);margin-bottom:8px}
#adminScreen h1{font-family:var(--disp);font-weight:700;font-size:26px;letter-spacing:-.01em}
#adminScreen .page-sub{color:var(--muted);font-size:14px;margin-top:8px;max-width:640px;line-height:1.55}
#adminScreen .backend-note{display:inline-flex;align-items:center;gap:8px;margin-top:14px;font-size:12px;font-weight:700;
    color:#E88; background:rgba(198,54,43,.10);border:1px solid rgba(198,54,43,.4);border-radius:9px;padding:8px 12px}
#adminScreen .card{background:var(--ink2);border:1px solid #234A3B;border-radius:14px;padding:20px;margin-top:20px}
#adminScreen .card h2{font-family:var(--disp);font-size:14px;font-weight:800;letter-spacing:.06em;margin-bottom:4px}
#adminScreen .card .hint{font-size:12px;color:var(--muted);margin-bottom:14px;line-height:1.5}
#adminScreen .grid2{display:grid;grid-template-columns:1fr 1fr;gap:12px}
#adminScreen .grid3{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px}
#adminScreen .field{margin-top:12px}
#adminScreen .field label{display:block;font-size:10.5px;font-weight:800;letter-spacing:.14em;color:#8A9E90;
    text-transform:uppercase;margin-bottom:6px}
#adminScreen /* list rows */
  .row-item{background:var(--ink3);border:1px solid #234A3B;border-radius:12px;padding:14px;margin-top:12px;position:relative}
#adminScreen .row-item .rm{position:absolute;top:10px;right:10px;background:none;color:#7A5A55;font-size:15px;padding:4px}
#adminScreen .row-item .rm:hover{color:var(--red)}
#adminScreen .row-tag{font-family:var(--disp);font-size:9px;font-weight:800;letter-spacing:.2em;color:var(--gold);margin-bottom:8px}
#adminScreen .add-btn{margin-top:14px;background:transparent;border:1.5px dashed var(--line);color:var(--muted);
    width:100%;padding:11px;border-radius:11px;font-size:13px;font-weight:700}
#adminScreen .add-btn:hover{border-color:var(--gold);color:var(--gold)}
#adminScreen /* overview status table */
  .st-row{display:flex;align-items:center;gap:12px;padding:11px 4px;border-bottom:1px dashed #234A3B;font-size:13.5px}
#adminScreen .st-row:last-child{border-bottom:none}
#adminScreen .st-row .nm{font-weight:700;width:210px;flex:0 0 auto}
#adminScreen .st-row .ds{color:var(--muted);flex:1;font-size:12.5px}
#adminScreen .pill{font-family:var(--disp);font-size:9px;font-weight:800;letter-spacing:.14em;border-radius:6px;padding:4px 8px;flex:0 0 auto}
#adminScreen .pill.ok{background:rgba(174,190,55,.14);color:var(--lime);border:1px solid rgba(174,190,55,.4)}
#adminScreen .pill.todo{background:rgba(198,54,43,.12);color:#E88;border:1px solid rgba(198,54,43,.4)}
#adminScreen /* director console */
  .dc-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px;margin-top:16px}
#adminScreen .dc-btn{background:var(--ink3);border:1px solid var(--line);border-radius:14px;padding:18px;text-align:left;color:var(--paper)}
#adminScreen .dc-btn:hover{border-color:var(--gold)}
#adminScreen .dc-btn .t{font-family:var(--disp);font-weight:800;font-size:14px;display:block}
#adminScreen .dc-btn .s{font-size:11.5px;color:var(--muted);display:block;margin-top:5px;line-height:1.4}
#adminScreen .dc-btn.danger{border-color:rgba(198,54,43,.5)}
#adminScreen .dc-btn.danger .t{color:#E88}
#adminScreen .phase-lights{display:flex;gap:10px;margin-top:14px;flex-wrap:wrap}
#adminScreen .phase{font-family:var(--disp);font-size:10px;font-weight:800;letter-spacing:.14em;border-radius:8px;
    padding:8px 12px;border:1px solid var(--line);color:var(--muted)}
#adminScreen .phase.on{background:rgba(249,200,94,.1);border-color:var(--gold);color:var(--gold)}
#adminScreen /* registrants table */
  table{width:100%;border-collapse:collapse;margin-top:12px;font-size:12.5px}
#adminScreen th{font-family:var(--disp);font-size:9px;font-weight:800;letter-spacing:.16em;color:#8A9E90;
    text-align:left;padding:8px 10px;border-bottom:1px solid var(--line)}
#adminScreen td{padding:9px 10px;border-bottom:1px dashed #234A3B;color:var(--muted)}
#adminScreen .empty{padding:34px;text-align:center;color:#41604F;font-size:13px;border:1.5px dashed #234A3B;border-radius:12px;margin-top:14px;line-height:1.7}
#adminScreen /* site-mode selector (Landing Page setup) */
  .mode-cards{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:6px}
#adminScreen .mode-card{text-align:left;background:var(--ink3);border:2px solid #234A3B;border-radius:12px;
    padding:16px;cursor:pointer;transition:border-color .15s,background .15s;color:var(--paper)}
#adminScreen .mode-card:hover{border-color:var(--line)}
#adminScreen .mode-card.sel{border-color:var(--gold);background:rgba(249,200,94,.08)}
#adminScreen .mode-card .mc-title{font-family:var(--disp);font-weight:800;font-size:14px;margin-bottom:5px}
#adminScreen .mode-card .mc-sub{font-size:12px;color:var(--muted);line-height:1.45}
#adminScreen .copy-row{display:flex;gap:8px;align-items:stretch}
#adminScreen .copy-row input{flex:1}
#adminScreen .copy-row .btn{flex:0 0 auto}
#adminScreen .collapse{margin-top:16px;border-top:1px dashed #234A3B;padding-top:12px}
#adminScreen .collapse-head{background:none;color:var(--gold);font-family:var(--disp);font-weight:800;font-size:12px;
    letter-spacing:.04em;cursor:pointer;padding:4px 0}
#adminScreen .collapse-body{margin-top:10px}
@media(max-width:640px){#adminScreen .mode-cards{grid-template-columns:1fr}}
#adminScreen #toast{position:fixed;left:50%;transform:translateX(-50%);bottom:26px;background:var(--lime);color:#16240E;
    font-weight:800;font-size:13px;border-radius:11px;padding:12px 18px;opacity:0;transition:opacity .25s;
    pointer-events:none;z-index:99;max-width:min(460px,86vw);text-align:center;box-shadow:0 10px 28px rgba(0,0,0,.45)}
#adminScreen #toast.warn{background:#F0C0BA;color:#4A1410}
@media(max-width:880px){#adminScreen .shell{flex-direction:column}
#adminScreen .side{width:100%;height:auto;position:static;flex-direction:column}
#adminScreen nav{display:flex;flex-wrap:wrap;padding:6px}
#adminScreen .nav-group{display:none}
#adminScreen .nav-btn{width:auto;border-left:none;border-bottom:3px solid transparent;padding:9px 12px}
#adminScreen .nav-btn.on{border-bottom-color:var(--gold)}
#adminScreen .grid2, #adminScreen .grid3{grid-template-columns:1fr}
#adminScreen .side-foot{display:none}
/* ⛔ 16px IS A FUNCTIONAL THRESHOLD ON iOS, NOT A TASTE PREFERENCE. Safari ZOOMS THE PAGE
   whenever a focused input's font-size is below 16px, and it does not zoom back out — so
   tapping any admin field on an iPhone left the client scrolled sideways in a layout they
   then had to pinch their way out of, on every field, on every tab. The base rule is 14px
   (`#adminScreen input…`, ~line 421), so this affected the WHOLE console, not one screen.
   ⚠️ Scoped to this breakpoint on purpose: desktop keeps 14px, so nothing reflows there.
   ➜ Mo, 2026-08-12: "i've seen u do stuff on desktop make sure everything is optimized for
   mobile." This is the cheapest and widest instance of that. */
#adminScreen input, #adminScreen select, #adminScreen textarea{font-size:16px}
/* 10.5px labels are unreadable on a phone for this audience (CLAUDE.md §12: big text, plain
   words). Letter-spacing comes down with the size or the caps get airy and wrap badly. */
#adminScreen .field label{font-size:12.5px;letter-spacing:.10em}
#adminScreen .hint{font-size:13.5px;line-height:1.5}}

/* Offer banner — pinned directly under the video in the theater/fullscreen overlay.
   position/top/width/centering are set by positionOfferBanner() in game.js. */
#offerBanner{position:absolute;box-sizing:border-box;text-decoration:none;max-width:calc(100% - 24px);background:linear-gradient(90deg,#F9C85E,#F0A93A);color:#241B06;font-weight:800;font-size:15px;line-height:1.35;text-align:center;padding:12px 16px;border-radius:12px;box-shadow:0 6px 20px rgba(0,0,0,.45);z-index:3}
#offerBanner[hidden]{display:none}
#offerBanner.linked{cursor:pointer}
#offerBanner.linked::after{content:" \2192"}
@media (max-width:640px){#offerBanner{font-size:13px;padding:10px 12px}}

/* Rehearsal Mode banner (onboarding-flow-spec §4.2) — persistent, NON-DISMISSIBLE.
   Deliberately NOT themed: it must read identically on every tenant's palette, because its
   whole job is to be unmistakable. Hard-coded amber-on-near-black rather than var(--cta),
   which a client could theme into something that blends in.
   z-index sits above the video overlay and every panel so nothing can cover it. */
#rehearsalBanner{position:fixed;top:0;left:0;right:0;z-index:9999;display:flex;align-items:center;justify-content:center;gap:10px;flex-wrap:wrap;
  box-sizing:border-box;padding:9px 16px;background:#B4520A;color:#FFF6E8;
  font-weight:800;font-size:14px;line-height:1.3;text-align:center;
  border-bottom:2px solid #F9C85E;box-shadow:0 4px 18px rgba(0,0,0,.5);
  /* diagonal hazard stripes — reads as "not a normal UI element" at a glance */
  background-image:repeating-linear-gradient(135deg,rgba(249,200,94,.16) 0 12px,transparent 12px 24px)}
#rehearsalBanner[hidden]{display:none}
#rehearsalBanner .rb-dot{width:9px;height:9px;border-radius:50%;background:#F9C85E;flex:0 0 auto;animation:rbPulse 1.6s ease-in-out infinite}
#rehearsalBanner .rb-t{letter-spacing:.04em}
#rehearsalBanner .rb-s{font-weight:600;font-size:12.5px;opacity:.92}
@keyframes rbPulse{0%,100%{opacity:1}50%{opacity:.35}}
@media (prefers-reduced-motion:reduce){#rehearsalBanner .rb-dot{animation:none}}
@media (max-width:640px){
  #rehearsalBanner{padding:7px 12px;font-size:12.5px;gap:6px}
  #rehearsalBanner .rb-s{font-size:11px;flex-basis:100%}
}
/* Push the world down so the banner never covers the top of the canvas or the mobile rail. */
body.rehearsing #game{margin-top:var(--rehearsal-h,44px)}

/* Roaming stage-audio widget — shown in the world during webinar/pitch */
#stageAudio{position:fixed;left:50%;bottom:var(--ww-mob-b1);transform:translateX(-50%);z-index:40;display:flex;align-items:center;gap:8px;background:rgba(11,15,20,.92);border:1px solid #2A3745;border-radius:999px;padding:6px 10px;box-shadow:0 8px 24px rgba(0,0,0,.4)}
#stageAudio[hidden]{display:none}
#stageAudioTap{appearance:none;cursor:pointer;border:0;background:#F9C85E;color:#241B06;font-weight:800;font-size:13px;border-radius:999px;padding:8px 14px}
#stageAudioCtl{display:flex;align-items:center;gap:8px} #stageAudioCtl[hidden]{display:none}
/* ⛔⛔ THE MUTE BUTTON CARRIES ITS OWN TAP TARGET NOW, BECAUSE DELETING THE SLIDER TOOK AWAY THE
   WIDTH IT WAS BORROWING. `#stageAudioCtl` is a flex row that was built for two children; with the
   slider gone it collapsed around an 18px glyph and the whole pill measured 40×32 on a phone —
   MEASURED 2026-08-23, not assumed. Mo asked for "a small little button", and 18px is not small,
   it is unhittable: it is under the 44px touch minimum and under this project's own audience rule.
   ⭐ Sized here rather than on the pill so the target belongs to the THING BEING TAPPED. A padded
   parent gives the same picture and a dead border — the glyph would still be the only hit area. */
#stageAudioMute{appearance:none;cursor:pointer;border:0;background:transparent;font-size:18px;
  line-height:1;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center}

/* ── Mobile floating joystick (Phase 4a) ── */
#joyWrap{position:fixed;left:50%;top:var(--ww-joy-rest);z-index:12;display:none;transform:translate(-50%,-50%);pointer-events:none;transition:left .28s cubic-bezier(.3,1.1,.5,1),top .28s cubic-bezier(.3,1.1,.5,1),opacity .25s;opacity:.4}
#game.mobile #joyWrap{display:block}
/* ⛔ FROZEN MEANS FROZEN, VISIBLY. #joyWrap rests at opacity .4 and goes to 1 only while a thumb is
   down, so during the pitch freeze it is pixel-identical to an idle WORKING stick — JT grabbed it,
   the knob did not follow, and the world read as broken. Same treatment `.buying` already gives it
   one rule above; the freeze is correct, the silent control was not. */
#game.mobile.frozen #joyWrap{display:none}
#joyWrap.live{transition:opacity .1s;opacity:1}
#joyBase{width:var(--ww-joy-size);height:var(--ww-joy-size);border-radius:50%;background:rgba(8,26,20,.38);border:2px solid rgba(249,200,94,.30);position:relative}
#joyKnob{width:48px;height:48px;border-radius:50%;background:rgba(249,200,94,.85);box-shadow:0 4px 14px rgba(0,0,0,.4);position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);transition:transform .22s cubic-bezier(.3,1.1,.5,1)}
#game.mobile #joyWrap.live #joyKnob{transition:none}

/* ── Mobile "Tap to open" button (Phase 4b) ── */
/* ⛔ z-index WAS 14 AND THE CORRIDOR DOCK IS 24, so the guide video drew ON TOP of the one
   control the attendee had to press. Two independent fixes, deliberately both:
     ① z-index 25 — the ACTION always wins over decoration. This is the safety property: even if
       the layout below is ever wrong again, the button stays tappable rather than unreachable.
     ② `#game.guiding` lifts it clear so they do not merely stack. Position is polish; ① is not.
   ⚠️ ① alone would leave a button sitting on top of a video, which looks broken. ② alone would
   leave the button unreachable the moment any layout assumption shifts. */
/* ⛔⛔ THE BOTTOM-CENTRE STACK, DECLARED ONCE — JT's items 13 and 14, which are one defect.
   Four controls shared `left:50%` at the bottom of a phone and their vertical extents were three
   independent magic numbers, so nothing connected them and nothing could notice when they met.
   MEASURED on the shipped CSS before this change:
     #stageAudio   bottom 74  height ~46  ⇒ spans  74-120   z-40
     #tapOpenBtn   bottom 96  height  48  ⇒ spans  96-144   z-25
   ⇒ a 24px band where the audio pill sat ON TOP of the interaction button and took the tap. That
   is "tap to interact sits directly under tap to hear stage", and it is also why the pitch desk
   "did nothing" on a phone: on mobile walking up only ARMS this button, so a button that cannot be
   hit is indistinguishable from a desk that does not respond.
   ⭐ THE FIX IS THE RELATIONSHIP, NOT THE NUMBER. `--ww-mob-b2` is DERIVED from b1 plus the pill's
   height plus a gap, so moving the pill moves the button and the two cannot silently converge
   again. A second hardcoded number would have fixed today and rotted by the next layout change.
   ⚠️ `--ww-mob-h1` is a measurement of the pill, not a rule for it: 13px text + 16px button padding
   + 12px wrapper padding + 2px border. If the pill's font or padding changes, THIS is the line to
   update, and suite-mobile-stacking asserts the two never overlap so a stale value fails loudly. */
:root{
  /* ══ THE STACKING NAMESPACE ═══════════════════════════════════════════════════════════════
     ⛔⛔ THIS EXISTS BECAUSE A GUARD WENT DEAD AND NOBODY COULD TELL. The corridor card carried a
     literal chosen to beat a toast at 20; #802 moved the toast to 90 to clear video (60) and chat
     (80), and the guard has protected nothing since — while its comment went on asserting "toast
     is z-index 20", so anyone reading it concluded the case was handled.
     ⭐ THE DIGIT WAS THE SYMPTOM. The real defect is that this was a shared namespace with no
     declaration: every lane picked a number by reading whatever neighbours it happened to notice,
     and a correct change by one lane silently broke a correct guard by another. Neither did
     anything wrong.
     ➜ SO THE BANDS ARE NAMED, AND A RULE THAT DEPENDS ON ANOTHER SAYS SO IN `calc()` RATHER THAN
     IN A DIGIT — "one above the toast, whatever the toast is" survives the toast moving; "24"
     cannot. Adding a token here is how you join the namespace.
     ⚠️ NOT EVERY z-index IN THIS FILE IS CONVERTED, and that is deliberate rather than unfinished:
     converting thirty rules in one pass is a large silent-risk change for no benefit. What must be
     a token is anything ANOTHER rule reasons about. The rest are listed here so the next person
     picking a number can see the landscape instead of guessing at it:
        in-world chrome 0-17 · #confirmPanel band 24-25 · misc 30-50 · #videoOverlay 60
        admin pill 70 · #chatPanel / #bookPanel 80 · #toast 90 · announcement banner 95
        #adminScreen #toast 99 · mwatch controls 100-101 · #buyModal 120 · #wwHolding 200
        #rehearsalBanner 9999
     ⚠️ `#chatPanel` is declared FIVE times; a first-match read of it finds the base rule, not the
     one that wins. Read the maximum across every matching rule before reasoning about it. */
  --z-toast:90;        /* the attendee toast — raised from 20 by #802 to clear video and chat */
  /* ⭐ THE CARD ANCHOR AND ITS BREATHING ROOM — one derivation, not four literals. Every bound
     on a `.panel`-anchored card is `viewport − anchor − gap`, and #720 derived that from `.panel`'s
     own `bottom:110px` rather than measuring until the card happened to fit. The figure was then
     hand-copied into each new card, so #vipOfferPanel's "nothing left to drift" had four copies to
     drift between: move the anchor and the bounds stay behind, silently, with no test to notice.
     ⚠️ FALLBACKS ARE DELIBERATE, exactly as `--corr-col` does it. An undefined var makes the whole
     `calc()` invalid, so the declaration DROPS — and a checkout card with no max-height is the
     original overflow bug back again. `var(--panel-anchor,110px)` degrades to today's behaviour.
     ⚠️ MOBILE DOES NOT USE THESE. `#game.mobile .panel` re-anchors to `top:64px` and carries its
     own 150px figure — a different derivation, still a literal in three places (1246/1488/1547). */
  --panel-anchor:110px; /* `.panel`'s bottom offset — what the bounds below are measured FROM */
  --panel-gap:24px;     /* clear air above the card so it can never clip the top of the window */
  --ww-mob-b1:74px;    /* #stageAudio — the persistent one, closest to the thumb */
  --ww-mob-h1:46px;    /* its measured height */
  --ww-mob-gap:12px;   /* clear air between two tappable things */
  --ww-mob-bar:71px;   /* #stageBar's MEASURED height (2026-08-22) — a measurement, like h1, not a rule */
  --ww-mob-rail:62px;  /* the strip #mobRail occupies from the right edge: right:10px + a 52px
                          button — MEASURED on the live build 2026-08-22, same status as h1/bar */
  /* ⛔⛔ DERIVED FROM THE THUMBSTICK, NOT FROM A PILL THAT LEFT. This read
     calc(b1 + h1 + gap) -- the button stacked above #stageAudio. #881 moved #stageAudio into
     #mobRail and left its base rule in place so nothing would shift, so b1 and h1 now measure a
     pill that IS NOT IN THIS STACK ANY MORE. The derivation was still arithmetically fine and had
     stopped describing anything.
     ⭐ THE BUTTON'S REAL NEIGHBOUR IS THE JOYSTICK, and always was. JT, looking at a phone: a
     control that cannot be pressed, drawn on top of the one that can, reads as broken.
     MEASURED on 375x812: the resting stick spans 171-283px from the bottom; the button spanned
     132-180 normally and 210-258 under .guiding -- so in the corridor guide, which is exactly
     first load, it sat DEAD CENTRE on the stick. Both are left:50%.
     ⚠️ The stick MATERIALISES WHERE THE THUMB LANDS (game.js show(ox,oy)); 72% is only its REST
     position. So this clears the resting stick -- the persistent, photographable one -- and makes
     no claim about a stick summoned under a moving thumb.
     ⭐ Derived, not a literal: retune --ww-joy-rest or --ww-joy-size and the button follows. */
  --ww-mob-b2:calc(100vh - var(--ww-joy-rest) + (var(--ww-joy-size) / 2) + var(--ww-mob-gap));
  /* ⛔ THE JOYSTICK'S RESTING PLACE HAD TWO WRITERS THAT DISAGREED, and neither was wrong on its
     own: this stylesheet rested it at 78% and `hide()` in game.js re-rested it at 72% on every
     release. So the FIRST render and every subsequent one put the ghost stick in a different
     place, which is not a thing anyone would report as a bug and is exactly the kind of drift a
     second writer produces.
     ⛔ THE MECHANISM, STATED CORRECTLY: game.js does NOT read this value back. `hide()` CLEARS its
     inline `left`/`top` and hands the position to this rule — strictly better than reading it,
     because there is then no second copy to drift at all. An earlier version of this comment said
     "reads it back off the computed style", which described a mechanism the code does not use;
     a comment that documents the wrong mechanism sends the next person to maintain something that
     is not there. `suite-mobile-stacking` S4 asserts the clearing, not a read.
     ⭐ AND WHICH VALUE WON IS A DECISION, RECORDED SO IT IS NOT MISTAKEN FOR A COIN FLIP (§3.10g).
     The two writers said 78% (this stylesheet) and 72% (game.js). **72% wins**, because it is what
     attendees actually saw: `hide()` ran on every touch release, so 78% only ever appeared on the
     very first render before anyone had touched the screen. Choosing 72% therefore changes what
     nobody was looking at and leaves what everybody saw alone. */
  --ww-joy-size:112px; /* #joyBase's diameter -- ONE writer; --ww-mob-b2 halves it for the radius */
  --ww-joy-rest:72vh;  /* was 72%: IDENTICAL for a position:fixed element, and a vh unit can be
                          subtracted from 100vh in the calc above, which a bare % cannot. */
}
/* ⛔ POINTER-EVENTS NONE, DELIBERATELY. This button sits at thumb height over a canvas that owns
   the touch and draws the joystick wherever the thumb lands, so taking pointer events made it a
   HOLE in the walking surface — JT reported being unable to move at the application desk three
   times. Its tap is hit-tested inside the canvas's own pointerup, which already tells a tap from
   a drag, so a press that MOVES walks and a press that does not opens. */
#tapOpenBtn{pointer-events:none;position:fixed;left:50%;bottom:calc(var(--ww-mob-b2) + env(safe-area-inset-bottom));transform:translateX(-50%);z-index:calc(var(--z-toast) + 2);display:none;min-height:48px;padding:14px 28px;border:0;border-radius:24px;background:#E8632A;color:#fff;font:700 16px "Montserrat",sans-serif;box-shadow:0 6px 20px rgba(0,0,0,.4);touch-action:manipulation}
#game.mobile #tapOpenBtn.on{display:block}
/* Clears a dock of video (≈22.5vw tall at 40vw wide, 16:9) + sound button + label + its 18px
   inset. Centred, so it does not collide with #mobRail on the right at bottom:150px. */
/* max(), not 210px: the guide lift must never drop the button BELOW the stick clearance. b2 now
   exceeds 210 on every phone, so this is inert today and stays because the dock is what 210
   answered and nothing here measures the dock. */
#game.mobile.guiding #tapOpenBtn{bottom:calc(max(var(--ww-mob-b2), 210px) + env(safe-area-inset-bottom,0px))}

/* ── Mobile rail chrome (Phase 6) ── */
/* ══ DESKTOP LEFT RAIL — Zone 1 of JT's nav redesign, 2026-09-06 ═══════════════════════════════
   ⛔ EVERY RULE HERE IS `#game:not(.mobile)`-SCOPED. The phone keeps #mobRail on the right,
   untouched — this spec is desktop-only for now, and a bare `#deskRail` rule would have put a
   second rail on every phone.
   ⛔⛔ AND IT IS PINNED. `position:fixed` with no state-dependent offsets, deliberately: the whole
   complaint this answers is that navigation gets pushed around (four rules move #mobRail by state;
   the desktop strip reflowed whenever the Watch buttons appeared). Nothing below may add a
   `bottom:` override keyed on a state class. Proximity content goes in the CENTRE instead. */
#deskRail,.dr-pop{display:none}
#game:not(.mobile) #deskRail{
  display:flex;flex-direction:column;gap:12px;
  position:fixed;left:18px;bottom:22px;z-index:70;
}
/* ⛔ THE PRESENTATION BAR IS 122px OF FULL-WIDTH CHROME AND THE RAIL SAT INSIDE IT. Measured at
   1440×900 with the bar up: #stageBar occupies y778–900, the rail y618–878, so the bottom two
   buttons stood in the band. The rail is z70 against the bar's z12, so they stayed visible and
   clickable — which is why nothing caught it: every reachability check passed and it simply
   looked wrong. ➜ Same treatment `#chatPanel` already gets one rule along (`bottom:150px`);
   122 of bar + 12 of gap. ⚠️ `.stagebar` is set by the same code that raises chat, so the two
   move together rather than one drifting under the bar on its own. */
/* (the `.stagebar` rail lift is gone for the same reason — see the card rule above) */

#game:not(.mobile) .dr-btn{
  position:relative;width:56px;height:56px;border-radius:16px;
  display:flex;align-items:center;justify-content:center;
  background:color-mix(in srgb, var(--panel-deep,#0B1A15) 92%, transparent);border:1px solid color-mix(in srgb, var(--gold) 28%, transparent);
  box-shadow:0 6px 18px rgba(0,0,0,.45);
  color:var(--paper);font-size:22px;cursor:pointer;padding:0;
  transition:border-color .18s,transform .12s;
}
#game:not(.mobile) .dr-btn:hover{border-color:color-mix(in srgb, var(--gold) 60%, transparent)}
#game:not(.mobile) .dr-btn:active{transform:translateY(1px)}
/* ⚠️ A VISIBLE FOCUS RING, not `outline:none` — the spec asks for keyboard reachability and this
   is the whole of it on a button that is already focusable. */
#game:not(.mobile) .dr-btn:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
#game:not(.mobile) .dr-i{pointer-events:none;line-height:1}
/* Tooltip to the RIGHT of the rail, as specced — the rail is on the left edge, so a tooltip on the
   other side would be off-screen. */
#game:not(.mobile) .dr-tip{
  position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);
  white-space:nowrap;background:color-mix(in srgb, var(--panel-deep,#0B1A15) 96%, transparent);border:1px solid color-mix(in srgb, var(--gold) 28%, transparent);
  color:var(--paper);font:600 12px/1 "Lato",system-ui,sans-serif;padding:7px 10px;border-radius:8px;
  opacity:0;pointer-events:none;transition:opacity .14s;box-shadow:0 6px 18px rgba(0,0,0,.45);
}
#game:not(.mobile) .dr-btn:hover .dr-tip,
#game:not(.mobile) .dr-btn:focus-visible .dr-tip{opacity:1}
/* ── the sound button's two states ──────────────────────────────────────────────────────────
   ⭐ OFF INVITES. Until the attendee turns sound on, the button glows and pushes a ring outward —
   the spec's "actively invites". Both stop the moment it is on, because an invitation that never
   ends is noise.
   ⚠️ `.near` is the PROXIMITY signal and is independent of on/off: it says stage audio is in range,
   which is the job the separate "tap to hear the stage" control used to do. */
#game:not(.mobile) #drSound.off{animation:drGlow 1.6s ease-in-out infinite}
#game:not(.mobile) .dr-ring{
  position:absolute;inset:-2px;border-radius:18px;border:2px solid color-mix(in srgb, var(--gold) 55%, transparent);
  opacity:0;pointer-events:none;
}
#game:not(.mobile) #drSound.off .dr-ring,
#game:not(.mobile) #drSound.near .dr-ring{animation:drRing 1.8s ease-out infinite}
@keyframes drGlow{0%,100%{box-shadow:0 6px 18px rgba(0,0,0,.45)}50%{box-shadow:0 6px 18px rgba(0,0,0,.45),0 0 18px color-mix(in srgb, var(--gold) 55%, transparent)}}
@keyframes drRing{0%{opacity:.8;transform:scale(1)}100%{opacity:0;transform:scale(1.45)}}
/* ── popovers, BESIDE the rail — never over it ─────────────────────────────────────────────
   ⛔ THIS OPENED ON TOP OF THE RAIL AND A SCREENSHOT IS THE ONLY THING THAT SAID SO. It was
   `left:18px; bottom:92px` — the rail's own column, 92px up from the bottom, which is INSIDE the
   rail's 260px span. Measured at 1440×900: the 232×272 menu covered #drRooms, #drSound and
   #drCrowd, leaving one visible button of four. Every geometry row was green: the buttons were
   laid out, sized and hit-testable, and the menu fitted the viewport — they simply occupied the
   same place, and no rect assertion asked whether two things overlap.
   ➜ The rail is 56 wide at left:18, so its right edge is 74; +12 of gap puts the menu at 86, and
   bottom:22px lines its foot up with the rail's. Pinned by the overlap check in desk-rail-shot. */
#game:not(.mobile) .dr-pop.show{
  display:block;position:fixed;left:86px;bottom:22px;z-index:71;
  min-width:232px;max-width:300px;
  background:color-mix(in srgb, var(--panel-deep,#0B1A15) 97%, transparent);border:1px solid color-mix(in srgb, var(--gold) 28%, transparent);border-radius:16px;
  box-shadow:0 14px 34px rgba(0,0,0,.5);padding:8px;
}
#game:not(.mobile) .dr-pop .dr-head{
  padding:8px 10px 10px;border-bottom:1px solid color-mix(in srgb, var(--gold) 16%, transparent);margin-bottom:6px;
  font:800 13px/1.2 "Montserrat",sans-serif;color:var(--gold);
}
#game:not(.mobile) .dr-pop .dr-head small{display:block;font:600 11px/1.4 "Lato",system-ui,sans-serif;color:var(--muted);margin-top:3px}
#game:not(.mobile) .dr-pop button{
  display:flex;align-items:center;gap:9px;width:100%;text-align:left;
  background:none;border:0;border-radius:10px;padding:10px;cursor:pointer;
  color:var(--paper);font:600 13px/1.2 "Lato",system-ui,sans-serif;
}
#game:not(.mobile) .dr-pop button:hover:not([disabled]){background:color-mix(in srgb, var(--gold) 10%, transparent)}
#game:not(.mobile) .dr-pop button:focus-visible{outline:2px solid var(--gold);outline-offset:-2px}
#game:not(.mobile) .dr-pop button[disabled]{opacity:.45;cursor:default}
/* Destructive, and last — sign out is demoted into this menu rather than owning a slot. */
#game:not(.mobile) .dr-pop button.dr-danger{color:var(--danger,#C6362B);border-top:1px solid color-mix(in srgb, var(--gold) 16%, transparent);border-radius:0 0 10px 10px;margin-top:6px}
@media (prefers-reduced-motion:reduce){
  #game:not(.mobile) #drSound.off,
  #game:not(.mobile) #drSound.off .dr-ring,
  #game:not(.mobile) #drSound.near .dr-ring{animation:none}
  #game:not(.mobile) .dr-tip{transition:none}
}
#mobRail{display:none}
#game.mobile #mobRail{display:flex;position:fixed;right:10px;bottom:calc(150px + env(safe-area-inset-bottom,0px));z-index:12;flex-direction:column;gap:12px}
/* ⛔⛔ THE SOUND CONTROL IS A RAIL BUTTON ON MOBILE. JT reported its centre placement three times
   in one QA pass: it sat at `left:50%` directly over the thumbstick, which is the one part of the
   screen a thumb is always on. It is docked into `#mobRail` in the DOM (see `stageAudioDock`), so
   everything below is about making it LOOK like its new neighbours rather than about placing it —
   the flex column owns the position and the spacing.
   ⚠️ `position:static` is the load-bearing line: the base rule is `position:fixed`, which cannot
   participate in a flex column at all. */
#stageAudio.in-rail{position:static;left:auto;right:auto;bottom:auto;transform:none;
  width:52px;height:52px;padding:0;border-radius:16px;border:2px solid rgba(249,200,94,.35);
  background:rgba(8,26,20,.82);box-shadow:0 6px 18px rgba(0,0,0,.4);
  display:flex;align-items:center;justify-content:center;touch-action:manipulation}
/* The words go; the glyph stays. Six words do not fit a 52px button, and 🔊 is the same symbol the
   card already uses, so the meaning survives the shortening. */
#stageAudio.in-rail .sa-lbl{display:none}
#stageAudio.in-rail #stageAudioTap{width:100%;height:100%;padding:0;border-radius:14px;
  background:transparent;color:#F9C85E;font-size:22px;display:flex;align-items:center;justify-content:center}
#stageAudio.in-rail #stageAudioCtl{width:100%;height:100%;justify-content:center;gap:0}
#stageAudio.in-rail #stageAudioMute{width:100%;height:100%;min-width:0;min-height:0;font-size:22px}
/* ⛔⛔ AND THESE MUST YIELD TO `hidden`, WHICH THE RULES ABOVE OTHERWISE DEFEAT. `[hidden]` is a UA
   `display:none`, and any author rule beats it — so `#stageAudio.in-rail #stageAudioTap{display:flex}`
   (two ids and a class) keeps a HIDDEN button laid out. MEASURED before this line existed: in the
   armed state the hidden tap still occupied 22×48, squeezing the mute button to 22px and shoving it
   to the right of the pill, so a tap at the pill's centre hit nothing at all.
   ⭐ The product toggles both children with the `hidden` PROPERTY, so the stylesheet has to honour
   it explicitly once it starts declaring `display` on them — the two mechanisms have to agree, and
   the author rule is the one that wins by default. */
#stageAudio.in-rail #stageAudioTap[hidden],
#stageAudio.in-rail #stageAudioCtl[hidden]{display:none}
.rail-btn{position:relative;width:52px;height:52px;border-radius:16px;border:2px solid rgba(249,200,94,.35);background:rgba(8,26,20,.82);color:#F9C85E;font-size:22px;box-shadow:0 6px 18px rgba(0,0,0,.4);touch-action:manipulation}   /* manipulation: no double-tap-zoom on the rail (felt broken) */
.rb-badge{position:absolute;top:-4px;right:-4px;min-width:18px;height:18px;padding:0 4px;border-radius:9px;background:#C6362B;color:#fff;font:700 11px/18px "Montserrat",sans-serif;display:none}
#mobDemoBtn{display:none}
#game.mobile #mobDemoBtn{display:flex;align-items:center;position:fixed;right:12px;top:calc(10px + env(safe-area-inset-top,0px));z-index:12;height:40px;padding:0 13px;border-radius:12px;background:rgba(198,54,43,.92);border:2px solid #8E241C;color:#fff;font-size:12.5px;font-weight:800;box-shadow:0 8px 22px rgba(0,0,0,.4)}
/* hide the desktop HUD button row on mobile; keep only the LIVE chip, repositioned top-left */
#game.mobile #demoBtn,#game.mobile #chatToggle,#game.mobile #muteBtn,#game.mobile #signOutBtn{display:none}
#game.mobile .hud-bottom{top:calc(14px + env(safe-area-inset-top,0px));bottom:auto;justify-content:flex-start}
#game.mobile #attChip{font-size:11px;padding:6px 10px}

/* ── Mobile fast-travel sheet (Phase 7) ── */
#navSheet{display:none;position:fixed;left:0;right:0;bottom:0;z-index:30;background:#F7F3E3;color:#241E10;border-radius:22px 22px 0 0;padding:16px 16px calc(22px + env(safe-area-inset-bottom));box-shadow:0 -18px 60px rgba(0,0,0,.5);max-height:72vh;overflow-y:auto}
#game.mobile #navSheet.show{display:block}
#navSheet .nav-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
#navSheet .p-eyebrow{font:800 12px "Montserrat",sans-serif;letter-spacing:.12em;color:#8A6D2A}
#navClose{width:34px;height:34px;border-radius:50%;border:0;background:#E7DFC8;color:#241E10;font-size:15px}

/* ── Mobile watch bar (Phase 8) ── */
.sb-sub{display:none}
/* Hidden everywhere by default — only the mobile `.mini` row below turns it on. */
.sb-mininote{display:none}
#game.mobile #stageBar{position:fixed;left:0;right:0;top:auto;bottom:0;z-index:12;display:flex;flex-direction:column;align-items:stretch;gap:12px;padding:22px 14px calc(26px + env(safe-area-inset-bottom,0px));background:linear-gradient(180deg,rgba(3,10,7,0) 0%,rgba(2,8,6,.94) 45%);transform:translateY(105%);transition:transform .34s cubic-bezier(.3,1.15,.5,1)}
#game.mobile #stageBar.show{transform:translateY(0)}
#game.mobile #stageBar .sb-dot{display:none}
#game.mobile #stageBar .sb-label{width:100%;text-align:center;font-size:13px;letter-spacing:.2em;color:var(--gold,#F9C85E)}
#game.mobile #stageBar .sb-sub{display:block;width:100%;text-align:center;font-size:12px;color:#CFE3D6;margin-top:-4px}
#game.mobile #stageBar button{width:100%;font-size:15.5px;font-weight:800;padding:17px 10px;border-radius:15px}
#game.mobile #stageBar.mini{flex-direction:row;align-items:center;gap:8px;padding:10px 12px calc(12px + env(safe-area-inset-bottom,0px))}
#game.mobile #stageBar.mini .sb-label,#game.mobile #stageBar.mini .sb-sub{display:none}
/* ⭐ THE NOTICE THE ROW DROPS OTHERWISE. `flex:0 0 auto` + `nowrap` so it never shrinks and never
   wraps — a wrap here would grow the bar's HEIGHT, which is the one dimension with no room (the
   bar must stay ≤ ~112px or the audio pill lands on the joystick — see syncMobileBarOffset).
   The buttons keep `flex:1`, so the cost is paid in WIDTH, which this row has.
   ⭐ The dot is re-shown HERE ONLY. It stays hidden in the mobile COLUMN, which carries its own
   full-width label and does not need it. */
#game.mobile #stageBar.mini .sb-dot{display:block;flex:0 0 auto;width:8px;height:8px;margin-right:-2px}
#game.mobile #stageBar.mini .sb-mininote{display:block;flex:0 0 auto;white-space:nowrap;font-family:var(--display);font-weight:800;font-size:11px;letter-spacing:.14em;color:var(--gold,#F9C85E)}
#game.mobile #stageBar.mini button{width:auto;flex:1;font-size:12px;padding:10px 8px;border-radius:11px}
#game.mobile.stagebar #mobRail{bottom:calc(250px + env(safe-area-inset-bottom,0px))}
#game.mobile.sbmini.stagebar #mobRail{bottom:calc(130px + env(safe-area-inset-bottom,0px))}
/* Watching: move the rail clear of the video CONTROL BAR in BOTH states (not just missing whichever button
   was last there). The bar lives at the bottom of the video band — screen-bottom in video-full (~0-64px),
   but ~460px+ up in video+chat (bottom of the 34vh band). bottom:80 sits ABOVE the video-full bar and BELOW
   the video+chat bar, so it clears the bar regardless of its contents. 3-class selector (.mwatch always
   co-occurs with .stagebar while watching) to beat the .sbmini.stagebar rule; placed after it to win order. */
#game.mobile.mwatch.stagebar #mobRail{bottom:calc(80px + env(safe-area-inset-bottom,0px))}
/* Prod hotfix: while watching (#game.mwatch) the full-height video overlay (z60) sat over #mobRail (z12) and
   ate its taps — the roster/profile buttons were visible-but-dead. Lift the rail above the overlay (and the
   chat pane z80) so it's reachable in EVERY watching state; still below #buyModal (z120), which must cover it. */
#game.mobile.mwatch #mobRail{z-index:100}
/* ⛔⛔ WHILE WATCHING, THE RAIL KEEPS ONLY ITS CHAT BUTTON — SETUP's finding, verified here on a
   rendered page before applying it: in `mwatch` the rail is z-100 (the #177 hotfix above) and ALL
   FOUR buttons render at 52x52, on screen, over the stream. Map, attendees and profile are
   world-navigation controls offered to somebody who is not in the world.
   ⛔ AND THE RAIL ITSELF IS NOT HIDDEN — this is the whole reason the fix is three selectors and
   not one. Three shipped fixes exist to keep `#mobRail` reachable while watching: #177 (the
   overlay ate its taps → z-index:100, the line directly above), #182 (dismiss reachable in
   video+chat) and #193 (clear of the control bar). And `#railChat` LIVES INSIDE THE RAIL, so
   hiding the rail would remove the one control JT wants kept and undo all three.
   ⚠️ Hidden by DISPLAY, not opacity or visibility: a 52x52 target that is merely invisible still
   takes the tap, which is the failure mode the bottom-column work spent a day on. */
#game.mobile.mwatch #railMap,
#game.mobile.mwatch #railHide,
#game.mobile.mwatch #railProfile{display:none}

/* ── Mobile watching controls (three-state fixes) ──
   FIX 1 (the trap): the dismiss ✕ lived in the bottom control bar, which the chat pane covers in the
   video+chat state → no way back to the world. Pin it to the video's top-LEFT corner, above the chat
   pane (z100), so it's reachable in BOTH watching states. */
#game.mobile.mwatch #vidClose{position:absolute;top:calc(env(safe-area-inset-top,0px) + 10px);left:10px;right:auto;z-index:100;background:rgba(8,26,20,.9);padding:8px 12px}
/* FIX 2 + 3: declutter the bottom bar so it stops fighting #mobRail for the corner and drop the loose
   volume slider. Chat toggles from the rail (#railChat); audio is the "Tap to hear the stage" pill. Keep
   only play + scrubber. This removes the "💬 Chat" button #mobRail's map icon was landing on. */
#game.mobile.mwatch #vidChatBtn,
#game.mobile.mwatch #vidTime,
#game.mobile.mwatch #vidModeBtn{display:none}
/* Fullscreen button (path A): hidden everywhere except mobile video-full with a real playable stream
   (#game.fsready = mwatch && !chatopen && videoOk). Never in video+chat (fullscreen drops chat), never on
   the OFFLINE slate, never on desktop. */
#vidFullBtn{display:none}
/* ⚠️ position+z-index are load-bearing, not tidying. #stageAudio is deliberately raised to
   z-index:100 in mwatch (line 664) so the audio pill sits above #videoOverlay (z60). The pill is
   centre-anchored (left:50%) and its right edge runs into this button, so at 375px it PAINTED OVER
   the button — the label read as clipped — and, worse, took the taps: measured on the live site,
   25px of this button's 103px hit-tested to #stageAudioTap, so roughly a quarter of the control
   silently UNMUTED instead of going fullscreen. Sitting one above the pill fixes both at once,
   since paint order and hit order are the same ordering. Asserted by suite-fullscreen-tap. */
#game.mobile.fsready #vidFullBtn{display:inline-flex;align-items:center;position:relative;z-index:101}

/* ── Mobile popup polish (Phase 6b) ── */
#bigClose{display:none;position:fixed;left:50%;transform:translateX(-50%);z-index:40;background:#C6362B;color:#fff;font-size:13.5px;font-weight:800;padding:11px 22px;border-radius:13px;border:2px solid #8E241C;box-shadow:0 10px 26px rgba(0,0,0,.45);white-space:nowrap}
#game.mobile #bigClose.on{display:block}
#bigClose:active{transform:translateX(-50%) scale(.94)}
/* ⛔ `max-height:46vh` CUT THE CARD OFF. It was sized for a panel holding a heading and a
   button; the corridor's opening card now carries an eyebrow, a headline, the star meter, the
   reward line, a video, a sound button AND a CTA. At 46% of a phone screen the CTA fell below
   the fold, and a scrollable panel with no visible scrollbar reads as "cut off", not "scroll me".
   ⭐ `dvh`, not `vh` — on iOS `100vh` is the LARGE viewport, so a vh-sized panel is partly behind
   the URL bar exactly when the bar is showing. `dvh` tracks the visible area. The `vh` line above
   it is the fallback for browsers without `dvh`, and must stay FIRST. */
/* ⛔ A NOTIFICATION IS NOT A PANEL, AND ON DESKTOP THIS ONE SAT ON THE PLAYER. `.panel` is
   `bottom:110px`, which on a 720px viewport puts #lockedPanel at 71% down — over the character.
   JT: "I can't even see my character. Both should be at the TOP."
   ⭐ SCOPED TO #lockedPanel, NOT TO `.panel`. Every other panel on that class is something the
   attendee OPENED and is reading; this is the only one the WORLD raises at them, and it is
   informational. Moving the whole class would be a visual change to eight panels nobody reported.
   ⚠️ Mobile already does this — `#game.mobile .panel` pins to top:64px, measured at 6% — so this
   makes desktop agree with the platform that was already right, rather than inventing a placement. */
#lockedPanel{bottom:auto;top:64px}

#game.mobile .panel{left:50%;bottom:auto;top:calc(64px + env(safe-area-inset-top,0px));transform:translateX(-50%) translateY(-12px);width:min(380px,94vw);max-height:calc(100vh - 150px - env(safe-area-inset-top,0px));max-height:calc(100dvh - 150px - env(safe-area-inset-top,0px));overflow-y:auto;overscroll-behavior:contain;padding:16px 18px;z-index:14}
#game.mobile #offerPanel.of-wide{width:94vw}   /* the tiers, one under the other on a phone (15 Oct) */
/* ⚠️ AND CAP THE VIDEO, or it takes the space back. At 100% width of a 380px card a 16:9 clip is
   ~214px tall — over half the room on a small phone — which is what pushed the button off. */
#game.mobile #confirmPanel video,#game.mobile #discoveryPanel.corridor-offer video{max-height:30dvh;object-fit:contain;background:#0F140C}
/* The primary action stays reachable even when the card does scroll: a CTA you have to discover
   by scrolling is the same failure as one that is cut off. */
#game.mobile .panel .btn-cta{position:sticky;bottom:0}
#game.mobile .panel.show{transform:translateX(-50%) translateY(0)}
/* A phone panel is the task. Keep the world rail from covering form fields and CTAs while it is open. */
#game.mobile:has(.panel.show) #mobRail,#game.mobile:has(.modal-bg.show) #mobRail{display:none}
/* ---- Chat peek strip (spec §1.2 state 5) — the world-view home of #chatBubbles ----
   Mobile only, and only while the sheet is closed and the attendee is NOT watching (game.js
   gates on vidOpen, so this and the in-overlay bubbles are mutually exclusive).
   ⛔ THE Z-INDEX IS THE CONTRACT, not the position. It sits just above the bottom furniture
   (#stageBar/#mobRail/#joyWrap at 12) so it reads as part of the world, and BELOW both
   #videoOverlay (60) and #chatPanel (80): while watching, the in-overlay bubbles do this job,
   and an open sheet must cover the strip rather than compete with it. Asserting that ordering
   is what suite-chat-video-states checks — the states share one mechanism, so the comparison
   is the thing to test, not each state. */
#chatPeek{display:none}
#game.mobile #chatPeek{display:flex;position:fixed;left:8px;right:8px;bottom:calc(96px + env(safe-area-inset-bottom,0px));z-index:13;flex-direction:column;gap:5px;align-items:flex-start;pointer-events:auto;cursor:pointer}
#game.mobile.stagebar #chatPeek{bottom:calc(196px + env(safe-area-inset-bottom,0px))}   /* clear the raised stage bar, same shift #mobRail takes */
#game.mobile.mwatch #chatPeek,#game.mobile.buying #chatPeek{display:none}   /* watching: the overlay bubbles have it. buying: nothing competes with checkout */
#chatPeek .cbub{pointer-events:none;max-width:90%;background:rgba(6,16,12,.86);border:1px solid rgba(46,90,72,.6);border-radius:12px;padding:5px 10px;font-size:13px;line-height:1.3;color:var(--paper);box-shadow:0 4px 12px rgba(0,0,0,.4);opacity:0;transform:translateY(6px);transition:opacity .2s ease,transform .2s ease}
#chatPeek .cbub.in{opacity:1;transform:none}
#chatPeek .cbub .nm{font-weight:700;color:var(--gold);margin-right:5px}
#game.mobile #chatPanel{left:0;right:0;bottom:0;top:auto;width:auto;height:34vh;border-radius:20px 20px 0 0;border:none;border-top:2px solid #2E5A48;transform:translateY(105%);z-index:80}   /* Fact B fix: was 34 (BELOW #videoOverlay z60) — the sheet opened behind full-screen video. Now above it, so #vidChatBtn works on mobile. */
/* Stage-prompt collision (mobile): the "We are LIVE" toast, "WATCH FULL SCREEN" bar and "Tap to hear the
   stage" pill all stacked at the bottom-centre. The bar is tall (~200px); the toast's existing .raised only
   lifted to 158 (inside it) and the audio pill sat at 74 (behind it). Lift both clear of the bar. While
   actually watching (#game.mwatch) the video overlay owns audio + the "watch" action, so hide the prompts. */
/* ⛔⛔ 214px WAS TUNED FOR A BAR THAT NO LONGER EXISTS AT THAT SIZE. The comment above says the bar
   is "~200px"; MEASURED 2026-08-22 it is 71px, so the pill floated 143px above it — JT: "when I
   exit fullscreen or theater view and am back in the room, the volume button just hovers in the
   middle of the screen." 68% down a 812px viewport.
   ⭐ THE NUMBER WAS NEVER WRONG, IT WENT STALE — which is why nothing caught it: an absolute offset
   cannot notice that the thing it clears has shrunk. Derived from the bar's measured height plus
   the shared gap, so the two move together from here.
   ⚠️ `--ww-mob-bar` is a MEASUREMENT, not a rule — the same status `--ww-mob-h1` already has, and
   it is asserted against the rendered bar in suite-mobile-stacking rather than trusted. */
#game.mobile.stagebar #stageAudio{bottom:calc(var(--ww-mob-bar) + var(--ww-mob-gap) + env(safe-area-inset-bottom,0px))}
#game.mobile.stagebar #toast{bottom:calc(268px + env(safe-area-inset-bottom,0px))}
#game.mobile.mwatch #stageBar{display:none}
/* ⭐ ONE OF EVERYTHING — the desktop half of the rule above, plus the two world controls that also
   duplicate the player's. `.vidwatch` = the player owns the screen (video open, not banded); in the
   BAND these all come back, because that is the walking-and-watching state where the world's
   controls are the ones you want. See syncWatchClass() in game.js for why it is one class. */
#game.vidwatch #stageBar{display:none}
/* ⚠️ THE AUDIO PILL IS DESKTOP-ONLY HERE, AND THAT IS A CORRECTION TO MY OWN FIRST VERSION. On a
   phone the video autoplays MUTED, so "tap to hear the stage" while watching is an affordance
   somebody built on purpose — `#game.mobile.mwatch #stageAudio{display:flex;z-index:100}` below,
   which also happens to win the specificity tie. Hiding it there would remove the only way to get
   sound, to fix a duplicate a phone user never reported. Matthew's count was DESKTOP. */
#game:not(.mobile).vidwatch #stageAudio{display:none}
/* ⛔⛔ AND ON DESKTOP IT ONLY EXISTS DOCKED INTO THE CONTROLS CARD (JT, 2026-09-06). There were
   THREE sound controls on one screen: the rail button, this pill floating at bottom-centre, and
   this same pill again docked into the card near the stage. "Get rid of that volume button that
   floats in the middle bottom of the screen. It's redundant and it's annoying."
   ⭐⭐ THE GUARD THIS WOULD HAVE BROKEN NO LONGER APPLIES, AND THAT IS WHY IT IS SAFE RATHER THAN
   A TRADE-OFF. `stageAudioDock`'s comment warns that nesting the pill permanently "would hide the
   only route to sound whenever somebody is away from the stage" — TRUE WHEN WRITTEN, and the left
   rail's sound button is now exactly that route, everywhere in the world, in every phase. The
   precondition of the hazard is gone, so the guard composes rather than needing an exception.
   ⚠️ MOBILE IS UNTOUCHED: there the pill docks into `#mobRail` instead, and `#stageAudio.in-rail`
   below is that state. This rule is desktop-only and keys on the BAR dock, not on proximity. */
#game:not(.mobile) #stageAudio:not(.in-bar){display:none}
/* ⛔ AND `#liveTag` IS DELIBERATELY NOT HERE. It is written INLINE from JS (`style.display='inline'`,
   game.js:4195), so no stylesheet rule can hide it — one added here would LOOK like a fix and do
   nothing, which is worse than the duplicate. It is also mobile-only in practice: measured, desktop
   never shows it in these states. Left as a known item rather than a rule that cannot bite. */   /* watching: the "WATCH FULL SCREEN" prompt is redundant (video auto-presents) */
/* "Tap to hear the stage" is the UNMUTE path over the muted auto-presented video — it must stay visible and
   reachable, so lift it above the overlay (z60) and pin it to the video band's lower edge. (Device-tune the offset.) */
/* ⛔⛔ `top: --vidh - 60px` ASSUMES THE BAND IS A BAND. With chat CLOSED — the DEFAULT watching
   state, since maybeAutoPresentVideo opens 'screen' — `--vidh` is the FULL viewport (812px
   measured), so "just under the video" resolves to 60px off the bottom of the SCREEN and lands on
   the video's own control bar. JT: "the volume button is directly on top of the video scrubber, so
   the scrubber is unusable." Measured: pill 752-799, #vidPlay 760-800 — the same strip.
   ➜ Keep the band-relative pin ONLY where there is a band. With chat closed the base rule's
   `bottom: var(--ww-mob-b1)` (74px) applies, which clears the control bar (~0-64px) by design —
   consuming the shared stack instead of a second number, which is the drift #796 fixed. */
#game.mobile.mwatch #stageAudio{display:flex;z-index:100}
#game.mobile.mwatch.chatopen #stageAudio{top:calc(var(--vidh,60vh) - 60px);bottom:auto}
#game.mobile #chatPanel.show{transform:translateY(0)}

/* ── Guided tour captions (Phase 10) ── */
#tutCap{display:none;position:fixed;left:50%;top:42%;transform:translate(-50%,-50%) scale(.94);z-index:16;font-weight:800;font-size:17px;letter-spacing:.07em;color:#F9C85E;text-align:center;background:rgba(6,14,10,.92);border:2px solid rgba(249,200,94,.55);border-radius:16px;padding:16px 22px;box-shadow:0 14px 44px rgba(0,0,0,.6);max-width:84vw;line-height:1.4;pointer-events:none}
#game.mobile #tutCap.on{display:block;animation:capIn .45s}
#tutCap.low{top:58%}
@keyframes capIn{from{opacity:0;transform:translate(-50%,-50%) scale(.94)}to{opacity:1;transform:translate(-50%,-50%) scale(1)}}
.rail-btn.pulse{border-color:#F9C85E;box-shadow:0 0 0 3px rgba(249,200,94,.4),0 6px 18px rgba(0,0,0,.4)}
#tourTip{display:none;position:fixed;z-index:17;background:rgba(6,14,10,.94);border:1.5px solid rgba(249,200,94,.6);border-radius:12px;padding:9px 13px;color:#F7F3E3;font-weight:700;font-size:12.5px;max-width:170px;line-height:1.35;transform:translateY(-50%);box-shadow:0 8px 26px rgba(0,0,0,.5);pointer-events:none}
#game.mobile #tourTip.on{display:block}

/* ⛔ THE CORRIDOR'S CARD IS THE PRIMARY INTERACTION, so nothing may render over its button. The
   "Welcome back" toast painted straight over the walkthrough's Continue button and the whole thing
   read as broken.
   ⛔⛔ AND THIS GUARD WAS DEAD FOR TWO WEEKS WHILE READING AS LIVE. It was `z-index:24`, chosen to
   beat a toast at 20; #802 raised the toast to 90 on 2026-08-22 to clear video and chat, and from
   that moment the card was UNDER it again — 8-30px of overlap on desktop. Nothing failed, no test
   moved, and the comment here still said "toast is z-index 20", so the next reader concluded the
   case was handled. A correct fix silently undid an earlier correct fix.
   ⭐ SO IT IS EXPRESSED AS THE RELATIONSHIP, NOT THE NUMBER. `--z-toast + 1` is "one above the
   toast, whatever the toast is" and it moves when the toast moves; 91 would be the same trap with
   a fresher digit, waiting for the next lane that needs to raise the toast for its own good reason.
   ⚠️ It stays BELOW the announcement banner (95), the admin toast (99) and #buyModal (120) — an
   announcement and a checkout both outrank a corridor card, and that ordering is deliberate. */
#confirmPanel{z-index:calc(var(--z-toast) + 1)}
#confirmPanel video,#discoveryPanel.corridor-offer video{display:block;width:100%;border-radius:10px;margin-top:8px;background:#0F140C}

/* ⭐ E — the docked guide: the SAME panel, shrunk into a corner, never a second video system. */
#confirmPanel.guide{width:min(190px,46vw);left:auto;right:10px;bottom:auto;top:auto;padding:0;background:transparent;box-shadow:none;transform:none}
/* ⛔ `#game.mobile .panel` is (1,2,0) and beat the (1,1,0) rule below it, so the docked guide
   rendered at full panel width — ~40% of a phone screen for something meant to sit in a corner. */
/* ⛔⛔ THE CORRIDOR'S NEXT-ACTION CARD SITS AT THE TOP, CENTRED. JT, with a red arrow sweeping from
   the bottom-right corner to the top of the screen: "the notification that says 'Walk up to the
   light' should be at the very top of the screen and centered."
   ⭐ MEASURED BEFORE: 150×184 at x225,y628 — bottom-right, and it clipped the joystick's corner
   (joy x132-244/y529-641 against the panel's x225-375/y628-812). The bottom of a phone screen is
   where the thumb lives; this card is the one thing in the corridor that answers "what now", and it
   was competing for the same 100px as the control the attendee is holding.
   ⭐⭐ AND THIS DOES NOT MOVE THE WALKTHROUGH VIDEO, WHICH IS THE OBVIOUS FEAR AND IS NO LONGER TRUE.
   `.guide` is applied only when `!confirmStep && confirmGuide` (the ambient dock), and the video is
   emitted only under `confirmStep==='video'` — MUTUALLY EXCLUSIVE BY CONSTRUCTION, so a `.guide`
   panel can never contain one. Verified at runtime as well as in the source: with `.guide` on,
   `panel.querySelector('video')` is null. The `#confirmPanel.guide video` rule below is a leftover
   from when the dock DID carry the clip, before Mo had it removed ("the video on the right side
   needs to be gone"). Left in place rather than swept in a placement change.
   ⚠️ THE TOP IS GENUINELY FREE HERE, which is why this is a move and not a trade: the corridor
   stands the world HUD down, and nothing else renders above y400 in it. Measured, not assumed.
   ⚠️ `.intro` is UNAFFECTED — `#game.mobile #confirmPanel.guide.intro` is (2 ids, 3 classes) and
   still beats this (2 ids, 2 classes), so the pre-movement full-screen scrim keeps its inset:0. */
#game.mobile #confirmPanel.guide{width:max-content;max-width:min(88vw,330px);left:50%;right:auto;
  top:calc(14px + env(safe-area-inset-top,0px));bottom:auto;transform:translateX(-50%);
  max-height:none;overflow:visible;padding:0;background:transparent;box-shadow:none}
#game.mobile #confirmPanel.guide .guide-next{font-size:11px;padding:6px 7px}
#confirmPanel.guide video{width:100%;border-radius:12px;border:2px solid rgba(249,200,94,.55);box-shadow:0 10px 30px rgba(0,0,0,.5)}
#confirmPanel.guide .guide-next{margin-top:6px;background:rgba(6,14,10,.92);border:1px solid rgba(249,200,94,.45);border-radius:10px;padding:7px 9px;color:#F9C85E;font-weight:800;font-size:12px;text-align:center;line-height:1.3}

/* ⭐ THE STAR METER. The stars existed as data and were never drawn — `attendee_stars` and
   `ww_my_stars` both shipped correct, and no glyph appeared anywhere. Mo: "there are no stars
   or any sort of boosting motivating factor for anything."
   ⚠️ FILLED AND HOLLOW TOGETHER. Showing only what is earned hides what is still available,
   and the available half is the part that motivates. The count sits alongside because the two
   glyphs are hard to tell apart at a glance on a small screen. */
.star-meter{display:flex;align-items:center;gap:9px;margin:10px 0 2px}
.star-meter .sm-g{font-size:23px;line-height:1;letter-spacing:3px;display:inline-flex;gap:3px}
/* ⛔ THE EARNED/UNEARNED DIFFERENCE MUST BE CARRIED BY COLOUR, NOT BY GLYPH SHAPE. Both stars
   were gold, so "0 of 3" read as three earned at arm's length — the meter said the opposite of
   the truth. Solid-vs-outline is far too fine a distinction at this size on a phone. */
.star-meter .sm-g i{font-style:normal;color:#B9C6BC;opacity:.5}
.star-meter .sm-g i.on{color:#F9C85E;opacity:1;text-shadow:0 2px 0 rgba(0,0,0,.35)}
.star-meter .sm-n{font:800 13px "Montserrat",sans-serif;color:#8A9E90;letter-spacing:.04em}
.star-meter.full .sm-g{filter:drop-shadow(0 0 7px rgba(249,200,94,.75))}
.star-meter.full .sm-n{color:#F9C85E}
/* In the corner dock it must not become the dock: centred, smaller, no number — the panel is
   150px wide and the glyphs alone still read as progress there. */
#confirmPanel.guide .star-meter{justify-content:center;margin:6px 0 0;gap:0}
#confirmPanel.guide .star-meter .sm-g{font-size:17px;letter-spacing:2px}
/* ⛔ UN-HIDDEN IN THE VOID CARD ONLY. The dock is a 190px corner label where "0 of 2" beside the
   glyphs is noise; the void card has room and JT asked for the target to be visible ("how many
   coins do I need to get?"). Scoped by #game, so the dock keeps the rule above it. */
#confirmPanel.guide .star-meter .sm-n{display:none}
/* ════════ THE VOID CARD — desktop only, positioned from vars game.js measures ════════
   ⛔ EVERY VALUE HERE COMES FROM --corr-void-*, WHICH game.js WRITES ONLY WHEN A STABLE VOID
   EXISTS (see corridorVoidRect). No fallbacks are given on purpose: if the vars are absent the
   rule does not apply, `.void` is never added, and the scrim renders instead. A fallback here
   would paint a card at a guessed position in exactly the cases the measurement refused. */
/* ════════ THE UPSELL OFFER CARD'S COPY SLOTS ════════
   ⚠️ `.upsell-body` CARRIES ITS OLD INLINE VALUES VERBATIM — 17px / 1.5 / pre-line — because it
   was converted from an inline `style=` in the same commit and this is a MOVE, not a restyle.
   ⛔ `white-space:pre-line` IS FUNCTIONAL ON ALL THREE, not decoration: the world writes these
   through `esc2`, so a client's line breaks are the only formatting that survives, and losing it
   would collapse their copy into one paragraph with nothing reporting it.
   ⭐ Deliberately minimal otherwise: making the note read as SUBTEXT and the footnote as small
   print is a design pass, and leaving that slate clean is the point of having classes at all. */
.upsell-body{font-size:17px;line-height:1.5;white-space:pre-line}
/* ⭐ THE PRICE LINE IS THE ONE THING ON THIS CARD THAT ARGUES. JT: "CTA assistance — 'regularly
   $300 but just $150 today'". It sits directly above the buy button, so it is the one copy slot
   that gets WEIGHT rather than quiet, and it borrows the HEADLINE's voice (`var(--display)`,
   like `.panel h3`) rather than the body's — a statement, not a third paragraph.
   ⚠️ 18px IS THE FLOOR FROM §12, NOT A PICKED NUMBER — "non-technical, skews older — big text
   (18px+)". Measured 4.55:1 against the cream paper, so the deep gold clears AA at that size;
   it is the same gold the card already spends on value (`.star-won`), not a new colour.
   ⚠️ SPACING IS ASYMMETRIC ON PURPOSE — 12px above, 6px below. Tighter to the button than to the
   copy is what makes it read as a caption FOR the button; even spacing would float it loose
   between the two and it would read as a third paragraph again. */
.upsell-cta-note{margin:12px 0 6px;font:800 18px/1.35 var(--display);color:#8A6A12;
  text-align:center;white-space:pre-line}

/* ⭐ THE FOOTNOTE IS THE QUIET ONE — the last thing on the card, under every button. Guarantees,
   "cancel anytime", the reassurance nobody reads until they need it. Keeps the BODY voice
   (`var(--body)`) because it is an aside to the copy, not a second argument.
   ⛔ 14px IS THE ONLY THING HERE THAT GOES UNDER §12's 18px FLOOR, AND IT IS DELIBERATE: this
   slot exists to be subordinate, and at 18px it competes with the price line two elements above
   it — which would cost the card the one line that sells.
   ⭐ SO IT IS QUIET BY SIZE AND NEVER BY CONTRAST. #6E6449 measures 5.27:1 on the paper — AA at
   any size. Fading it instead (the reflex) is what would actually make it unreadable for the
   audience this file keeps naming, because opacity takes the contrast with it. */
.upsell-footnote{margin:10px 0 0;font:600 14px/1.45 var(--body);color:#6E6449;
  text-align:center;white-space:pre-line}
/* ⛔⛔ THE DESKTOP FALLBACK, WHICH IS WHAT AN ATTENDEE ACTUALLY MEETS ON A SHORT WINDOW. The void
   card below needs the whole corridor box to FIT the viewport — past that the camera tracks the
   player and there is no stable rectangle to sit in, so `.void` is dropped and the card fell back
   to `#confirmPanel.guide`: a 190px transparent tab in the top-right corner, written before the
   corridor cards had a visual language. JT, from a screenshot: "one left over tab at the top right
   corner … that doesn't match our updated version of these cards."
   ⭐ NOT A NEW POSITION — the corner is deliberate (the camera is following the player, so anything
   nearer the middle would sit on them). What changes is that it now looks like the other cards:
   the same panel, border, radius and type scale as `.void`, at a readable width.
   ⚠️ `:not(.intro)` because the pre-first-step scrim is a different thing entirely, and `:not
   (.void)` so this can never fight the card it is the fallback for — two backgrounds on one
   element is this stylesheet's own recorded failure. */
#game:not(.mobile) #confirmPanel.guide:not(.void):not(.intro){
  width:min(320px,30vw);right:18px;top:18px;left:auto;bottom:auto;padding:16px 18px;
  background:color-mix(in srgb, var(--panel-deep,#0B1A15) 94%, transparent);
  border:1px solid color-mix(in srgb, var(--gold) 24%, transparent);
  border-radius:18px;box-shadow:0 14px 40px rgba(0,0,0,.5)}
#game:not(.mobile) #confirmPanel.guide:not(.void):not(.intro) .guide-next{
  background:none;border:0;padding:0;margin:0;font-size:16px;line-height:1.35;color:var(--paper)}
#game:not(.mobile) #confirmPanel.guide:not(.void):not(.intro) .guide-welcome{
  font-size:15px;line-height:1.35;margin-bottom:6px}
#game:not(.mobile) #confirmPanel.guide:not(.void):not(.intro) .star-meter{
  justify-content:flex-start;margin:12px 0 0;gap:10px}
#game:not(.mobile) #confirmPanel.guide:not(.void):not(.intro) .star-meter .sm-g{font-size:22px;letter-spacing:4px}
#game:not(.mobile) #confirmPanel.guide:not(.void):not(.intro) .star-meter .sm-n{display:inline;font-size:13px}
#game:not(.mobile) #confirmPanel.guide.void{
  position:fixed; inset:auto; transform:none;
  left:var(--corr-void-x); top:var(--corr-void-y);
  width:var(--corr-void-w); height:var(--corr-void-h);
  right:auto; bottom:auto; max-width:none;
  display:flex; flex-direction:column; justify-content:center; align-items:flex-start;
  padding:clamp(16px,2.4vw,40px); background:transparent; box-shadow:none; text-align:left;
}
#game:not(.mobile) #confirmPanel.guide.void .guide-welcome{
  font:800 clamp(22px,2.9vw,44px)/1.14 "Montserrat",sans-serif;
  color:var(--gold,#F9C85E); margin:0 0 .45em; text-wrap:balance;
  text-shadow:0 2px 0 rgba(0,0,0,.45);
}
/* ⚠️ text-align IS RESTATED, NOT INHERITED. The dock centres its text and the void card is
   left-aligned, so without this the instruction sat centred under a left-aligned headline —
   two alignments in one card, which reads as a bug rather than a choice. Found by rendering it. */
#game:not(.mobile) #confirmPanel.guide.void .guide-next{
  font:600 clamp(19px,2.1vw,33px)/1.32 "Lato",system-ui,sans-serif;
  color:#EAF2E6; background:none; border:0; padding:0; margin:0;
  max-width:26ch; text-align:left;
}
#game:not(.mobile) #confirmPanel.guide.void .star-meter{justify-content:flex-start;margin:1.15em 0 0;gap:14px}
/* ⚠️ ○ AND ● SIT SMALL INSIDE THEIR EM BOX — noticeably smaller than a ★ at the same font-size,
   which is why this is larger than the dock's number rather than proportional to it. */
#game:not(.mobile) #confirmPanel.guide.void .star-meter .sm-g{font-size:clamp(30px,3vw,46px);letter-spacing:6px}
#game:not(.mobile) #confirmPanel.guide.void .star-meter .sm-n{display:inline;font-size:clamp(14px,1.25vw,20px)}

/* The teaser is the motivating half and must be visible BEFORE anything is earned. */
.bonus-line{margin:8px 0 2px;padding:11px 13px;border-radius:12px;
  background:rgba(8,26,20,.06);border:1px dashed rgba(30,64,52,.45);
  font:700 15px/1.45 "Montserrat",sans-serif;color:#2A4034}
.bonus-line div{font-weight:600;font-size:13.5px;opacity:.8;margin-top:3px}
.bonus-line.won{background:rgba(249,200,94,.16);border:1px solid rgba(249,200,94,.6);color:#5B4A16}
.star-won{font:800 17px "Montserrat",sans-serif;color:#8A6A12;margin:10px 0 0}

/* ⛔ `.btn-ghost` HAD NO BASE RULE — it was styled only under `.modal` and `#adminScreen`, and
   the corridor panel is a `.panel`, so "Back to the room" inherited NOTHING and rendered as a
   raw user-agent button. Mo: "very small. It's like a blue URL link. It's not like an actual
   button." ⭐ The bug was an absent selector, not a wrong value, which is why nothing looked
   wrong in the stylesheet: there was nothing there to look at.
   ⚠️ Sized to the 44px touch minimum and given the same full width as `.panel .btn-cta`, so the
   two read as one stack rather than a button followed by a link. */
.panel .btn-ghost{display:block;width:100%;min-height:48px;margin-top:10px;padding:13px 18px;
  background:rgba(8,26,20,.72);color:#F0E6CE;border:2px solid rgba(249,200,94,.45);border-radius:12px;
  font:700 16px "Montserrat",sans-serif;text-align:center;touch-action:manipulation}
.panel .btn-ghost:active{transform:translateY(2px);background:rgba(8,26,20,.9)}

/* ⛔⛔ THIS DELIBERATELY UNDOES THE RULE ABOVE, FOR ONE BUTTON ONLY — JT, 2026-09-05: the offer
   card's decline "should not be a button, it should look just like blue hyperlink text."
   ⚠️ AND IT IS THE EXACT THING MO ASKED TO HAVE FIXED — "very small. It's like a blue URL link.
   It's not like an actual button." Both are recorded because the next person to read this needs
   to know it is a REVERSAL, not an oversight: Mo met an UNSTYLED user-agent button and read it
   as broken; JT wants a deliberate link, which is a different thing wearing a similar look. If
   this flips back, that is the argument to weigh, not a bug to fix.
   ⭐ SCOPED TO THE DECLINE ALONE. `.btn-ghost` dresses three buttons — this one, "Back to the
   walk" and "Back to the room" — and only the offer card's decline is meant to recede. It is
   opt-in by a class the markup asks for, so the other two cannot be caught by it.
   ⚠️ ORDER IS LOAD-BEARING: `.panel .btn-link` and `.panel .btn-ghost` are both (0,2,0), so this
   wins ONLY by sitting later in the file. That also makes it correct whether the markup REPLACES
   `btn-ghost` or merely ADDS `btn-link` beside it — do not move this above that rule.
   ⚠️ THE TOUCH TARGET SURVIVES THE LOOK. It still reserves 44px of height and full width, so the
   tap area is the button's — §12's audience skews older, and a link that is hard to hit is the
   real cost of this change. Only the PAINT is a link; the target is not.
   ⚠️ #14509E measures 7.07:1 on the paper, and the underline carries the affordance as well as
   the colour — colour alone is not a link cue for a colour-blind reader. */
.panel .btn-link{display:flex;align-items:center;justify-content:center;
  width:100%;min-height:44px;margin-top:8px;padding:10px 12px;
  background:none;border:0;border-radius:0;box-shadow:none;
  color:#14509E;font:400 15px/1.4 var(--body);text-decoration:underline;
  text-underline-offset:2px;text-align:center;cursor:pointer;touch-action:manipulation}
.panel .btn-link:active{transform:none;background:none;color:#0B3C7A}

/* The sound toggle. ⛔ NOT decoration: mobile browsers refuse to autoplay with audio, so this
   button is the ONLY route to sound — Mo met a silent video in both the card and the corner
   dock. It is therefore full-width and labelled in words rather than a speaker glyph alone. */
.btn-sound{display:block;width:100%;min-height:48px;margin-top:8px;padding:12px 16px;
  background:rgba(8,26,20,.72);color:#F9C85E;border:2px solid rgba(249,200,94,.5);border-radius:12px;
  font:800 15px "Montserrat",sans-serif;text-align:center;touch-action:manipulation}
.btn-sound.on{background:rgba(249,200,94,.16);color:#F9C85E}
.btn-sound:active{transform:translateY(2px)}

/* ⭐ THE SOUND TOGGLE IS A SETTING, NOT A DECISION — JT: "style the sound button so it doesn't
   look like the other buttons." On the card it sat full-width at 48px with a gold border, i.e.
   the exact shape of "Yes, I want this" and "No thanks", so the eye counted THREE choices where
   there are two. Buying and declining are the decisions; sound is a control you flick.
   ⭐ SHAPE CARRIES THAT, NOT COLOUR. Auto width and a pill radius read as a different KIND of
   control at a glance; the two real buttons keep the full-width 12px-radius block to themselves.
   ⚠️ 44px MINIMUM SURVIVES. It shrinks in width only — the base rule's own note is that every
   control in the dock became hard to hit once it got small, and §12's audience makes that worse.
   ⚠️ #5C5340 measures 6.83:1 on the paper and the `.on` gold 5.67:1 — quieter, still AA.
   ⛔ SCOPED `:not(.guide)` ON PURPOSE. The corner dock has its own sizing at (1 id, 2 classes)
   and would win only on the properties it restates — anything NEW here would leak straight past
   it, which is the specificity trap this file already records three times. `:not(.guide)` cannot
   reach the dock at all, so the dock is untouched by construction rather than by hoping. */
#confirmPanel:not(.guide) .btn-sound,#discoveryPanel.corridor-offer .btn-sound{
  display:flex;align-items:center;justify-content:center;gap:7px;
  width:fit-content;min-height:44px;margin:10px auto 0;padding:8px 16px;
  background:rgba(36,30,16,.05);color:#5C5340;
  border:1px solid rgba(36,30,16,.22);border-radius:999px;
  font:700 13.5px var(--body);letter-spacing:.01em}
#confirmPanel:not(.guide) .btn-sound.on,#discoveryPanel.corridor-offer .btn-sound.on{
  background:rgba(249,200,94,.20);color:#7A5B10;border-color:rgba(201,151,63,.55)}
/* In the corner dock it has to stay small enough not to become the dock — but never below the
   44px touch minimum, which is what made every other control in there hard to hit. */
#confirmPanel.guide .btn-sound{margin-top:6px;min-height:44px;padding:8px 6px;font-size:12px;line-height:1.2}

/* ⛔ THE FIRST INSTRUCTION IS A SCRIM OVER THE SCENE, NOT A CARD IN THE CORNER. Mo: "it needs to
   be like a pop-up, almost a gradient… as soon as they move their thumbs up, it disappears."
   Same element as the docked guide — no third caption system — wearing `.intro` until the first
   movement, after which it drops back to the quiet side label. */
#confirmPanel.guide.intro{position:fixed;inset:0;width:auto;max-width:none;left:0;right:0;top:0;bottom:0;
  background:linear-gradient(180deg,rgba(4,12,8,.78) 0%,rgba(4,12,8,.55) 38%,rgba(4,12,8,.10) 72%,rgba(4,12,8,0) 100%);
  display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:10px;
  padding:calc(96px + env(safe-area-inset-top,0px)) 22px 0;pointer-events:none;border:0;box-shadow:none}
#confirmPanel.guide.intro .guide-next{font-size:22px;line-height:1.25;padding:0;background:none;border:0;
  color:#F7F3E3;text-shadow:0 3px 18px rgba(0,0,0,.8);max-width:15ch;text-align:center;animation:introRise .5s ease-out}
#confirmPanel.guide.intro .star-meter{opacity:.75}
@keyframes introRise{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
/* ⛔⛔ THE BANNER FOLLOWS THE WORLD COLUMN ON DESKTOP, because the two disagreed about how wide
   the screen is. `.intro` is `inset:0` — the full viewport — while the desktop corridor renders
   as a ~400px column centred in the window, so the scrim and the caption spanned black bands the
   world was not using. `--corr-col` is the column's REAL on-screen width, published by
   `syncCorridorColumnVar()` from `corridorCamBox().w * zoom`, so this cannot drift from the
   geometry the way a hardcoded 400px would.
   ⭐ Why constrain the chrome rather than scale the world: the corridor is PORTRAIT and a desktop
   window is LANDSCAPE — filling the width needs 2.67-4.74x while the offer room's framing caps
   the scale at 1.28x. The measurement and both reasons are in `corridorVisibleSpan()`'s comment.
   ⛔ DESKTOP-ONLY BY SELECTOR, NOT BY LUCK. `#game:not(.mobile)` can never match a phone, so
   mobile is untouched no matter what this rule grows later. Adding these properties to the base
   `#confirmPanel.guide.intro` would have MOVED MOBILE: its override restates position/inset but
   not `transform` or `width`, so anything new here leaks past it — the specificity trap this
   stylesheet already records twice, a third time.
   ⚠️ Falls back to `100%` when the var is absent, so a corridor-less world is byte-identical. */
#game:not(.mobile) #confirmPanel.guide.intro{
  left:50%;right:auto;transform:translateX(-50%);
  width:min(100%,var(--corr-col,100%));max-width:100%}
/* ⛔ THE MOBILE OVERRIDE MUST CARRY THE WHOLE SCRIM. `#game.mobile #confirmPanel.guide` is
   (2 ids, 2 classes) and beats `#confirmPanel.guide.intro` (1 id, 2 classes), so the corner-dock
   geometry won and the gradient never rendered — the same specificity trap that made the docked
   guide fill 40% of the screen. Restate the layout at the higher specificity rather than hoping. */
/* ⛔⛔ `transform:none` IS LOAD-BEARING AND WAS ONLY UNNECESSARY BY ACCIDENT. This rule overrides
   every position longhand it needs and never overrode `transform`, because it never had to — the
   docked mobile rule above used to declare `transform:none`. Centring that rule with
   `translateX(-50%)` handed the scrim a transform it does not reset, and the whole full-screen
   overlay shifted half its own width off the left edge: MEASURED by screenshot, "Drag your thumb to
   walk" clipped to "your / to walk" at x<0.
   ⭐ THE SCRIM WAS CORRECT ONLY BECAUSE NO TRANSFORM EXISTED — the exact shape CLAUDE.md's
   accidental-constraint entry describes, and the reason its trigger now reads "remove, PIN, force,
   default or clamp". Pinning a value on a shared rule takes away an absence something else relied
   on, and the diff shows an addition.
   ⚠️ Caught by the CONTROL screenshot, not by the change's own. The docked state rendered perfectly;
   only the state I was not changing broke. */
#game.mobile #confirmPanel.guide.intro{position:fixed;transform:none;inset:0;width:auto;max-width:none;left:0;right:0;top:0;bottom:0;
  background:linear-gradient(180deg,rgba(4,12,8,.78) 0%,rgba(4,12,8,.55) 38%,rgba(4,12,8,.10) 72%,rgba(4,12,8,0) 100%);
  display:flex;flex-direction:column;align-items:center;justify-content:flex-start;gap:10px;
  padding:calc(112px + env(safe-area-inset-top,0px)) 22px 0;max-height:none;overflow:visible;
  border:0;box-shadow:none;pointer-events:none}

/* ════════ MOBILE — THE GUIDE IS A CARD, NOT TEXT OVER THE TILES ════════
   JT, from a phone: the copy was "small, centered, low-contrast text floating over the play area,
   hard to read against the tile pattern." That was the DOCK — 11px, transparent, top-centred.
   ⛔ BOTH MOBILE STATES GET IT, and that is the whole reason this block exists as one rule. The
   phone shows the scrim before they move and the dock after; restyling one leaves the other as it
   was, and the attendee meets both within seconds of each other.
   ⚠️ `.guide.intro` IS RESTATED IN THE SELECTOR ON PURPOSE. The scrim rule above is
   (2 ids, 3 classes) and would otherwise beat a `.guide` rule at (2 ids, 2 classes) — so every
   property it sets is reset here explicitly, and this block must stay AFTER it in the file.
   ⚠️ `pointer-events:none` IS KEPT FROM THE SCRIM, not dropped. There is no control in this card
   — the guide branch renders a heading, a line and the meter, nothing tappable — and the
   interaction on a phone is a thumb drag. A solid card that swallowed touches in the top strip
   would be a new bug in exchange for a nicer box.
   ⛔ AND NOT A `@media (max-width)` BLOCK. This project decides mobile in JS —
   `matchMedia('(pointer:coarse)') && touch` sets `.mobile` on #game, which 71 rules key on. A
   width query disagrees with that in both directions: a narrow desktop window would take mobile
   CSS while game.js served desktop copy and the desktop void card, and a landscape tablet the
   reverse. The class is the condition the rest of the file already uses.
   ⚠️ z-index is deliberately NOT set. #confirmPanel already sits at `--z-toast + 1` = 91; the
   ladder at the top of this file puts 30 below #videoOverlay, chat and the toast. */
#game.mobile #confirmPanel.guide,
#game.mobile #confirmPanel.guide.intro{
  position:fixed; inset:auto; transform:none;
  top:calc(env(safe-area-inset-top,0px) + 10px); left:12px; right:12px; bottom:auto;
  width:auto; max-width:none; max-height:none; overflow:visible; display:block;
  background:rgba(12,16,10,.90);
  -webkit-backdrop-filter:blur(9px); backdrop-filter:blur(9px);
  border:1px solid rgba(232,194,90,.42); border-radius:18px;
  padding:16px 17px; box-shadow:0 14px 34px rgba(0,0,0,.5);
  text-align:left; pointer-events:none;
}
/* ⛔⛔ ONE ELEMENT, TWO ROLES — this is why there is no `.title` class to hang a rule on.
   `.guide-welcome` is the heading on the intro step and is ABSENT from the DOM afterwards, so on
   every later step `.guide-next` IS the heading. Styling `.guide-next` as body copy throughout
   would leave steps 1 and 2 with nothing but small cream text and no heading at all. */
#game.mobile #confirmPanel.guide .guide-welcome,
#game.mobile #confirmPanel.guide:not(.intro) .guide-next{
  margin:0; font:800 19px/1.2 "Montserrat",sans-serif; color:#f2cf62;
  background:none; border:0; padding:0; text-align:left;
}
/* The body exists only on the intro, under the welcome. It needs no `:empty` collapse: the world
   omits `.guide-welcome` from the markup entirely rather than emitting an empty node. */
#game.mobile #confirmPanel.guide.intro .guide-next{
  margin:6px 0 0; font:400 14.5px/1.42 "Lato",system-ui,sans-serif; color:#ece6d4;
  background:none; border:0; padding:0; text-align:left;
  /* ⛔ `max-width:none` IS THE POINT OF THIS LINE. `#confirmPanel.guide.intro .guide-next` above
     sets `max-width:15ch` — sized for a centred banner floating over the world, where a narrow
     measure is right. Inside a 366px card it wrapped the body to 137px and four ragged lines.
     ⭐ MEASURED, not guessed: `body w=137 max-width=137px` at a true 390px viewport. Higher
     specificity does not undo a property this rule never sets, which is the same reset problem
     the `.intro` selector above already carries. */
  max-width:none;
}
/* ⛔ THE COINS ARE TEXT GLYPHS, NOT BOXES — ● and ○ inside <i>. A width/height/border-radius rule
   would draw a box with the character still sitting inside it. The hollow/solid difference is
   carried by COLOUR here, which is what the meter's own note says the eye actually reads. */
#game.mobile #confirmPanel.guide .star-meter{justify-content:flex-start;align-items:center;gap:11px;margin:14px 0 0}
#game.mobile #confirmPanel.guide .star-meter .sm-g{font-size:15px;letter-spacing:3px;gap:0}
#game.mobile #confirmPanel.guide .star-meter .sm-g i{color:rgba(180,190,160,.55);opacity:1}
#game.mobile #confirmPanel.guide .star-meter .sm-g i.on{color:#e8c25a;opacity:1}
/* Un-hidden here: the dock hides the count because a 190px corner label has no room for it. */
#game.mobile #confirmPanel.guide .star-meter .sm-n{
  display:inline; font:800 12.5px/1 "Montserrat",sans-serif; letter-spacing:.3px; color:#9fb08a;
}

/* ⛔ THE CHECKOUT CARD IS THE ONLY SCROLLER. Without a bound here, `.panel` has no max-height on
   desktop, so the ~985px checkout either overflowed the window or had to scroll inside its own
   frame — a box inside a card, which is the thing #711 removed on mobile. Bounding the card lets
   the frame be taller than its content, so the frame never scrolls and the card always can. */
/* ⛔ THE BOUND MUST MATCH THE ANCHOR. `.panel` is `position:absolute; bottom:110px`, so a
   max-height of `100vh - 72px` puts the card's TOP at vh − 110 − (vh − 72) = −38px: it clipped
   off the top of the window on desktop, measured by SETUP on unmodified main. The 72 was written
   for a differently-anchored card and never re-derived when this rule was scoped to #confirmPanel.
   ⭐ Derived from the anchor instead of guessed: 110px of bottom offset + 24px of breathing room
   at the top, so the card always fits whatever the viewport is. Likely part of why the desktop
   checkout kept reading as awkward. */
#confirmPanel.checkout{max-height:calc(100vh - var(--panel-anchor,110px) - var(--panel-gap,24px));overflow-y:auto;overscroll-behavior:contain}
/* ⛔⛔ THE OFFER CARD FILLS THE RIGHT SIDE ON DESKTOP (JT, 2026-09-06, drawn on a screenshot in
   red: "the first pop-up can take up way more space on the screen … we could fill the entire right
   side"). It was a 390px `.panel` — the same width as a booking card — carrying a video, three
   paragraphs, a button, a decline and a footnote, so everything wrapped narrow and the world
   behind it was empty black anyway.
   ⭐ RIGHT-ANCHORED, NOT WIDENED IN PLACE. `.panel` centres with `left:50%; transform:
   translateX(-50%)`, and simply growing the width there re-imposes the half-screen cap that
   `suite-centred-width-cap` exists to catch: a centred element can never exceed twice its distance
   from the edge. Dropping the centring is what makes the width reachable at all.
   ⚠️ `transform:none` MUST be restated on `.show` too — `.panel.show` re-declares
   `translateX(-50%)`, and transform is one property, so a rule that only overrides the base gets
   the centring back the moment the card is shown. Same trap as the stage controls card.
   ⚠️ SCOPED ON `.offer`, A CLASS ADDED FOR THIS. The first version keyed on `.checkout:not(.embed)`
   and matched NOTHING: measured in a browser, the offer card's className is exactly "panel show" —
   `.checkout`/`.buying` belong to the buying path. #confirmPanel is also the corridor's guide card
   and the booking card, so a marker of its own is the only way to reach this one alone.
   ⚠️ DESKTOP ONLY — the phone has no room to the side and its own stacked layout. */
#game:not(.mobile) #confirmPanel.offer,#game:not(.mobile) #discoveryPanel.offer{
  left:auto;right:24px;top:24px;bottom:24px;
  width:min(680px,48vw);max-height:none;
  transform:none;display:flex;flex-direction:column;
  /* ⚠️ CENTRED IN THE COLUMN, or the card reads as unfinished. Anchoring top AND bottom makes it
     full height, which is what was asked for — but the content is a video and five short blocks,
     so left at `flex-start` it stacks at the top over a third of a screen of empty cream. Centring
     costs nothing when the content is tall and fixes the case where it is not.
     ⚠️ `overflow-y:auto` still comes from `#confirmPanel.checkout` for the buying card; this one
     needs its own, because content taller than the viewport must still scroll rather than clip. */
  justify-content:center;overflow-y:auto}
#game:not(.mobile) #confirmPanel.offer.show,#game:not(.mobile) #discoveryPanel.offer.show{transform:none}
/* The video should not be dwarfed by the room it now has, and the copy under it reads better on a
   measure rather than across the full 680px. */
#game:not(.mobile) #confirmPanel.offer video,#game:not(.mobile) #discoveryPanel.offer video{max-height:46vh;object-fit:cover}
#game.mobile #confirmPanel.checkout{max-height:calc(100dvh - 150px - env(safe-area-inset-top,0px))}

/* ⭐ TWO COLUMNS ON DESKTOP — Mo: "on desktop we have the entire space… I want the video to be
   playing alongside it at the same time." The panel is already the sole scroller (#711/#714), so
   the constraint is WIDTH, not height. ⚠️ Mobile is untouched and stays single-column stacked. */
#confirmPanel.buying{width:min(1080px,94vw)}
#confirmPanel .buy-split{display:grid;grid-template-columns:1fr 1fr;gap:16px;align-items:start}
#confirmPanel .buy-vid video{width:100%;border-radius:10px;display:block;position:sticky;top:44px}
/* ⛔⛔ THE STICKY VIDEO COULD BARELY TRAVEL, SO THE COLUMN WENT BLANK AT THE MOMENT OF PAYMENT.
   `position:sticky` is bounded by its CONTAINING BLOCK, and the grid's `align-items:start` sizes
   `.buy-vid` to its CONTENT — 351px against a 1028px payment column. So the video stuck for 351px
   and then scrolled away, leaving 677px of empty cream beside the card where the money is
   (measured at 1920×1080; the taller the viewport, the more of it you see at once).
   ⭐ THE STICKY DECLARATION WAS ALREADY CORRECT AND HAD NOTHING TO STICK WITHIN — which is why
   this reads as a layout nit and is really the sticky never having worked at all.
   ⚠️ `align-self` on the ITEM, not `align-items:stretch` on the grid: the `start` default is
   deliberate for the row as a whole, and stretching every child is a wider change than the bug
   needs. Mobile is single-column with `position:static` and is untouched by an align rule that
   only matters when there are two columns to align. */
#confirmPanel .buy-vid{align-self:stretch}
/* ⛔ THE OFFER COPY FOLLOWS THE ATTENDEE INTO THE CHECKOUT (JT, 2026-09-06): "I want the copy to
   persist underneath the video … that copy to also follow into the second screen." It sits under
   the video in the same column, which was empty space on desktop.
   ⚠️ MOBILE EMBED HIDES IT, and that is not an oversight: `#game.mobile #confirmPanel.embed
   .buy-vid` is a FIXED ASPECT-RATIO BAND with `overflow:hidden` — the band exists so the video
   stops eating the form's height — so anything added inside it is clipped rather than shown. The
   phone keeps the video band and the scrolling form, which is the layout that was measured. */
/* ⛔⛔ THE CHECKOUT'S WAY OUT — IN THE FLOW, NOT PINNED. JT, twice: "it floats on top of the
   pop-up … as I scroll and try to check out, that button is there", then after it was shrunk to a
   corner button and kept sticky: "Button still floats on top of everything."
   ⛔ THIS REVERSES A GUARD THAT WAS ADDED FOR A REPORTED DEFECT, so the reason is recorded rather
   than the line quietly deleted. `position:sticky` came from: "you can't exit out of the payment
   without awkwardly clicking on the side of the checkout frame and scrolling down." I argued the
   size was the real problem, shipped a compact version that stayed pinned, and JT reported the
   same complaint against it — so the floating itself is what he is rejecting, and that is his
   call to make about his own funnel.
   ⭐ WHAT STILL PROTECTS THE ORIGINAL DEFECT, because it was not only the stickiness: the exit is
   rendered FIRST in the card, ABOVE the payment frame, which is the half that stops anyone having
   to drag past a form that captures the finger to reach it. `suite-corridor-offer` P3 still pins
   that ordering — it is the assertion that matters and it is untouched.
   ⚠️ SO THE RESIDUAL IS REAL AND NAMED: scroll to the total and the way out is off-screen above.
   If that is reported again, the fix is NOT to re-add sticky (it was rejected on sight, twice) —
   it is a second exit at the foot of the card, where somebody who has finished reading already is.
   ⚠️ `align-self:flex-start` AND `width:max-content` are both still needed: the panel is a column,
   so a button with neither stretches to the full width whatever its padding says. */
#confirmPanel .upsell-back{align-self:flex-start;
  width:max-content;margin:0 0 10px;padding:7px 14px;font-size:13.5px;border-radius:10px;
  background:var(--paper);color:#241E10;border:1px solid rgba(160,117,43,.5)}
#confirmPanel .buy-copy{margin-top:12px}
#confirmPanel .buy-copy .upsell-body{margin:0 0 8px}
#confirmPanel .buy-copy .upsell-cta-note{margin:0 0 6px}
#confirmPanel .buy-copy .upsell-footnote{margin:0}
#game.mobile #confirmPanel.embed .buy-copy{display:none}
#game.mobile #confirmPanel.buying{width:min(380px,94vw)}
#game.mobile #confirmPanel .buy-split{grid-template-columns:1fr}
#game.mobile #confirmPanel .buy-vid video{position:static}
/* ⛔⛔ THE CHECKOUT STATE: VIDEO BANDED ABOVE, FORM SCROLLING BELOW, BOTH VISIBLE.
   Mo: "we need to have the video there while they're paying because they might click 'Yes, I want
   this' BEFORE they watch the full video and the pitch."
   ⭐ THE BAND IS THE ASPECT RATIO, NOT A FRACTION OF THE SCREEN. A 16:9 picture at this content
   width is the SMALLEST band that still shows a video rather than a letterboxed strip, so it is the
   one that leaves the most room for the payment form. Measured on 375x812: 34vh gives 387px of
   form, 30vh 419, 26vh 452 — and the aspect ratio gives 193px of video and 470px of form, better
   than all of them AND exact on every screen width instead of approximately right on one.
   ⚠️ SAME SHAPE AS #916's BOOKING PANEL, DIFFERENT MECHANISM, AND THAT IS DELIBERATE. That one
   bands `#videoOverlay` — the watched BROADCAST — through `--vidh`, which only exists under
   `#game.mwatch`. This is `#confirmVid`, the offer video living inside the corridor card. Same
   answer to "video plus a form on a phone"; the elements are different so the plumbing has to be.
   ⚠️ `align-items:stretch` is LOAD-BEARING: `.buy-split` above is a grid with `align-items:start`,
   and that survives into a flex column as a CROSS-AXIS shrink — measured, the form rendered at
   ZERO WIDTH while its height read a healthy 470px. A height-only check saw nothing wrong. */
/* ⛔⛔ ONE SCROLLER, AND IT IS THE CARD — NOT THE FORM (JT, 2026-09-06). It was a flex column with
   the video pinned outside an inner `.buy-pay` scroller: "it's STILL PINNED. when I scroll the
   video doesn't move with the rest of the page … as I scroll down to see the checkout, the video
   should scroll up and away."
   ⭐ THE INNER SCROLLER EXISTED FOR A REAL DEFECT AND THAT DEFECT IS NOW UNREACHABLE. It came from
   "when I click Yes, ALL THE CHECKOUT INFORMATION IS BELOW THE FOLD" — measured on a 375x812 phone
   where the panel was only 662px tall and the video ate 214 of them, leaving a 236px sliver. The
   card is FULL-SCREEN now (top:0;bottom:0), so on an 844px phone the header is 163 and the video
   199 — the form starts at ~370 with ~470px of it already on screen. Nothing is below the fold on
   arrival, and past that the whole card scrolls as one page, which is what a phone does anyway. */
#game.mobile #confirmPanel.embed{top:0;bottom:0;left:0;right:0;width:auto;height:auto;max-height:none;
  transform:none;border-radius:0;display:block;overflow-y:auto;-webkit-overflow-scrolling:touch}
/* ⚠️ PLAIN BLOCK FLOW. The flex column needed `align-items:stretch` because the desktop grid's
   `align-items:start` survived into it as a CROSS-AXIS shrink — the form rendered at ZERO WIDTH
   while its height read a healthy 470px. Block children are full-width by definition, so that
   hazard stops existing here rather than being defended against. */
#game.mobile #confirmPanel.embed .buy-split{display:block}
#game.mobile #confirmPanel.embed .buy-vid{margin-bottom:10px}
/* ⛔⛔ THE BAND SIZES TO ITS CONTENT NOW, AND THAT IS WHAT MAKES THE CONTROLS REACHABLE. It was
   `flex:0 0 <16:9 of the width>` with `overflow:hidden` — a fixed box the video exactly filled, so
   the sound button beneath it was CLIPPED. Measured at 390x844: band 201px, the column inside it
   253px, the button at y380 against a band ending at y364. It had a rect and was never painted.
   ⭐ The band existed so the video would stop eating the FORM's height (#849), and that still
   holds — the video is capped, so the column can only be the video plus one control row.
   ⚠️ 18vh IS A MEASUREMENT, NOT A TASTE. On a 390x844 phone the video's own 16:9 height is 201px
   and the checkout form got 448 of 844 — JT: "I can really only see the checkout form on half the
   screen." Capping the VIDEO (native controls overlay it, so they cost no extra height):
       26vh  band 207  form 448  53%
       20vh  band 177  form 478  57%
       18vh  band 160  form 495  59%   <- here
       16vh  band 143  form 512  61%
   16vh buys two more points for a visibly smaller picture, and the native fullscreen button means
   the video can still be watched properly on demand. */
#game.mobile #confirmPanel.embed .buy-vid{overflow:visible;border-radius:10px}
/* ⛔ NATURAL ASPECT, NO CROP. `object-fit:cover` under a `max-height` cropped the picture to a
   letterbox strip — JT: "the video is squished." The cap existed to stop the video eating the
   form's height; now that the card scrolls as one, scrolling past it does that instead. */

/* ⛔ ONE MUTE WRITER ON THIS SURFACE. The native controls carry mute, so the custom button would be
   a second control writing `v.muted` — the drift this file has paid for more than once. Hidden
   here only; every other card still renders and uses it. */
#game.mobile #confirmPanel.embed #confirmSound{display:none}
#game.mobile #confirmPanel.embed .buy-vid video{width:100%;height:auto;max-height:none;object-fit:contain}
#game.mobile #confirmPanel.embed .buy-pay{overflow:visible}

/* ⛔ AND THE SELECTOR NAMES BOTH CARDS THAT HOLD A CHECKOUT (#719). The VIP door reuses
   `upsellEmbedHtml`, so it inherited the 1000px frame — and NOT the bound above, which is what
   makes 1000px safe. Measured on desktop before the fix: the card rendered 1179px tall at
   top:-412, putting its own exit button off the top of the window.
   ⭐ The renderer and its bound are two halves of one fix, and only one of them travelled with
   the reuse. Keyed on the `.checkout` CLASS rather than a panel id so the next surface that
   mounts a checkout gets it by saying what it is.
   ⛔⛔ AND IT TAKES #720's DERIVED NUMBER RATHER THAN ITS OWN. This branch carried
   `100vh - 150px`, arrived at by measuring until the card fit; #720 derived `110 + 24` from
   `.panel`'s own `bottom:110px` while this PR was open. Both fit, and that is exactly why two
   numbers must not stand: these panels are anchored IDENTICALLY, so a second figure is a second
   thing to re-derive, and the bug being fixed here was a number written for a differently
   anchored card.
   ⭐ ONE RULE, ONE DERIVATION — AND IT IS NOW A TOKEN RATHER THAN AN AGREEMENT. This sentence used
   to end "there is nothing left to drift", which was true of the two panels it was written about and
   stopped being true the moment a third card copied the figure. `--panel-anchor`/`--panel-gap` in
   :root make the anchor and its bounds ONE value, so the agreement is enforced instead of asserted.
   ⚠️ #bookPanel (451, 506) still carries the literal — a different surface, left deliberately. */
#vipOfferPanel.checkout{max-height:calc(100vh - var(--panel-anchor,110px) - var(--panel-gap,24px));overflow-y:auto;overscroll-behavior:contain}
#game.mobile #vipOfferPanel.checkout{max-height:calc(100dvh - 150px - env(safe-area-inset-top,0px))}

/* Discovery rewards: readable forms, one obvious close control, no overlay on fullscreen video. */
#discoveryPanel:not(.corridor-offer) { z-index:35; width:min(520px,calc(100vw - 32px)); max-height:78dvh; overflow:auto; font-size:18px; padding:28px; }
#discoveryPanel label { display:block; margin:18px 0 8px; }
#discoveryPanel textarea { display:block; width:100%; box-sizing:border-box; font:inherit; padding:12px; }
#discoveryPanel:not(.corridor-offer) button:not(.p-close), #discoveryPanel:not(.corridor-offer) a, #confirmPanel #discoveryDownload, #confirmPanel #discoveryCollect, #confirmPanel #discoveryContinue, #confirmPanel #discoveryWear { font:inherit; min-height:44px; padding:12px 16px; margin:8px 0; }
#discoveryPanel:not(.corridor-offer) button:not(.p-close), #confirmPanel #discoveryDownload, #confirmPanel #discoveryCollect, #confirmPanel #discoveryContinue, #confirmPanel #discoveryWear { border:0; border-radius:10px; background:#F9C85E; color:#1A1205; font-weight:800; cursor:pointer; }
#discoveryPanel h2 { padding-right:32px; }
#discoveryStatus { min-height:1.4em; }

/* Guided discovery uses the original movement/camera concept on both device types. */
#game #confirmPanel.discovery-guide{position:fixed;left:50%;right:auto;top:20px;bottom:auto;transform:translateX(-50%);width:min(520px,calc(100vw - 28px));max-width:none;padding:18px 22px;background:rgba(6,18,16,.96);border:2px solid #c9a24a;border-radius:18px;text-align:center;pointer-events:none;box-sizing:border-box}
#confirmPanel.discovery-guide .discovery-eyebrow{color:#f9c85e;font-size:12px;font-weight:800;letter-spacing:.12em}
#confirmPanel.discovery-guide h2{margin:6px 0;font-size:24px;color:#fff0c4}
#confirmPanel.discovery-guide p{margin:7px 0;font-size:17px;line-height:1.4;color:#f7f3e3}
.discovery-steps{display:flex;flex-wrap:wrap;justify-content:center;gap:6px 14px;margin-top:12px;font-size:12px;color:#b0bbae}
.discovery-steps .active{color:#f9c85e}.discovery-steps .done{color:#82ddaa}
#game.discovery-tour #tutCap.on{display:block;top:18%;max-width:min(560px,88vw);font-size:23px;animation:capIn .45s}
#game.discovery-tour #tutCap small{display:block;font-size:17px;line-height:1.5;letter-spacing:0;font-weight:500;margin-top:8px;color:#f7f3e3}
@media(max-width:620px){#game #confirmPanel.discovery-guide{top:12px;padding:12px 14px}#confirmPanel.discovery-guide h2{font-size:20px}#confirmPanel.discovery-guide p{font-size:15px}#game.discovery-tour #tutCap.on{font-size:20px;top:20%}}

/* Discovery rewards: one clear action, readable cards, earned collection states. */
#game .panel.discovery-card{box-sizing:border-box;background:linear-gradient(155deg,#fffaf0,#f4eddb);color:#25231b;border:1px solid #ddc17b;border-radius:24px;box-shadow:0 22px 70px #0009,inset 0 1px #fff;padding:28px 30px;text-align:center;width:min(540px,calc(100vw - 28px));max-height:72dvh;overflow:auto}
#game .discovery-card h2{font-size:28px;line-height:1.15;letter-spacing:-.025em;margin:10px 0 14px;padding:0 16px;white-space:normal}
#game .discovery-card p{font-size:17px;line-height:1.55;margin:10px auto 18px;max-width:42ch;white-space:pre-line}
.discovery-emblem{display:grid;place-items:center;width:66px;height:66px;margin:0 auto 14px;border-radius:50%;font-size:40px;color:#9c711d;background:radial-gradient(circle,#fff7d1,#e8cd87);box-shadow:0 0 30px #edc46255;border:1px solid #d4b669}
.discovery-card .discovery-eyebrow{color:#846117;font-size:11px;font-weight:800;letter-spacing:.15em}
#game .discovery-card #discoveryCollect,#game .discovery-primary{display:block;min-height:48px;margin:20px auto 6px;padding:13px 24px;border:1px solid #ddb851;border-radius:12px;background:linear-gradient(#ffdb78,#f5c454);box-shadow:0 3px 0 #b98b2f;color:#231d10;font:800 17px/1.3 'Lato',sans-serif;cursor:pointer;max-width:100%}
#game .discovery-primary:disabled{opacity:.6;cursor:default}
#game .discovery-card .p-close{right:12px;top:12px;width:40px;height:40px;font-size:25px;color:#655c46}
#game .discovery-card #discoveryStatus:empty{display:none}
#game .discovery-card .discovery-questions{text-align:left;background:#fff9;padding:16px;border:1px solid #d7c9a6;border-radius:16px;margin:20px 0}
#game .discovery-card label{font-size:16px;font-weight:700;line-height:1.5;margin:12px 0 6px}
.discovery-questions label span{color:#92702d;font-size:12px}
#game .discovery-card textarea{display:block;box-sizing:border-box;width:100%;border:1px solid #b5b8ab;border-radius:9px;background:#fffef9;min-height:62px;font:16px/1.45 'Lato',sans-serif;padding:12px}
.discovery-progress{display:flex;justify-content:center;gap:6px;margin:16px 0;flex-wrap:wrap}
.discovery-progress span{max-width:100%;box-sizing:border-box;overflow-wrap:anywhere;padding:7px 9px;border:1px solid #b9bdac;border-radius:20px;font-size:12px;font-weight:700;color:#63715e;background:#e8ece1}
.discovery-progress .earned{background:#d7efdb;border-color:#56a878;color:#1b6844}
.discovery-honours{display:grid;gap:8px;margin:22px 0}
.discovery-honours>div{display:grid;grid-template-columns:38px 1fr;align-items:center;text-align:left;padding:12px 16px;background:linear-gradient(120deg,#133e34,#08261f);color:#ffecaf;border:1px solid #b1954c;border-radius:12px}
.discovery-honours>div>span:last-child{grid-column:2;font-size:9px;letter-spacing:.12em;color:#b3ceb9;margin-top:4px}
.discovery-medal{grid-row:span 2;font-size:28px;color:#f9cd68}
#game .discovery-card[data-layout=compact] .discovery-emblem{display:none}
#game .discovery-card[data-layout=compact]{padding:20px}
#game #confirmPanel.discovery-guide{width:min(450px,calc(100vw - 28px));padding:14px 18px}
#game #confirmPanel.discovery-guide h2{font-size:25px}
#game .discovery-card:focus-within{outline:none}
@media(max-width:620px){#game .panel.discovery-card{padding:20px;max-height:64dvh;bottom:18px;left:50%;transform:translateX(-50%)}#game .discovery-card h2{font-size:23px}#game .discovery-card p{font-size:16px}.discovery-emblem{height:48px;width:48px;font-size:30px;margin-bottom:10px}#game .discovery-card .discovery-questions{padding:12px}#game #confirmPanel.discovery-guide h2{font-size:21px}.discovery-steps{display:none}}
@media(prefers-reduced-motion:reduce){#game.discovery-tour #tutCap.on{animation:none}}

/* Keep discovery actions centered despite the older ID-scoped panel button rules. */
#game #discoveryPanel .discovery-primary,#game #confirmPanel #discoveryCollect{margin:20px auto 8px;display:block}

#game.mobile .panel.discovery-card{top:calc(64px + env(safe-area-inset-top,0px));bottom:auto;height:auto;max-height:calc(100dvh - 190px)}
#game.discovery-following #tutCap.on{display:block;top:18px;font-size:18px;max-width:min(440px,88vw);pointer-events:none}
#game.discovery-following #tutCap small{display:block;font-size:15px;line-height:1.45;margin-top:8px;letter-spacing:0}

/* The caption is anchored below browser chrome, never translated above its own top edge. */
#game:is(.discovery-tour,.discovery-following) #tutCap.on{top:max(18px,env(safe-area-inset-top,0px));transform:translateX(-50%);animation:none;box-sizing:border-box;width:min(480px,88vw);max-width:88vw;max-height:calc(100dvh - 120px);overflow:hidden;line-height:1.4}
#game.mobile:is(.discovery-tour,.discovery-following) #tutCap.on{top:calc(76px + env(safe-area-inset-top,0px));font-size:18px;padding:14px 18px}
#discoveryPanel.corridor-offer{z-index:calc(var(--z-toast) + 1)}

/* Put the next destination before the wall names so no scrolling is needed to continue. */
#game .discovery-next-action{margin:16px 0;padding:18px;border:1px solid #b99543;border-radius:16px;background:linear-gradient(135deg,#163e33,#08271f);text-align:center}
#game .discovery-next-action h3{margin:0;color:#ffe49b;font-size:21px;line-height:1.3;overflow-wrap:anywhere}
#game .discovery-next-action p{color:#e6edde;font-size:16px;line-height:1.45;margin:10px auto 14px}
#game #discoveryPanel .discovery-next-action .discovery-primary{margin:0 auto;font-size:16px}
#game .discovery-card .discovery-wall-invitation{font-size:15px;margin:16px auto 8px}
@media(max-width:620px){#game .discovery-next-action{padding:14px;margin:12px 0}#game .discovery-next-action h3{font-size:18px}#game .discovery-next-action p{font-size:15px}}

/* Gift choices use the same avatar renderer as the world. */
#game .discovery-arrow-keys{display:flex;align-items:flex-end;justify-content:center;gap:5px;margin:12px auto 0}
#game .discovery-arrow-keys>span{display:grid;gap:4px}
#game .discovery-arrow-keys kbd{display:block;background:#17382e;border:1px solid #bfa052;border-bottom-width:3px;border-radius:6px;padding:3px 12px;font:700 20px/1.2 sans-serif;color:#ffe8a5}
#game .discovery-options{display:flex;justify-content:center;gap:10px;margin:16px 0}
#game .discovery-option{flex:1;min-width:0;max-width:160px;border:2px solid #d8caa8;border-radius:16px;padding:4px 5px 12px;background:#eee8d8;color:#302617;cursor:pointer}
#game .discovery-option canvas{display:block;width:100%;height:auto;max-width:140px;margin:auto}
#game .discovery-option strong,#game .discovery-option span{display:block;font:700 15px/1.3 'Lato',sans-serif}
#game .discovery-option span{font-size:12px;margin-top:5px;color:#70664c}
#game .discovery-option[aria-pressed=true]{border-color:#0d7464;background:#e1f0d8;box-shadow:0 0 0 2px #0d746422}
#game .discovery-option[aria-pressed=true] span{color:#0d7464}
#game .discovery-primary:disabled{opacity:.55;cursor:default;box-shadow:none}
#game .discovery-bonus{display:block;margin:16px auto 0;padding:10px;border:0;background:transparent;color:#146758;text-decoration:underline;font:700 15px 'Lato',sans-serif;cursor:pointer}
#discoveryCollectionHud{position:absolute;z-index:70;top:76px;left:18px;max-width:calc(100% - 36px);box-sizing:border-box;padding:10px 16px;border:1px solid #bfa052;border-radius:30px;background:#071d16ed;color:#ffe4a2;font:700 14px/1.35 'Lato',sans-serif;pointer-events:none}
#discoveryCollectionHud[hidden],#game:has(.panel.discovery-card.show) #discoveryCollectionHud,#game.discovery-tour #discoveryCollectionHud{display:none}
#game.mobile #discoveryCollectionHud{top:calc(76px + env(safe-area-inset-top,0px));bottom:auto;left:50%;transform:translateX(-50%);width:max-content;max-width:90%;font-size:13px;text-align:center}
@media(max-width:420px){#game .discovery-options{gap:6px}#game .discovery-option strong{font-size:13px}#game .discovery-option{border-radius:12px;padding:3px 3px 10px}}

/* Keep the welcome instructions above the first chest on laptop-height screens. */
@media(min-width:621px) and (max-height:820px){
  #game #confirmPanel.discovery-guide{padding:10px 18px}
  #game #confirmPanel.discovery-guide h2{font-size:22px}
  #game #confirmPanel.discovery-guide p{display:flex;align-items:center;justify-content:center;gap:12px;margin:6px 0;font-size:16px}
  #game #confirmPanel.discovery-guide .discovery-arrow-keys{margin:0;gap:3px;flex-shrink:0}
  #game #confirmPanel.discovery-guide .discovery-arrow-keys>span{display:flex;gap:3px}
  #game #confirmPanel.discovery-guide .discovery-arrow-keys kbd{padding:2px 7px;font-size:18px}
  #game #confirmPanel.discovery-guide .discovery-steps{margin-top:8px}
}
@media(min-width:621px) and (max-height:560px){
  #game #confirmPanel.discovery-guide{left:16px;width:calc(50vw - 88px);transform:none}
  #game #confirmPanel.discovery-guide p{flex-wrap:wrap}
}

/* Keep the mobile controls beside chat, with a dedicated touch lane. Both the world
   sheet and video/chat split use this so opening chat cannot cover sound or profile. */
#game.mobile.chatshown #chatPanel{right:74px}
#game.mobile.chatshown.stagebar #mobRail,
#game.mobile.chatshown #mobRail{
  top:auto;bottom:calc(var(--ww-mob-b1) + env(safe-area-inset-bottom,0px));z-index:100;
  max-height:calc(100dvh - 88px - env(safe-area-inset-bottom,0px));
  overflow-y:auto;overscroll-behavior:contain;gap:8px;
}
#game.mobile.chatshown #mobRail>*{flex-shrink:0}

/* Discovery QA: game gestures should not invoke the browser's text selection UI.
   Keep writing and copying available in actual input fields. */
#game.mobile { -webkit-user-select:none; user-select:none; -webkit-touch-callout:none; }
#game.mobile :is(input,textarea,[contenteditable="true"]) { -webkit-user-select:text; user-select:text; -webkit-touch-callout:default; }
#game.mobile :is(button,a,canvas) { touch-action:manipulation; }
#game.mobile #gameCanvas { touch-action:none; }
/* Reserve the top for one instruction at a time. Panels take precedence over hints. */
#game:has(.panel.show) #discoveryCollectionHud,
#game.discovery-following #discoveryCollectionHud { display:none; }
#game:has(.panel.show:not(.discovery-guide)) #tutCap.on { display:none; }
#game #confirmPlayRecovery[hidden] { display:none; }
#game #confirmPlayRecovery { display:block; margin:10px auto; min-height:44px; }
#game .discovery-complete { color:#146758; font-weight:800; }

/* ── THE NEW WORLD'S PANELS ON A PHONE (21 Sep 2026) ─────────────────────────────────────────────
   Ahmed, walking the deploy on his phone: "the popups … customise them for mobile, it does not have
   to be the desktop". Same card, same place as every other panel in the world (#game.mobile .panel),
   but: its own thumb-sized ✕ (so no red pill and no orange button on top of it), room for that ✕
   beside the title, the three-gift strip set small enough to read as three steps, and a shop row
   whose price button drops under the words instead of squeezing them into a column. */
#game.mobile .panel.v2sheet{padding-top:20px}
#game.mobile .panel{-webkit-overflow-scrolling:touch;touch-action:pan-y}
#game.mobile .panel.v2sheet .p-eyebrow,#game.mobile .panel.v2sheet h3{padding-right:44px}
#game.mobile .panel.v2sheet{position:fixed}
#game.mobile #bigClose.on{z-index:41}
#game.mobile .panel.v2sheet .p-close{position:absolute;top:8px;right:8px;margin:0;width:44px;height:44px;border-radius:50%;background:rgba(127,127,127,.18);font-size:20px;line-height:44px;padding:0;z-index:2}
#game.mobile #v2GiftPanel.v2sheet .p-close,#game.mobile #v2ShopPanel.v2sheet .p-close,#game.mobile #v2QPanel.v2sheet .p-close{color:#fff;background:rgba(255,255,255,.14)}
#game.mobile .panel.v2sheet .v2-strip{gap:6px!important;margin-bottom:12px!important;clear:both}
#game.mobile .panel.v2sheet .v2-cell{padding:9px 4px!important;border-radius:12px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(1){font-size:22px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(2){font-size:9.5px!important;letter-spacing:.06em!important;white-space:nowrap}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(3){font-size:13px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(4){font-size:12px!important}
#game.mobile .panel.v2sheet .v2-shoprow{flex-wrap:wrap;gap:8px!important}
#game.mobile .panel.v2sheet .v2-shoprow>div:first-child{flex:1 1 100%!important}
#game.mobile .panel.v2sheet .v2-shoprow .btn-cta{width:100%!important;position:static}
#game.mobile .panel.v2sheet textarea{font-size:16px}
/* 21 Sep sync — "they shouldn't have to scroll to find the button". Measured at 360x640 (Android Chrome with its bars): the
   gift's waiting card ran 176px past the fold and the coin machine 125px. A phone card has its ✕, so the "Back to the room"
   button is a second exit that costs 60px; the opened gift's paragraph and the machine's intro are said elsewhere. */
#game.mobile .panel.v2sheet{max-height:calc(100dvh - 90px - env(safe-area-inset-top,0px))}
#game.mobile .panel.v2sheet #v2GiftClose,#game.mobile .panel.v2sheet #v2ShopClose,#game.mobile .panel.v2sheet #v2QClose{display:none}
#game.mobile .panel.v2sheet .v2-g1 h3{font-size:20px!important;margin:2px 0 4px}
#game.mobile .panel.v2sheet .v2-g1 .p-eyebrow{display:none}
#game.mobile .panel.v2sheet .v2-waitbox{margin:8px 0 4px!important;padding:10px 12px!important}
#game.mobile .panel.v2sheet .v2-calhead{font-size:17px!important;line-height:1.3!important}
#game.mobile .panel.v2sheet .v2-waitbox .btn-cta{margin:10px 0 8px!important;padding:13px 12px!important}
#game.mobile .panel.v2sheet .v2-purse{margin:8px 0 10px!important;padding:8px 12px!important;font-size:17px!important}
#game.mobile .panel.v2sheet .v2-item{padding:10px 12px!important}
#game.mobile .panel.v2sheet .v2-item .v2-shoprow>div:first-child>div:first-child{font-size:15.5px!important}
#game.mobile .panel.v2sheet .v2-item .v2-shoprow>div:first-child>div:nth-child(2){font-size:13.5px!important}
#game.mobile .panel.v2sheet .v2-item .btn-cta{padding:10px 12px!important;font-size:15px}
   /* under 16px iOS zooms the page on focus */
/* THE PRE-SHOW VIDEO ON A PHONE. Until the one map, a phone could not open the video in the lobby at all (the bar was
   live-only there), so the desktop control row was never seen at 390px — it ran off the right edge and took "Chat"
   with it. Not in `mwatch` (that layout is the live band): the scrubber gets its own line, the buttons share the next. */
@media (orientation:portrait){
#game.mobile:not(.mwatch) #videoOverlay .vid-controls{flex-wrap:wrap;gap:8px;padding:10px 12px calc(12px + env(safe-area-inset-bottom,0px))}
#game.mobile:not(.mwatch) #videoOverlay .vid-track{flex:1 1 55%;min-width:0}
#game.mobile:not(.mwatch) #videoOverlay .vid-mode,#game.mobile:not(.mwatch) #videoOverlay .vid-close{flex:1 1 28%;min-width:0;min-height:44px;padding:10px 6px;font-size:14px;white-space:nowrap;text-align:center;justify-content:center}
}
/* ── A PHONE HELD SIDEWAYS (21 Sep 2026) ───────────────────────────────────────────────────────────
   Ahmed: "what if the game is played sideways too — full screen, much closer to the desktop". The map is already
   the desktop's; game.js fits the zoom to the SHORT side (baseZoom). This block is the chrome: everything the
   portrait layout stacks up the right edge or pins 150–250px off the bottom has 390px to live in here.
   Scoped to a short landscape viewport, so a tablet and the portrait phone never see it. */
@media (orientation:landscape) and (max-height:520px){
  #game.mobile{--ww-mob-land-bar:62px}   /* the sideways stage bar's MEASURED height (21 Sep 2026, 844x390) — what the rail and the orange button stand on while it is up */
  #game.mobile #mobRail{right:calc(8px + env(safe-area-inset-right,0px));bottom:calc(var(--ww-mob-gap) + env(safe-area-inset-bottom,0px))!important;flex-direction:column;flex-wrap:wrap-reverse;align-content:flex-start;height:138px;gap:6px}
  #game.mobile.stagebar #mobRail{bottom:calc(var(--ww-mob-land-bar) + var(--ww-mob-gap) + env(safe-area-inset-bottom,0px))!important}
  #game.mobile #mobRail .rail-btn{width:42px;height:42px;border-radius:13px;font-size:18px}
  #game.mobile #joyWrap:not(.live){left:calc(14% + env(safe-area-inset-left,0px));top:62vh}
  #game.mobile #tapOpenBtn{bottom:calc(var(--ww-mob-gap) + env(safe-area-inset-bottom,0px))}
  #game.mobile.stagebar #tapOpenBtn{bottom:calc(var(--ww-mob-land-bar) + var(--ww-mob-gap) + env(safe-area-inset-bottom,0px))}
  #game.mobile #stageBar{flex-direction:row;align-items:center;gap:8px;left:50%;right:auto;width:min(560px,68vw);padding:8px 12px calc(8px + env(safe-area-inset-bottom,0px));border-radius:16px 16px 0 0;transform:translate(-50%,105%)}
  #game.mobile #stageBar.show{transform:translate(-50%,0)}
  #game.mobile #stageBar .sb-label,#game.mobile #stageBar .sb-sub{display:none}
  #game.mobile #stageBar .sb-dot{display:block;flex:0 0 auto;width:8px;height:8px}
  #game.mobile #stageBar .sb-mininote{display:block;flex:0 0 auto;white-space:nowrap;font-family:var(--display);font-weight:800;font-size:11px;letter-spacing:.14em;color:var(--gold,#F9C85E)}
  #game.mobile #stageBar button{width:auto;flex:1;font-size:12.5px;padding:10px 8px;border-radius:11px}
  #game.mobile #bigClose.on{top:auto!important;bottom:calc(14px + env(safe-area-inset-bottom,0px));left:auto;right:calc(16px + env(safe-area-inset-right,0px));transform:none}
  #game.mobile #stageAudio.in-rail{width:42px;height:42px;border-radius:13px}
  #game.mobile #stageAudio.in-rail #stageAudioMute{font-size:18px}
  #game.mobile #v2TrailHud{gap:4px!important;max-width:46vw!important}
  #game.mobile #v2TrailNext{font-size:12.5px!important;padding:5px 10px!important;max-width:300px!important}
  /* the two bottom sheets: 34vh / 72vh of a 390px-tall screen is a strip. Chat becomes a side panel on the LEFT (the rail
     and the HUD own the right), fast travel a centred card the height of the screen. */
  #game.mobile #chatPanel{left:0;right:auto;top:0;bottom:0;height:auto;width:min(340px,42vw);border-radius:0 18px 18px 0;border-top:none;border-right:2px solid #2E5A48;transform:translateX(-105%);padding-left:env(safe-area-inset-left,0px)}
  #game.mobile #chatPanel.show{transform:translateX(0)}
  #game.mobile #navSheet{left:50%;right:auto;transform:translateX(-50%);width:min(560px,72vw);max-height:calc(100dvh - 6px);padding:10px 14px calc(10px + env(safe-area-inset-bottom,0px));border-radius:18px 18px 0 0}
  #game.mobile #navSheet .nav-head{margin-bottom:6px}
  #game.mobile #navGrid>div{height:min(352px,calc(100dvh - 84px))!important}
  /* ── THE SIDEWAYS SHEET (22 Sep 2026). Ahmed: "when I open any box, make it like full screen — not full screen, almost —
     the popup takes the whole screen; you have the room then, do a good design". So held sideways a card is a SHEET: a dim
     backdrop, the card fills the screen with a thin margin, and the content lays itself in two columns — what it SAYS on the
     left (eyebrow, title, text), what you DO on the right (lists, fields, buttons). It scrolls if it must; the buttons never hide. */
  #game.mobile .panel.show{position:fixed;left:50%;top:50%;bottom:auto;transform:translate(-50%,-50%);width:min(640px,calc(100vw - 24px));max-width:none;max-height:calc(100dvh - 20px);padding:12px 14px;border-radius:14px;box-shadow:0 0 0 100vmax rgba(3,8,14,.55),0 18px 50px rgba(0,0,0,.5);overflow-y:auto;-webkit-overflow-scrolling:touch;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);column-gap:16px;row-gap:6px;align-content:start;align-items:start}
  #game.mobile .panel.show>*{grid-column:2;margin-top:0}
  #game.mobile .panel.show>.p-eyebrow,#game.mobile .panel.show>.discovery-eyebrow,#game.mobile .panel.show>h3,#game.mobile .panel.show>h2,#game.mobile .panel.show>p,#game.mobile .panel.show>.v2-g1,#game.mobile .panel.show>.v2-strip,#game.mobile .panel.show>.v2-purse,#game.mobile .panel.show>.v2-intro{grid-column:1}
  #game.mobile .panel.show>.v2-g1 h3{font-size:22px!important}
  #game.mobile .panel.show>.v2-g1 .p-eyebrow{margin-top:0}
  #game.mobile .panel.show>.v2-waitbox{margin:0!important}
  #game.mobile .panel.show .v2-cell>div:nth-child(2){white-space:normal!important}
  #game.mobile .panel.show>.p-eyebrow{margin-top:2px}
  #game.mobile .panel.show>h3,#game.mobile .panel.show>h2{font-size:17px!important;line-height:1.15;margin:0 0 4px}
  #game.mobile .panel.show>p{font-size:13px!important;line-height:1.4!important;margin:0 0 6px}
  #game.mobile .panel.show>.v2-strip{margin:0 0 10px!important}
  #game.mobile .panel.show>.v2-strip{grid-column:1 / 3;gap:10px!important}
  #game.mobile .panel.show>.v2-strip .v2-cell{padding:6px 8px!important;display:flex;align-items:center;gap:8px;text-align:left!important}
  #game.mobile .panel.show>.v2-strip .v2-cell>div:nth-child(1){font-size:20px!important}
  #game.mobile .panel.show>.v2-strip .v2-cell>div:nth-child(2){margin-top:0!important}
  #game.mobile .panel.show>.v2-strip .v2-cell>div:nth-child(3),#game.mobile .panel.show>.v2-strip .v2-cell>div:nth-child(4){display:none}
  #game.mobile .panel.show .p-close{position:absolute;top:6px;right:6px;margin:0;width:32px;height:32px;line-height:32px;font-size:15px;float:none;z-index:3}
  #game.mobile .panel.show .btn-cta{position:static;width:100%;margin:4px 0 0;white-space:normal;line-height:1.2}
  #game.mobile .panel.show .btn-ghost{margin-top:6px}
  #game.mobile .panel.show>h3:first-child,#game.mobile .panel.show>.p-eyebrow+h3{padding-right:0}
  #game.mobile .panel.show textarea{min-height:70px}
  /* cards whose whole body is one block (profile, closet, sorting, quiz, calendar, booking): one column, centred, still a sheet */
  #game.mobile #profileCard.show,#game.mobile #closetPanel.show,#game.mobile #sortPanel.show,#game.mobile #quizPanel.show,#game.mobile #calendarPanel.show,#game.mobile #bookPanel.show,#game.mobile #confirmPanel.show,#game.mobile #discoveryPanel.show,#game.mobile #bonusPanel.show,#game.mobile #resourcePanel.show,#game.mobile #lockedPanel.show,#game.mobile #mysteryPanel.show,#game.mobile #goldenPanel.show,#game.mobile #applyPanel.show,#game.mobile #upsellPanel.show,#game.mobile #formPanel.show,#game.mobile #vipOfferPanel.show{display:block;width:min(520px,calc(100vw - 24px))}
  #game.mobile .panel.v2sheet.show{max-height:none!important}
}
/* THE SIGN-UP PAGE ON A PHONE — "Claim my seat" IS THE PAGE (21 Sep sync, Tyler on his phone: "I want this call to action
   to be more visible, not cut off at the bottom — shift all this stuff up"). Measured at 360x640: the button's top edge sat
   at y633 of a 640px screen. The column was vertically CENTRED with 48px of padding, under a full-size logo, a row of lights
   and generous gaps — all worth less than the one button the page exists for. On a phone the column starts at the top and
   everything above the email is tightened; nothing is removed. */
@media (max-width:520px){
  #landing .land-wrap{justify-content:flex-start;padding:22px 20px 28px}
  #landing .logo-wrap svg,#landing .logo-wrap img,#landing #lwMark{width:48px;height:48px}
  #landing .marquee-lights{margin:6px 0 12px;gap:8px}
  #landing .eyebrow{margin-bottom:8px}
  #landing .land-sub{margin-top:10px;font-size:15.5px;line-height:1.45}
  #landing .optin{margin-top:16px;flex-direction:column;gap:10px}
  #landing .optin .btn-cta{width:100%}
}
@media (max-width:520px) and (max-height:700px){
  #landing .land-sub{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
}
/* ── THE PHONE MENU (22 Sep 2026) ── the rail is a folded menu under one ☰ button; the top-right corner belongs to the purse. */
#railMenu{display:none}
#game.mobile #railMenu{display:flex;align-items:center;justify-content:center;position:fixed;right:10px;top:calc(58px + env(safe-area-inset-top,0px));z-index:13;width:46px;height:46px;border-radius:14px;font-size:20px;box-shadow:0 6px 18px rgba(0,0,0,.45)}
#game.mobile.railopen #railMenu{background:#F9C85E;color:#1A1205;border-color:#fff}
#game.mobile #mobRail{top:calc(112px + env(safe-area-inset-top,0px));bottom:auto!important;transform:translateX(120%);opacity:0;pointer-events:none;transition:transform .22s cubic-bezier(.3,1.1,.5,1),opacity .18s;gap:8px}
#game.mobile.railopen #mobRail{transform:none;opacity:1;pointer-events:auto}
#game.mobile #mobRail .rail-btn{width:46px;height:46px;border-radius:14px;font-size:20px}
#game.mobile #stageAudio.in-rail{width:46px;height:46px;border-radius:14px}
#game.mobile.railopen #v2TrailNext,#game.mobile.railopen #tapOpenBtn{display:none!important}
#game.mobile #railMenu.in-hud{position:static;pointer-events:auto;width:40px;height:40px;border-radius:50%;font-size:18px;box-shadow:0 4px 12px rgba(0,0,0,.4);order:3}
#game.mobile #v2TrailBell{order:2}#game.mobile #v2TrailPurse{order:1}#game.mobile #v2TrailNext{order:4}
#game.mobile:has(.panel.show) #railMenu,#game.mobile:has(.modal-bg.show) #railMenu,#game.mobile.v2-tour #railMenu,#game.mobile.mwatch #railMenu{display:none}
@media (orientation:landscape) and (max-height:520px){
  #game.mobile #railMenu{top:calc(10px + env(safe-area-inset-top,0px));right:calc(8px + env(safe-area-inset-right,0px))}
  #game.mobile #mobRail{top:calc(62px + env(safe-area-inset-top,0px));height:auto;flex-wrap:nowrap}
  #game.mobile.stagebar #mobRail{bottom:auto!important}
}
/* phone cards, second pass (22 Sep — "the bubble is too big, I have to scroll in it, its buttons are huge"): a phone card
   sits under the LIVE chip and never past the fold; its type is one step smaller; the "ticket back" calendar card too. */
#game.mobile .panel{top:calc(54px + env(safe-area-inset-top,0px));max-height:calc(100dvh - 66px - env(safe-area-inset-top,0px));width:min(380px,95vw)}
#game.mobile .panel h3{font-size:19px!important;margin:2px 0 6px}
#game.mobile .panel p{font-size:15px!important;line-height:1.4!important;margin:4px 0 8px}
#game.mobile .panel .btn-cta{padding:12px 14px;font-size:15.5px;min-height:44px}
#game.mobile .panel .btn-ghost{min-height:42px;padding:10px 14px;font-size:15px;margin-top:8px}
#game.mobile .panel.v2sheet .v2-waitbox .p-eyebrow{font-size:10.5px}
#game.mobile .panel.v2sheet .v2-calhead{font-size:16px!important}
#game.mobile #calendarPanel h3{font-size:19px!important}
#game.mobile #calendarPanel p{font-size:14.5px!important}
#game.mobile #calendarPanel .cal-row,#game.mobile #calendarPanel [class*="cal"]{font-size:14px}
#game.mobile #v2TrailNext{font-size:13px!important;padding:6px 10px!important;max-width:calc(100vw - 96px)!important}
#game.mobile #v2TrailBell{width:40px!important;height:40px!important;font-size:17px!important}
/* ══ THE PHONE SCALE (22 Sep 2026) ══════════════════════════════════════════════════════════════════════════════════
   Ahmed: "why is everything so huge? use the space like any mobile game — smaller buttons, smaller everything; a
   different experience for the phone". The world's cards were built at desktop sizes, most of them INLINE (17–24px type,
   48px buttons), and "bigger for touch" made a phone card a wall of text with one button per screen. A mobile game's UI
   is small and dense: 12–14px type, 34–38px buttons, 6–8px gaps. This block is that scale, and it beats inline styles on
   purpose (!important) so every card — old world, new world, calendar, shop, profile — comes out the same size. */
#game.mobile .panel{padding:12px 14px;border-radius:14px;font-size:13px}
#game.mobile .panel .p-eyebrow,#game.mobile .panel .discovery-eyebrow{font-size:9px!important;letter-spacing:.14em;margin:0 0 3px}
#game.mobile .panel h2,#game.mobile .panel h3{font-size:17px!important;line-height:1.15!important;margin:0 0 4px!important}
#game.mobile .panel h3[style],#game.mobile .panel h2[style]{font-size:17px!important}
#game.mobile .panel p,#game.mobile .panel p[style],#game.mobile .panel li,#game.mobile .panel label{font-size:13px!important;line-height:1.4!important;margin:0 0 6px}
#game.mobile .panel small,#game.mobile .panel .hint{font-size:11.5px!important}
#game.mobile .panel div[style*="font-size:1"],#game.mobile .panel div[style*="font-size:2"],#game.mobile .panel span[style*="font-size:1"]{font-size:12.5px!important}
#game.mobile .panel div[style*="font:800 1"],#game.mobile .panel div[style*="font:900 1"],#game.mobile .panel div[style*="font:800 2"]{font-size:13.5px!important}
#game.mobile .panel .btn-cta,#game.mobile .panel .btn-cta[style],#game.mobile .panel button.btn-cta{font-size:13.5px!important;padding:9px 12px!important;min-height:36px!important;border-radius:9px!important;box-shadow:0 3px 0 var(--cta-dark)!important;margin-top:6px}
#game.mobile .panel .btn-ghost,#game.mobile .panel a.btn-ghost{font-size:13px!important;padding:8px 12px!important;min-height:34px!important;margin-top:6px!important;border-radius:9px}
#game.mobile .panel .btn-link{font-size:12.5px!important;min-height:0!important;padding:6px 0!important}
#game.mobile .panel input,#game.mobile .panel select{font-size:14px!important;padding:8px 10px!important;min-height:36px}
#game.mobile .panel textarea{font-size:14px!important;padding:8px 10px!important;min-height:64px!important;margin:4px 0 2px!important}
#game.mobile .panel .p-close{width:32px!important;height:32px!important;line-height:32px!important;font-size:15px!important;top:6px!important;right:6px!important;margin:0!important}
#game.mobile .panel.v2sheet{padding-top:12px}
#game.mobile .panel.v2sheet .p-eyebrow,#game.mobile .panel.v2sheet h3{padding-right:34px}
#game.mobile .panel.v2sheet .v2-strip{gap:5px!important;margin:0 0 8px!important}
#game.mobile .panel.v2sheet .v2-cell{padding:6px 3px!important;border-radius:9px!important;border-width:1.5px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(1){font-size:17px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(2){font-size:8px!important;letter-spacing:.05em!important;margin-top:3px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(3){font-size:11px!important;margin-top:2px!important}
#game.mobile .panel.v2sheet .v2-cell>div:nth-child(4){font-size:10px!important;margin-top:1px!important}
#game.mobile .panel.v2sheet .v2-waitbox{margin:6px 0 4px!important;padding:8px 10px!important;border-radius:10px!important}
#game.mobile .panel.v2sheet .v2-calhead{font-size:13.5px!important;line-height:1.25!important}
#game.mobile .panel.v2sheet .v2-waitbox .btn-cta{margin:6px 0 4px!important}
#game.mobile .panel.v2sheet .v2-purse{margin:6px 0 8px!important;padding:6px 10px!important;font-size:14px!important;border-radius:9px!important}
#game.mobile .panel.v2sheet .v2-item{padding:8px 10px!important;border-radius:10px!important;border-width:1.5px!important}
#game.mobile .panel.v2sheet .v2-shoprow{gap:6px!important}
#game.mobile .panel.v2sheet .v2-shoprow .btn-cta{padding:7px 10px!important;min-height:32px!important;margin-top:4px}
#game.mobile .panel.v2sheet #v2GiftClose,#game.mobile .panel.v2sheet #v2ShopClose,#game.mobile .panel.v2sheet #v2QClose{display:none}
#game.mobile .panel .v2-list{gap:5px!important;margin:8px 0!important}
#game.mobile .panel .v2-list-row{padding:6px 10px!important;border-radius:9px!important;font-size:13px!important;gap:8px!important}
#game.mobile .panel .v2-list-ic{font-size:17px!important}
#game.mobile #calendarPanel .cal-ev,#game.mobile #calendarPanel [style*="padding:1"]{padding:8px 10px!important}
#game.mobile #calendarPanel button:not(.p-close){font-size:13px!important;padding:8px 12px!important;min-height:36px!important}
#game.mobile #bigClose{font-size:12px;padding:8px 14px;border-radius:10px}
#game.mobile #tapOpenBtn{min-height:38px;padding:9px 18px;font-size:13.5px;border-radius:19px}
#game.mobile .panel:not(.show){display:none}
@media (orientation:portrait){
  #game.mobile .panel{top:calc(54px + env(safe-area-inset-top,0px));width:min(340px,92vw);max-height:calc(100dvh - 70px - env(safe-area-inset-top,0px))}
}
/* ── SIDEWAYS, THE MEDIA SITS LEFT AND THE WORDS RIGHT (22 Sep) — the story card and the mystery box read as one row. ── */
@media (orientation:landscape) and (max-height:520px){
  #game.mobile #hofCard{width:min(760px,calc(100vw - 24px));max-height:calc(100dvh - 20px);flex-direction:row;align-items:stretch}
  #game.mobile #hofCard .hof-hero{flex:0 0 46%;min-height:0;height:auto;align-self:stretch}
  #game.mobile #hofCard .hof-hero video,#game.mobile #hofCard .hof-hero img{height:100%;object-fit:cover}
  #game.mobile #hofCard .hof-body{flex:1 1 auto;padding:18px 40px 14px 18px;font-size:13px}
  #game.mobile #hofCard .hof-name{font-size:17px}#game.mobile #hofCard .hof-quote{font-size:14px}#game.mobile #hofCard .hof-story{font-size:12.5px}
  #game.mobile #mysteryPanel.show{display:grid;grid-template-columns:minmax(0,.9fr) minmax(0,1.1fr);width:min(560px,calc(100vw - 24px))}
  #game.mobile #mysteryPanel.show>.p-eyebrow{grid-column:1 / 3}
  #game.mobile #mysteryPanel.show>h3,#game.mobile #mysteryPanel.show>p{grid-column:1}
  #game.mobile #mysteryPanel.show>#mysteryCd,#game.mobile #mysteryPanel.show>#mysteryBtn{grid-column:2;align-self:center}
  /* the sound / crowd / full-screen buttons were the ones cut off the folded menu sideways: the menu is a 2-column grid now */
  #game.mobile #mobRail{display:grid!important;grid-template-columns:46px 46px;gap:6px;height:auto!important;max-height:calc(100dvh - 70px);overflow:visible}
  #game.mobile #mobRail>*{width:46px!important;height:46px!important}
}
/* ── IN THE THEATRE ON A PHONE (22 Sep — "once I am in the stage there should be a button right away: chat, sound, exit"):
   the video's own bar carries the three it needs, always visible, at the phone scale. ── */
#game.mobile.mwatch #vidChatBtn{display:inline-flex!important}
#game.mobile.mwatch #videoOverlay .vid-controls{gap:6px;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px))}
#game.mobile.mwatch #videoOverlay .vid-mode,#game.mobile.mwatch #videoOverlay .vid-close{min-height:36px;padding:7px 10px;font-size:12.5px;white-space:nowrap}
#game.mobile.mwatch #stageAudio:not(.in-rail){bottom:calc(64px + env(safe-area-inset-bottom,0px))}
/* the pill inside the theatre bar: a bar button like its neighbours, always shown while watching */
#game.mobile #stageAudio.in-theatre{display:inline-flex!important;position:static!important;width:auto!important;height:36px!important;min-height:36px;padding:0 10px!important;border-radius:10px!important;top:auto!important;bottom:auto!important;left:auto!important;transform:none!important;z-index:auto!important;flex:0 0 auto;order:2}
#game.mobile #stageAudio.in-theatre #stageAudioTap,#game.mobile #stageAudio.in-theatre #stageAudioMute{height:100%;font-size:18px;padding:0 4px}
#game.mobile #stageAudio.in-theatre .sa-lbl{display:none}
/* upright pre-show: the three buttons share a row (28% each); the pill takes the room the row has left, in that same row */
#game.mobile.vidbar:not(.mwatch) #videoOverlay .vid-controls #stageAudio{order:2;flex:0 0 auto!important;width:auto!important;padding:0 8px!important}
#game.mobile.vidbar:not(.mwatch) #videoOverlay .vid-controls{gap:6px}
#game.mobile.vidbar:not(.mwatch) #videoOverlay .vid-mode,#game.mobile.vidbar:not(.mwatch) #videoOverlay .vid-close{flex:1 1 22%;padding:10px 4px;font-size:13px}
#game.mobile.mwatch #videoOverlay .vid-controls #vidClose{order:3}
#game.mobile.mwatch #videoOverlay .vid-controls #vidChatBtn{order:1}
#game.mobile.mwatch #videoOverlay .vid-controls #vidFullBtn{order:4}
/* SIDEWAYS: a chest / golden / bonus reward that is a VIDEO reads like the story card — video left, words and buttons right */
@media (orientation:landscape) and (max-height:520px){
  #game.mobile #resourcePanel.show:has(.r-video),#game.mobile #goldenPanel.show:has(.r-video){display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);column-gap:14px;width:min(700px,calc(100vw - 24px))}
  #game.mobile #resourcePanel.show:has(.r-video)>*,#game.mobile #goldenPanel.show:has(.r-video)>*{grid-column:2}
  #game.mobile #resourcePanel.show:has(.r-video)>#resourceBody,#game.mobile #goldenPanel.show:has(.r-video)>#goldenBody{grid-column:1;grid-row:1 / span 6;align-self:center}
  #game.mobile #resourcePanel.show .r-video,#game.mobile #goldenPanel.show .r-video{margin-top:0}
}
/* ── THE PHONE VIDEO BAR IS ICONS (22 Sep) ── one symbol each, 40px, the scrubber gets the rest of the row ── */
#game.mobile #videoOverlay .vid-controls .vid-mode,#game.mobile #videoOverlay .vid-controls .vid-close,#game.mobile #videoOverlay .vid-controls #vidClose{flex:0 0 40px!important;width:40px!important;min-width:0!important;height:36px!important;min-height:36px!important;padding:0!important;font-size:18px!important;line-height:1;border-radius:10px!important;white-space:nowrap;text-align:center;align-items:center;justify-content:center}   /* no display: here — which buttons show is still the theatre/walk/vidmin rules' call */
#game.mobile #videoOverlay .vid-controls{gap:6px!important;padding:8px 10px calc(8px + env(safe-area-inset-bottom,0px))!important;flex-wrap:nowrap!important}
#game.mobile #videoOverlay .vid-controls .vid-track{flex:1 1 auto!important;min-width:40px}
#game.mobile #videoOverlay .vid-controls #vidTime{display:none}
#game.mobile #videoOverlay .vid-controls .vid-play{width:36px;height:36px;font-size:14px;flex:0 0 36px}
#game.mobile #videoOverlay .vid-controls #stageAudio.in-theatre{flex:0 0 40px!important;width:40px!important;padding:0!important}
/* ── THE OFFER CARD SIDEWAYS (22 Sep — "the mystery box video should be split like the testimonials: video left, words right") ── */
@media (orientation:landscape) and (max-height:520px){
  #game.mobile #confirmPanel.offer.show,#game.mobile #discoveryPanel.offer.show{display:grid!important;grid-auto-flow:row dense;grid-template-columns:minmax(0,1.15fr) minmax(0,1fr);column-gap:14px;row-gap:4px;width:min(720px,calc(100vw - 24px));align-items:start;align-content:start}
  #game.mobile #confirmPanel.offer.show>*,#game.mobile #discoveryPanel.offer.show>*{grid-column:2;margin-top:0}
  #game.mobile #confirmPanel.offer.show>#confirmVid,#game.mobile #discoveryPanel.offer.show>#confirmVid{grid-column:1;grid-row:1 / span 9;width:100%;max-height:none;align-self:center;border-radius:10px;margin:0}
  #game.mobile #confirmPanel.offer.show>#confirmSound,#game.mobile #discoveryPanel.offer.show>#confirmSound{grid-column:1;justify-self:center;margin-top:4px}   /* the "Tap for sound" pill sits under its video */
  #game.mobile #confirmPanel.offer.show>.p-eyebrow,#game.mobile #discoveryPanel.offer.show>.p-eyebrow{grid-row:1}
  #game.mobile #confirmPanel.offer.show>h3,#game.mobile #discoveryPanel.offer.show>h3{grid-row:2;font-size:16px!important}
}

/* ══ THE NEW WORLD ON A PHONE: TOASTS STAND ABOVE THE THUMBSTICK, NOT ON THE CHARACTER ══
   JT, 23 Sep: the arrival toast "covered my character". The character is pinned at the resting thumbstick's height, and
   #toast.world sat at bottom:76px — exactly there. Derived from the same two variables as the stick, so retuning the stick
   moves the toasts with it. Centred: a left-anchored toast reads as a corner label on a 360px screen. */
#game.mobile.world-v2 #toast.world,#game.mobile.world-v2 #toast.world.raised{bottom:calc(100vh - var(--ww-joy-rest) + var(--ww-joy-size) / 2 + 10px);left:50%;transform:translateX(-50%);text-align:center;max-width:min(320px,86vw)}
/* the crowd reveal's pointer (v2CrowdPoint): the 👥 button pulses with a tag for seven seconds so "hide them any time" has a place */
.dr-btn.v2-point,.rail-btn.v2-point{animation:v2Point .9s ease-in-out 7;box-shadow:0 0 0 4px rgba(249,200,94,.95),0 0 24px 8px rgba(249,200,94,.55)!important;z-index:15}
.dr-btn.v2-point::after,.rail-btn.v2-point::after{content:'hide attendees';position:absolute;top:50%;transform:translateY(-50%);white-space:nowrap;background:#F9C85E;color:#1A1205;font:800 12px/1 "Montserrat",sans-serif;padding:7px 10px;border-radius:9px;box-shadow:0 6px 18px rgba(0,0,0,.45);pointer-events:none}
.dr-btn.v2-point::after{left:calc(100% + 12px)}
.rail-btn.v2-point::after{right:calc(100% + 10px)}
@keyframes v2Point{0%,100%{transform:scale(1)}50%{transform:scale(1.16)}}

/* V7: portrait chat uses the full width; world controls return when chat closes. */
@media(orientation:portrait){
 #game.mobile.chatshown #chatPanel{left:0;right:0;width:auto}
 #game.mobile.chatshown #mobRail{display:none}
 #game.mobile.mwatch.chatopen #demoVid{object-fit:contain}
}

#chatMsgs .cm-avatar{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;margin-right:8px;border-radius:50%;background:#263e58;color:#f2f7ff;font:800 11px/1 system-ui;vertical-align:middle}
#chatMsgs .nm{color:#dbeafe;margin-right:8px}


/* V7: the minimise control belongs in the flex row; an old absolute rule covered play/pause. */
#game.mobile #videoOverlay .vid-controls #vidClose{position:static!important;inset:auto!important}

/* Ask Host must animate even when the coin HUD is disabled. */
@keyframes wwAskHostPulse{0%,100%{box-shadow:0 0 0 0 rgba(173,207,246,.7)}50%{box-shadow:0 0 0 10px rgba(173,207,246,0)}}
@media(prefers-reduced-motion:reduce){#v2QBtn{animation:none!important}}

/* Mobile alpha: the world controls share the station's silver/navy palette. */
#roomCount{display:none!important}
#mobileAlpha{padding-left:8px;margin-left:8px;border-left:1px solid #8eabc4;color:#c3e5ff;font-size:11px;white-space:nowrap}
#game.mobile #navSheet{background:#0c1a2b;color:#e9f4ff;border:1px solid #7894ae}
#navSheet .p-eyebrow{color:#c4e0f8}
#navClose{background:#20364d;color:#e9f4ff}
#game.mobile .rail-btn,#game.mobile .wwi-mobile-bag{background:#102237;border-color:#7894ae;color:#d7eaff}
#game.mobile #joyWrap,#game.mobile #joyWrap:not(.live){left:calc(76px + env(safe-area-inset-left,0px));top:auto;bottom:calc(28px + env(safe-area-inset-bottom,0px));transform:translateX(-50%);opacity:.65}
#joyBase{background:#14273c99;border-color:#9bb9d480}
#joyKnob{background:#d2e6fa}
#game.mobile #joyWrap.live{opacity:1}
#joyWrap.teach:not(.live) #joyKnob{animation:ww-stick-demo 2.8s ease-in-out infinite}
#mobileMoveHint{position:fixed;bottom:calc(16px + env(safe-area-inset-bottom,0px));left:145px;right:65px;color:#dceeff;font:13px/1.4 Lato,sans-serif;pointer-events:none;z-index:13}
#mobileAlphaNotice{position:fixed;inset:0;z-index:16000;display:grid;place-items:center;padding:20px;background:#06111af2;color:#edf4ff}
#mobileAlphaNotice>div{max-width:440px;background:linear-gradient(145deg,#203951,#0b1929);border:1px solid #8cacc9;border-radius:22px;padding:26px;box-shadow:0 20px 80px #0008}
#mobileAlphaNotice small{letter-spacing:.18em;color:#a6d7ff}
#mobileAlphaNotice h2{font:800 28px/1.2 Montserrat,sans-serif;margin:18px 0}
#mobileAlphaNotice p{font:16px/1.6 Lato,sans-serif;color:#c8d8e8}
#mobileAlphaNotice button{width:100%;padding:16px;border-radius:12px;border:1px solid #d7eaff;background:linear-gradient(#f2f9ff,#bad2e9);color:#102035;font-weight:800;font-size:16px}
#jtFeedback{z-index:13000;max-width:440px;width:calc(100vw - 32px);padding:24px;background:#102237;color:#eef5ff;border:1px solid #8cacc9;border-radius:18px}
#jtFeedback textarea{box-sizing:border-box;width:100%;min-height:120px;padding:12px;margin:12px 0;background:#091725;color:#eef5ff;border:1px solid #8cacc9;border-radius:10px;font:16px/1.5 Lato,sans-serif}
#jtFeedback button{padding:12px 18px;border-radius:10px;min-height:44px;margin:4px;background:#d7eaff;color:#102035;border:1px solid #aac5de}
@keyframes ww-stick-demo{0%,15%,100%{transform:translate(-50%,-50%)}35%,50%{transform:translate(calc(-50% + 64px),calc(-50% - 12px))}70%,85%{transform:translate(calc(-50% - 64px),-50%)}}
@media(prefers-reduced-motion:reduce){#joyWrap.teach #joyKnob{animation:none}}
@media(max-height:500px){.wwi-chest{display:grid;grid-template-columns:1fr 1fr}.wwi-chest .wwi-header{grid-column:1/3}.wwi-chest .wwi-video{grid-column:1;grid-row:2/5;max-height:60dvh}.wwi-chest>.wwi-card{grid-column:2}.wwi-chest .wwi-media-message:empty{display:none}}
