/* ==========================================================================
   VR VIBE — main.css
   Plain CSS. No build step. Works from file://
   Type: Inter (self-hosted, see fonts.css)
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. TOKENS
   -------------------------------------------------------------------------- */
:root {
  /* Indigo/violet ground — the brand duotone */
  --indigo-950:  #120A33;
  --indigo-900:  #1B1050;
  --indigo-800:  #241465;
  --indigo-700:  #2E1A78;
  --indigo-600:  #4429B8;
  --violet-500:  #5B3FD9;
  --violet-400:  #7C5CFF;

  /* Brand accent — headline word, logo mark, focus rings */
  --accent:      #B57BFF;
  --accent-lift: #C79BFF;
  /* Same lavender for use on white. --accent itself is 2.90:1 there, a hair
     under the 3:1 large-text floor; this is 3.53:1. */
  --accent-ink:  #A566FF;

  /* Action colour. Mint is near-complementary to the purple ground, so the
     CTA separates from the background harder than any purple could. */
  --mint:        #43EFBA;
  --mint-lift:   #5CF3C6;

  /* Deal signal. Yellow, not pink — pink sits next to violet on the wheel
     and muddies against this background. Yellow is near-complementary,
     so it pops. 7.8:1 on the mid-indigo ground. */
  --deal:        #FFD43B;

  /* Text on dark */
  --text:        #FFFFFF;
  --text-soft:   #CFC7E0;
  --text-muted:  #9A8FB5;

  /* Light section */
  --paper:       #FFFFFF;
  --paper-ink:   #0B0710;   /* 19.4:1 on white ✓ AAA */
  --paper-muted: #6B6478;   /*  5.7:1 on white ✓ AA  */

  --ink-dark:    #0B0714;   /* button label on --accent — 7.2:1 ✓ AAA */

  /* Type */
  --font: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, Arial, sans-serif;
  /* Small uppercase labels — kickers, stat captions, quote attributions.
     These were monospace; the mechanical rhythm read as techy rather than
     premium next to Inter everywhere else. Same family as the body now, so
     the label look comes from the caps and the tracking, not a second face. */
  --font-label: var(--font);

  /* Reference measures ~10.4% of viewport width as the left gutter, and runs
     effectively full-bleed rather than in a narrow centred column. */
  --gutter: clamp(1.375rem, 5vw, 2.5rem);
  --wrap: 1800px;
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* ----------------------------------------------------------------------
     HERO PHOTO — THE ONE-LINE SWAP
     Drop the photo at assets/img/hero/hero.jpg, then change `none` to:
         url('../img/hero/hero.jpg?v=25')

     KEEP THE ?v= AND KEEP IT EQUAL TO THE ONE IN THE HTML. This is the only
     asset URL in the whole site that is written inside a stylesheet rather
     than in a page, and it is the same immutable one-year cache as
     everything else: replace the photo without changing this number and
     returning visitors keep seeing the old one indefinitely. It also has to
     match the <link rel="preload"> for the hero in every page, or the
     browser preloads one URL and the CSS then requests a different one —
     the hero downloads twice and the preload buys nothing.

     The duotone grade below crushes it into the brand indigo automatically.
     ---------------------------------------------------------------------- */
  --hero-photo: url('../img/hero/hero.jpg?v=25');
  /* Subjects sit centre-left in this shot, so bias right — keeps faces out
     from under the headline. */
  --hero-photo-position: 62% center;

  /* --- Glow reach -----------------------------------------------------------
     The neon edge on .clip__frame / .reasons__photo / .parent__shot /
     .kidgame__art paints this far OUTSIDE the element's border box: the
     largest blur (34px) plus its spread (8px).

     box-shadow is INK overflow — it contributes nothing to layout and nothing
     to scrollable overflow, so it is simply sliced off by the first ancestor
     that clips. For anything in normal flow that is `html, body { overflow-x:
     clip }`, which has to stay because the page must never pan sideways.

     Any glowing element therefore needs at least this much clear space between
     its border box and the nearest clipping edge. On a phone --gutter is 22px
     (the 1.375rem floor wins — 5vw is only 19.5px at 390px), i.e. 20px short,
     which is what cut the photo flat at both screen edges. Kept as a token so
     the allowances below stay honest if the shadow is ever retuned. */
  --glow-reach: 42px;
}


/* --------------------------------------------------------------------------
   2. RESET
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

/* --- Horizontal lock -------------------------------------------------------
   Several sections deliberately bleed past the gutter (the carousels, the
   hero glow, the words that slide in from off-canvas). Any one of those can
   let a thumb drag the whole page sideways on a phone and expose a strip of
   dead space beside it.

   overflow-x: clip, NOT hidden. `hidden` turns the element into a scroll
   container, which silently breaks position:sticky and can hijack anchor
   scrolling; `clip` just clips. Set on both html and body because either
   one can end up being the viewport's scrolling box.

   overscroll-behavior-x stops the rubber-band drag at the edges too. */
html,
/* Both elements, not just body. A sideways overflow escapes to whichever of
   the two is the scrolling box, so clipping only one leaves the page
   pannable — you pinch out on a phone and can then drag the whole layout
   left and right with empty gutters either side. overscroll-behavior stops
   the rubber-band that makes it feel draggable even when it is not. */
html,
body {
  max-width: 100%;
  overflow-x: clip;
  overscroll-behavior-x: none;
}

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--indigo-950);
  color: var(--text);
  font-family: var(--font);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, ol, ul { margin: 0; }
ol, ul { padding: 0; list-style: none; }
img { display: block; max-width: 100%; }

/* --- Form controls ---------------------------------------------------------
   button / input / select / textarea do NOT inherit `color`. The browser's own
   stylesheet sets it ON the element (`color: buttontext` / `fieldtext`), and an
   explicit declaration always beats inheritance — so the modal panels'
   `color: var(--paper-ink)` never reached them. Those are *system* colours,
   resolved per platform: near-black on macOS/Windows, but the system TINT
   (#007AFF) on iOS. That is the whole "black on desktop, blue on iPhone"
   report, and it hits exactly the four classes that never set a colour of
   their own — .bloc (location), .bchip (duration/time/players), .bdate (the
   date strip, built by book.js) and .bfield__input (the form fields).

   Element selectors, so specificity 0,0,1 — below every class rule in this
   file. .bchip.is-active keeps its white on violet, .btn--primary keeps its
   dark label on mint, .bback keeps --paper-muted, .gmodal__close stays white.
   Source order is therefore irrelevant and nothing deliberate is overridden. */
button,
input,
select,
textarea,
optgroup {
  color: inherit;
}

/* iOS paints its own glyph fill on disabled controls and ignores `color`
   outright, so -webkit-text-fill-color is the only property that wins. It also
   dims them on top of recolouring, hence opacity. Scoped to :disabled so the
   muted look of the .cpay__card mock-up fields is preserved deliberately
   rather than by accident. */
button:disabled,
input:disabled,
select:disabled,
textarea:disabled {
  -webkit-text-fill-color: currentColor;
  opacity: 1;
}

/* Safari repaints autofilled values with its own fill AND its own yellow
   background. #b-name and #b-phone carry autocomplete="name"/"tel", so this
   fires for any returning visitor. The background is not settable via
   background-color — a large inset shadow is the only lever. */
input:-webkit-autofill,
input:-webkit-autofill:hover,
input:-webkit-autofill:focus,
textarea:-webkit-autofill {
  -webkit-text-fill-color: var(--paper-ink);
  box-shadow: inset 0 0 0 100px #fff;
}

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 4px;
  border-radius: 2px;
}

.u-sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding-inline: var(--gutter);
}


/* ==========================================================================
   3. HEADER + NAV
   ========================================================================== */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 200;
  padding: 0.875rem 1.25rem;
  background: var(--accent);
  color: var(--ink-dark);
  font-size: 0.875rem;
  font-weight: 800;
  text-decoration: none;
}
.skip:focus { left: 0; }

.hdr {
  position: fixed;
  inset: 0 0 auto;
  z-index: 100;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease);
}

.hdr.is-stuck {
  background: rgba(13, 7, 32, 0.88);
  border-bottom-color: rgba(255, 255, 255, 0.10);
  -webkit-backdrop-filter: blur(14px) saturate(1.3);
  backdrop-filter: blur(14px) saturate(1.3);
}

/* --- Contact bar (Oakville / Brampton only) -------------------------------
   A thin strip ABOVE the nav carrying that arena's phone and email. It is
   part of the fixed header, so it scrolls away with nothing and simply sits
   on top of .hdr__bar. Hidden until .is-stuck: over the hero the header is
   transparent and a second row of small text on a photo is unreadable, so
   the bar only appears once the header has gone solid past the hero.
   -------------------------------------------------------------------------- */
.hdr__contact {
  display: none;                      /* opt in per page — see .hdr--loc */
  border-bottom: 1px solid rgba(255, 255, 255, 0.08);
  background: rgba(8, 5, 12, 0.55);
}

.hdr--loc .hdr__contact { display: block; }

/* Collapsed by default so it takes no vertical space over the hero, then
   opened by the same .is-stuck flag nav.js already toggles on scroll.
   grid-template-rows animates cleanly from 0 without needing a fixed height,
   which a two-line wrap on a phone would otherwise break. */
.hdr--loc .hdr__contact {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  transition: grid-template-rows 0.25s var(--ease), opacity 0.2s var(--ease);
}

.hdr--loc.is-stuck .hdr__contact {
  grid-template-rows: 1fr;
  opacity: 1;
}

.hdr__contact-inner {
  overflow: hidden;                   /* required for the 0fr collapse */
  display: flex;
  align-items: center;
  gap: clamp(1rem, 4vw, 2rem);
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  padding-inline: var(--gutter);
}

.hdr__contact-link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding-block: 0.5rem;
  color: var(--text-soft);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.16s var(--ease);
}

.hdr__contact-link:hover { color: var(--text); }

.hdr__contact-icon {
  flex: none;
  width: 15px;
  height: 15px;
  color: var(--accent);
}

/* On a narrow phone the email address is what gives, not the phone number:
   a tap-to-call is the action a local visitor actually wants from a header. */
@media (max-width: 30em) {
  .hdr__contact-link--email .hdr__contact-text { display: none; }
}

/* In-page anchor jumps have to clear the fixed header, or the section heading
   they were aimed at lands underneath it and the page looks like it scrolled
   to the wrong place. Applied to the anchor targets rather than globally so it
   cannot affect scrollIntoView anywhere else.

   Not paired with scroll-behavior: smooth — a long smooth scroll to a section
   far down the page reads as sluggish, and it fights the momentum scrolling on
   iOS. The jump is instant and lands correctly. */
:is(#faq, #amounts, #packages, #parent-reviews, #contact):target,
#faq, #amounts, #packages, #parent-reviews {
  scroll-margin-top: 5.5rem;
}

/* No backdrop-filter support: fall back to a solid bar rather than
   an unreadable translucent one. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .hdr.is-stuck { background: #0D0720; }
}

.hdr__bar {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  width: 100%;
  max-width: var(--wrap);
  height: 64px;
  margin: 0 auto;
  padding-inline: var(--gutter);
}


/* --- Logo ----------------------------------------------------------------- */
.logo {
  flex: none;
  margin-right: auto;
  color: var(--text);
  font-size: 1.5rem;
  font-weight: 900;
  letter-spacing: -0.035em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
}

.logo__mark { color: var(--accent); }
.logo__word { margin-left: 0.12em; }


/* --- Desktop nav ---------------------------------------------------------- */
.nav { display: none; }

.nav__list {
  display: flex;
  align-items: center;
  gap: 1.75rem;
}

.nav__link {
  color: var(--text-soft);
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  transition: color 0.16s var(--ease);
}

.nav__link:hover { color: var(--text); }

.nav__div {
  width: 1px;
  height: 15px;
  background: rgba(255, 255, 255, 0.22);
}

.btn--sm {
  min-height: 42px;
  padding: 0 1.25rem;
  font-size: 0.75rem;
  letter-spacing: 0.1em;
}

.hdr__cta { display: none; }


/* --- Burger --------------------------------------------------------------- */
.burger {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  margin-right: -0.5rem;
  padding: 0;
  border: 0;
  background: none;
  color: var(--text);
  cursor: pointer;
}

.burger__box {
  display: grid;
  gap: 6px;
  width: 24px;
}

.burger__line {
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 0.22s var(--ease), opacity 0.22s var(--ease);
}

.burger[aria-expanded="true"] .burger__line:first-child {
  transform: translateY(4px) rotate(45deg);
}
.burger[aria-expanded="true"] .burger__line:last-child {
  transform: translateY(-4px) rotate(-45deg);
}


/* --- Mobile menu ---------------------------------------------------------- */
.is-locked, .is-locked body { overflow: hidden; }

.menu {
  position: fixed;
  inset: 0;
  /* Overlay stack, in one place so it stays coherent:
       .skip    200  — must outrank everything, including an open overlay
       .gmodal  150  — game modal, opened from behind the menu
       .menu    120
       .bmodal  110  — booking and contact are peers, never both open
       .cmodal  110
       .hdr     100
     Everything that covers the viewport now sits above the header. Below it,
     the burger and the desktop nav + "Book Now" CTA stayed clickable through
     an open overlay, so a second overlay could be opened over the first —
     and the two share one .is-locked scroll lock, so the first one to close
     released it while the other was still up. */
  z-index: 120;
  background: var(--indigo-950);
  opacity: 0;
  transition: opacity 0.24s var(--ease);
}

/* opacity:0 does NOT stop hit-testing. Close drops .is-open immediately but
   only restores [hidden] behind a transition timer, so for a few hundred ms
   this is still a full-viewport, invisible layer that eats taps — and worse,
   .menu__inner is overflow-y:auto, so a wheel or swipe landing mid-screen
   scrolls the invisible panel's own overflow instead of the page. That is
   the "sometimes the site won't scroll" report: it self-heals when the timer
   fires, which is why it reads as intermittent. The timer is not even
   reliable, since transitionend bubbles and the listeners are {once:true} on
   the container — a descendant's transition can consume the one shot early.
   Making every non-open state transparent to input removes the whole class
   of symptom, regardless of how the teardown races. */
.menu:not(.is-open) { pointer-events: none; }

/* Top-left, clear of the notch and the right thumb. 44px is the minimum
   comfortable tap target, and it needs to be genuinely reachable because it
   is the only way out of this overlay on a touch device. */
.menu__close {
  position: absolute;
  top: max(1rem, env(safe-area-inset-top));
  left: var(--gutter);
  z-index: 1;
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: #fff;
  font-size: 1.75rem;
  line-height: 1;
  cursor: pointer;
  transition: background-color 0.16s var(--ease);
}

.menu__close:hover { background: rgba(255, 255, 255, 0.16); }

.menu.is-open { opacity: 1; pointer-events: auto; }
.menu[hidden] { display: none; }

.menu__inner {
  display: flex;
  flex-direction: column;
  height: 100%;
  padding:
    calc(96px + env(safe-area-inset-top)) var(--gutter)
    calc(2.5rem + env(safe-area-inset-bottom));
  overflow-y: auto;
}

.menu__list { display: grid; }

.menu__link {
  display: block;
  padding: 0.7rem 0;
  color: var(--text);
  font-size: clamp(1.75rem, 8vw, 2.25rem);
  font-weight: 900;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  text-decoration: none;
}

.menu__label {
  margin: 1.75rem 0 0.5rem;
  color: var(--text-muted);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.menu__link--sub {
  font-size: clamp(1.25rem, 5.5vw, 1.5rem);
  color: var(--accent);
}

/* The rating block that used to sit here pushed the CTA to the bottom with
   its own margin-top:auto. With it gone that job moves back to .menu__cta. */
.menu__cta {
  margin-top: auto;
  width: 100%;
}

/* --- Nav at desktop -------------------------------------------------------
   Breakpoint is 78em, not the 56.25em the hero layout uses. Seven nav items
   plus the button plus a 10vw gutter genuinely do not fit below ~1250px —
   at 56.25em the nav ran off the right edge of the screen. */
@media (min-width: 78em) {
  .hdr__bar  { height: 68px; }
  .nav       { display: block; }
  .hdr__cta  { display: inline-flex; }
  .burger    { display: none; }
  .menu      { display: none !important; }

  .nav__list { gap: clamp(1.0625rem, 1.55vw, 1.75rem); }

  /* --- Experiences dropdown -----------------------------------------------
     Opens on hover AND on keyboard focus (:focus-within), so it isn't
     mouse-only. The panel is positioned off the <li>, and the <li> keeps a
     bit of bottom padding so the pointer can travel from the link into the
     panel without crossing a dead gap and closing it. */
  .nav__item--has-sub {
    position: relative;
    padding-bottom: 0.75rem;
    margin-bottom: -0.75rem;
  }

  .nav__caret {
    display: inline-block;
    width: 0.38em;
    height: 0.38em;
    margin-left: 0.45em;
    border-right: 2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform: translateY(-0.15em) rotate(45deg);
    opacity: 0.7;
    transition: transform 0.18s var(--ease);
  }

  .nav__sub {
    position: absolute;
    top: 100%;
    left: 50%;
    z-index: 20;
    min-width: 13.5rem;
    padding: 0.5rem;
    border: 1px solid rgba(255, 255, 255, 0.14);
    background: rgba(13, 7, 32, 0.97);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    box-shadow: 0 18px 40px -12px rgba(0, 0, 0, 0.7);
    list-style: none;
    /* Hidden by clipping rather than display:none so it can transition. */
    opacity: 0;
    visibility: hidden;
    transform: translate(-50%, -0.4rem);
    transition: opacity 0.18s var(--ease), transform 0.18s var(--ease),
                visibility 0s linear 0.18s;
  }

  .nav__item--has-sub:hover .nav__sub,
  .nav__item--has-sub:focus-within .nav__sub {
    opacity: 1;
    visibility: visible;
    transform: translate(-50%, 0);
    transition-delay: 0s;
  }

  .nav__item--has-sub:hover .nav__caret,
  .nav__item--has-sub:focus-within .nav__caret {
    transform: translateY(0.05em) rotate(225deg);
  }

  .nav__sublink {
    display: block;
    padding: 0.625rem 0.75rem;
    color: var(--text-soft);
    font-size: 0.9375rem;
    font-weight: 700;
    text-decoration: none;
    white-space: nowrap;
    transition: background-color 0.15s var(--ease), color 0.15s var(--ease);
  }

  .nav__sublink:hover,
  .nav__sublink:focus-visible {
    background: rgba(255, 255, 255, 0.08);
    color: #fff;
  }
}


/* ==========================================================================
   4. HERO
   Left-aligned at every size. Bottom-anchored on mobile, centred
   vertically on desktop.
   ========================================================================== */
.hero {
  position: relative;
  isolation: isolate;
  display: flex;
  min-height: 100vh;      /* fallback */
  min-height: 100svh;     /* no mobile browser-chrome jump */
  overflow: hidden;
  background-color: var(--indigo-900);
}

/* --- The one-view guarantee ------------------------------------------------
   min-height: 100svh and deliberately NO max-height.

   svh is the SMALL viewport — the height with every browser toolbar showing.
   For a given device and orientation it is a CONSTANT: it does not move when
   the toolbars slide, so a 100svh box is one stable screen that is always
   fully visible.

   There used to be a `max-height: 100dvh` here. It was wrong twice over. dvh
   is *defined* to track the toolbars as they slide, so the hero physically
   resized mid-scroll — and because .hero__inner is a column flex container,
   every one of those reflows re-ran flex shrinking and the CTA visibly popped
   between its squashed and full height as you scrolled. Second, .hero has
   `overflow: hidden` for the inset:-1px background bleed, so the moment the
   height was capped, any content taller than the cap was silently CUT rather
   than being reachable — that is what put the $29.99 row under the Safari bar.

   Uncapped, the box grows and you scroll a few pixels in the worst case,
   which is always strictly better than losing the price. Section 11 sizes the
   content so that worst case does not arise on a real device. Do not
   reintroduce a max-height here, and do not use dvh in any layout property. */

/* --- The graded ground (what shows until a photo lands) ------------------- */
.hero::before {
  content: '';
  position: absolute;
  inset: -1px;
  z-index: 0;
  background:
    /* key light — the lit subject area, upper right */
    radial-gradient(85% 70% at 70% 24%,
      rgba(124, 92, 255, 0.85) 0%,
      rgba(91, 63, 217, 0.42) 34%,
      transparent 68%),
    /* shadow pooling, lower left */
    radial-gradient(75% 65% at 16% 88%,
      rgba(18, 10, 51, 0.92) 0%,
      rgba(18, 10, 51, 0.30) 55%,
      transparent 78%),
    /* saturated base */
    linear-gradient(158deg,
      var(--indigo-700) 0%,
      var(--indigo-800) 44%,
      var(--indigo-950) 100%);
}

/* --- Photo ---------------------------------------------------------------- */
.hero__media {
  position: absolute;
  inset: -1px;
  z-index: 1;
  background-image: var(--hero-photo);
  background-size: cover;
  background-position: var(--hero-photo-position);
  background-repeat: no-repeat;
  /* Desaturate first so the grade below can recolour cleanly. */
  filter: saturate(0.28) contrast(1.08) brightness(0.82);
}

/* --- Duotone grade --------------------------------------------------------
   `color` blend takes the hue+saturation of this layer and the luminance of
   the photo underneath — a true duotone. Falls back to a plain violet wash
   if a browser doesn't support the blend mode, which still looks correct. */
/* The video hero's own grade — the purple IS the look here, and it is the
   original. Do not neutralise this one: the sub-page photo hero has its own
   .phero__grade, which is deliberately colourless, and an earlier edit of
   mine matched both rules and drained the colour out of this one too. */
.hero__grade {
  position: absolute;
  inset: -1px;
  z-index: 2;
  background:
    linear-gradient(150deg,
      var(--violet-500) 0%,
      var(--indigo-600) 40%,
      var(--indigo-900) 100%);
  opacity: 0.72;
}

@supports (mix-blend-mode: color) {
  .hero__grade {
    mix-blend-mode: color;
    opacity: 0.92;
  }
}

/* --- Scrim — legibility over any photo. Left-weighted, where the text is. -- */
.hero__scrim {
  position: absolute;
  inset: -1px;
  z-index: 3;
  background:
    linear-gradient(to top,
      rgba(11, 6, 26, 0.92) 0%,
      rgba(11, 6, 26, 0.72) 30%,
      rgba(11, 6, 26, 0.32) 62%,
      rgba(11, 6, 26, 0.22) 100%);
}

.hero__inner {
  position: relative;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* The stack fills the full phone screen at any height: the block margins
     below are the MINIMUM gaps and leftover height is shared between them,
     so the price always lands at the bottom instead of leaving dead space. */
  justify-content: space-between;
  width: 100%;
  max-width: var(--wrap);
  margin: 0 auto;
  min-height: 100vh;
  min-height: 100svh;
  /* Top offset does double duty: it drops the headline clear of the header
     so it stops reading as attached to it, and because the stack is
     space-between, every rem added here comes back out of the gaps —
     lower headline AND tighter spacing between the blocks. */
  /* svh, not vh. vh resolves to the LARGE viewport on mobile (toolbars
     hidden) while the box above is 100svh (toolbars shown), so mixing them
     computes the padding against a taller reference than the container and
     pushes the price below the fold on a real device. Emulators have no
     dynamic toolbars, which is why this only shows up on a real phone. */
  padding:
    calc(clamp(6rem, 15vh, 10.5rem) + env(safe-area-inset-top)) var(--gutter)
    calc(2.5rem + env(safe-area-inset-bottom));
  padding:
    calc(clamp(6rem, 15svh, 10.5rem) + env(safe-area-inset-top)) var(--gutter)
    calc(2.5rem + env(safe-area-inset-bottom));
  text-align: left;
}


/* --- Headline ------------------------------------------------------------- */
/* Phone reference: the headline dominates the screen — FOUR lines
   (STEP / INTO / ANOTHER / WORLD.), with "ANOTHER" running nearly the full
   content width. That works out to ~17.5vw on phones. Desktop reference is
   ~110px at 1440px, so a flatter linear takes over above ~430px, and 12vh
   guards short viewports.

   Solves to: 320px→50px · 390px→60px · 430px→67px · 768px→78px · 1440px→99px.

   The four-line phone layout is forced by <br class="hero__br"> in the
   markup (hidden from 48em up) — at these sizes "STEP INTO" wouldn't
   reliably self-wrap, and the reference is unambiguous about the break. */
.hero__title {
  margin-bottom: 1.5rem;
  margin-left: -0.03em;           /* optical alignment: cancels the S sidebearing */
  font-size: clamp(2.75rem, min(3.4rem + 3.1vw, 15.5vw, 11.5vh), 8.75rem);
  font-size: clamp(2.75rem, min(3.4rem + 3.1vw, 15.5vw, 11.5svh), 8.75rem);
  font-weight: 900;
  /* Sub-1 leading is safe here: it's all-caps, so there are no descenders
     to collide with the line below. */
  line-height: 0.95;
  letter-spacing: -0.038em;
  text-transform: uppercase;
}

/* --- Optional video background ---------------------------------------------
   Sits in the same layer as .hero__media and after it in the DOM, so the
   footage paints over the photo and still lands under the duotone grade and
   the scrim — the clip gets the identical brand treatment a still would.

   The photo stays in the markup underneath on purpose: it's the poster while
   the video buffers, and it's the whole background for anyone on
   prefers-reduced-motion, where the video is dropped entirely. */
.hero__video {
  position: absolute;
  inset: -1px;
  z-index: 1;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  object-fit: cover;
  /* Lighter than the photo's saturate(0.28): motion is the reason to use a
     video at all, and at full duotone strength you lose it. */
  filter: saturate(0.55) contrast(1.05) brightness(0.7);
}

/* Footage carries far more visual noise than a still, so the grade comes
   down and the scrim comes up — the headline has to stay readable over a
   frame we don't control. */
.hero--video .hero__grade { opacity: 0.55; }

@supports (mix-blend-mode: color) {
  .hero--video .hero__grade { opacity: 0.7; }
}

.hero--video .hero__scrim {
  background:
    linear-gradient(to top,
      rgba(11, 6, 26, 0.94) 0%,
      rgba(11, 6, 26, 0.80) 30%,
      rgba(11, 6, 26, 0.46) 62%,
      rgba(11, 6, 26, 0.40) 100%);
}

@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
}


/* --- Kicker above the headline --------------------------------------------
   Sub-pages only — the landing page headline stands alone. Kicker and h1 are
   wrapped in .hero__head so the space-between stack treats them as one block;
   left loose they'd get pushed apart by the distributed leftover height.
   The h1's own margin-bottom stays inside the wrapper and still supplies the
   gap down to the subhead (flex items don't collapse margins out). */
.hero__head { max-width: 100%; }

.hero__kick {
  margin-bottom: 0.875rem;
  color: var(--accent);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

.hero__period { color: var(--text); }

/* Phone-only line break: STEP / INTO as separate lines, per the reference.
   From tablet up "STEP INTO" fits on one line, so the break goes away. */
@media (min-width: 48em) {
  .hero__br { display: none; }
}


/* --- "WORLD" neon glitch --------------------------------------------------
   Two offset colour layers (cyan + violet) clipped into shifting bands,
   running continuously. Offsets stay in the 2–6px range so it reads as
   chromatic aberration / signal noise rather than the text falling apart. */
.glitch {
  position: relative;
  display: inline-block;
  color: var(--accent);
  text-shadow:
    0 0 20px rgba(181, 123, 255, 0.45),
    0 0 52px rgba(110, 231, 255, 0.16);
}

.glitch::before,
.glitch::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  text-shadow: none;
}

/* Deliberately mismatched durations — 1.1s against 1.7s only realigns every
   ~19s, so the two layers never settle into a visible repeating pattern. */
.glitch::before {
  color: #6EE7FF;                    /* cyan */
  animation: glitch-cyan 1.1s steps(1) infinite;
}

.glitch::after {
  color: #E9C0FF;                    /* pale violet */
  animation: glitch-violet 1.7s steps(1) infinite;
}

@keyframes glitch-cyan {
  0%   { opacity: .70; clip-path: inset(4%  0 78% 0); transform: translate(-3px, -1px); }
  8%   { opacity: .80; clip-path: inset(38% 0 40% 0); transform: translate( 4px,  1px); }
  16%  { opacity: .65; clip-path: inset(66% 0 14% 0); transform: translate(-5px,  0);   }
  24%  { opacity: .85; clip-path: inset(12% 0 66% 0); transform: translate( 2px, -1px); }
  32%  { opacity: .70; clip-path: inset(52% 0 28% 0); transform: translate(-4px,  1px); }
  40%  { opacity: .90; clip-path: inset(24% 0 54% 0); transform: translate( 5px,  0);   }
  48%  { opacity: .60; clip-path: inset(72% 0  8% 0); transform: translate(-2px, -1px); }
  56%  { opacity: .85; clip-path: inset(30% 0 48% 0); transform: translate( 3px,  1px); }
  64%  { opacity: .70; clip-path: inset(58% 0 22% 0); transform: translate(-6px,  0);   }
  72%  { opacity: .80; clip-path: inset(8%  0 72% 0); transform: translate( 4px, -1px); }
  80%  { opacity: .65; clip-path: inset(44% 0 34% 0); transform: translate(-3px,  1px); }
  88%  { opacity: .90; clip-path: inset(68% 0 10% 0); transform: translate( 2px,  0);   }
  96%  { opacity: .70; clip-path: inset(18% 0 60% 0); transform: translate(-4px, -1px); }
  100% { opacity: .70; clip-path: inset(4%  0 78% 0); transform: translate(-3px, -1px); }
}

/* --- Glitch on a white ground ---------------------------------------------
   Same effect, retuned for paper. --accent is only 2.90:1 on white so the
   base word takes --accent-ink, and the neon glow is dropped — a glow needs
   darkness behind it to read as light rather than as a smudge. The two
   offset layers get deeper hues for the same reason; the hero's cyan and
   pale violet all but vanish on white. */
.glitch--light {
  color: var(--accent-ink);
  text-shadow: none;
}

.glitch--light::before { color: #0093B8; }   /* deep cyan  */
.glitch--light::after  { color: #B25CFF; }   /* deep violet */

/* --- Cycling word ---------------------------------------------------------
   The box, first. Every word in the rotation is present as a hidden sizer,
   and all of them — sizers and the live word — share one grid cell. Grid
   sizes that cell to the widest item, so the box is locked to the longest
   word from first paint and the swap can never reflow the line above it.

   visibility:hidden rather than display:none: a hidden item still
   contributes its width to the grid track, a removed one doesn't. */
/* The word has to sit on the same axis the rest of the heading does, and
   that axis changes with the breakpoint: the heading is left-aligned on
   mobile and centred on desktop. So the word aligns left here and centre
   there. Getting this wrong is very visible — the box is locked to the
   width of the longest word, so a short word aligned the wrong way hangs
   off to one side of the line above it. */
.cycle {
  display: inline-grid;
  justify-items: start;
  vertical-align: top;
  /* The section title runs at line-height 0.92, tight enough that the glitch
     layers on this word nearly touch the line above it. em so the gap tracks
     the headline size at every breakpoint. */
  margin-top: 0.18em;
}

.cycle > * { grid-area: 1 / 1; }

.cycle__sizer { visibility: hidden; }

/* The swap itself is deliberately plain — a short fade with a small upward
   slide. The words are the interesting part; the transition is just how they
   get on and off, so it stays out of the way of the glitch already running
   underneath it.

   inline-block because a slide needs a transform, and transforms don't apply
   to inline boxes. */
/* Must stay in step with FADE in cycler.js. */
.cycler {
  transition:
    opacity 0.13s ease,
    transform 0.13s ease;
}

/* Leaving: lifts and fades. Arriving is the same move in reverse, set from
   JS between the two halves of the swap. */
.cycler.is-out {
  opacity: 0;
  transform: translateY(-0.16em);
}

/* transition:none matters. This class is applied for a couple of frames to
   move the (already invisible) word to its start position below the
   baseline. Without suppressing the transition it would animate INTO that
   position and the fade-in would then start from wherever it happened to be
   mid-flight — the word would drift in from above instead of below. */
.cycler.is-next {
  opacity: 0;
  transform: translateY(0.16em);
  transition: none;
}

@keyframes glitch-violet {
  0%   { opacity: .70; clip-path: inset(54% 0 26% 0); transform: translate( 3px,  1px); }
  8%   { opacity: .60; clip-path: inset(10% 0 70% 0); transform: translate(-4px, -1px); }
  16%  { opacity: .85; clip-path: inset(34% 0 44% 0); transform: translate( 5px,  0);   }
  24%  { opacity: .70; clip-path: inset(70% 0 10% 0); transform: translate(-2px,  1px); }
  32%  { opacity: .90; clip-path: inset(20% 0 58% 0); transform: translate( 4px, -1px); }
  40%  { opacity: .65; clip-path: inset(60% 0 20% 0); transform: translate(-5px,  0);   }
  48%  { opacity: .80; clip-path: inset(6%  0 76% 0); transform: translate( 2px,  1px); }
  56%  { opacity: .70; clip-path: inset(46% 0 32% 0); transform: translate(-3px, -1px); }
  64%  { opacity: .85; clip-path: inset(74% 0  6% 0); transform: translate( 6px,  0);   }
  72%  { opacity: .60; clip-path: inset(26% 0 52% 0); transform: translate(-4px,  1px); }
  80%  { opacity: .90; clip-path: inset(62% 0 18% 0); transform: translate( 3px, -1px); }
  88%  { opacity: .70; clip-path: inset(14% 0 64% 0); transform: translate(-2px,  0);   }
  96%  { opacity: .80; clip-path: inset(40% 0 38% 0); transform: translate( 5px,  1px); }
  100% { opacity: .70; clip-path: inset(54% 0 26% 0); transform: translate( 3px,  1px); }
}


/* --- Subhead -------------------------------------------------------------- */
/* Full content width on mobile — a narrow measure here just forces more
   lines on a screen that has no width to spare. */
.hero__sub {
  max-width: 100%;
  margin-bottom: 1.75rem;
  color: var(--text-soft);
  font-size: clamp(1.125rem, 5.2vw, 1.4375rem);
  line-height: 1.5;
  text-wrap: pretty;
}

/* One line, sitting above the facts. Tighter margin than a full .hero__sub
   because the facts below carry their own generous row spacing — the two
   together should read as one block, not two stacked paragraphs. */
.hero__sub--tight {
  margin-bottom: clamp(0.875rem, 2.5vw, 1.25rem);
}

/* --- Hero facts — the three-up that replaced the subtext -------------------
   The brief was: identical footprint. The paragraph it replaced was 5 lines at
   390px (~20.3px type, 1.5 leading = ~152px) plus a 1.75rem bottom margin, and
   the whole hero stack is space-between inside a fixed-height box — so if this
   block came out any shorter, EVERY gap above and below it would redistribute
   and the headline, button and price would all shift. Nothing else in the hero
   is allowed to move.

   So the height is pinned, not left to content: three rows at 3.125rem each is
   150px, within 2px of the paragraph, and the same 1.75rem margin closes it
   out. The rows are a grid rather than flex so all three are exactly equal
   regardless of label length.

   Vertical on phones, horizontal from 48em — matching the reference. */
.hero__facts {
  display: grid;
  grid-auto-rows: 3.125rem;
  align-content: start;
  margin: 0 0 1.75rem;
  padding: 0;
  list-style: none;
}

.hero__fact {
  display: flex;
  align-items: center;
  gap: 0.6875rem;
  color: var(--text);
  font-size: clamp(0.9375rem, 4vw, 1.0625rem);
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.2;
}

.hero__fact-icon {
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--accent);
}

/* From 48em the three sit on one line, as in the reference. The row height
   stops being a stack of three and becomes a single row, so the reserved
   space collapses to one row plus the same closing margin — which is what the
   desktop paragraph occupied there anyway. */
@media (min-width: 48em) {
  .hero__facts {
    grid-auto-flow: column;
    grid-auto-columns: max-content;
    grid-auto-rows: auto;
    gap: clamp(1.25rem, 3vw, 2.25rem);
    align-items: center;
  }
}



/* --- Button --------------------------------------------------------------
   Light accent fill, dark label, uppercase, letterspaced, near-square.
   Flat — no gradient, no glow, no icon. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding: 0 1.75rem;
  border: 0;
  border-radius: 0;                 /* reference is square — keep it square */
  font-family: inherit;
  font-size: clamp(0.8125rem, 3.3vw, 0.875rem);
  font-weight: 800;
  letter-spacing: 0.085em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;              /* never let "Book Now" break in two */
  cursor: pointer;
  transition: background-color 0.16s var(--ease);
}

.btn--primary {
  color: var(--ink-dark);              /* 13.6:1 on --mint ✓ AAA */
  background-color: var(--mint);
}

.btn--primary:hover { background-color: var(--mint-lift); }

/* Two-line button: label over capacity. */
.btn--stack {
  flex-direction: column;
  gap: 0.35rem;
  min-height: 0;
  padding: 1.375rem 2.5rem 1.4375rem;
  font-size: 1.0625rem;
  line-height: 1;
}

.btn__sub {
  color: #1A4A3A;                   /* 6.9:1 on --mint ✓ AA */
  font-size: 0.8125rem;
  font-weight: 700;
  letter-spacing: 0.1em;
}


/* --------------------------------------------------------------------------
   4. SUMMER SPECIAL
   Flat typography. No card, no border, no tinted panel — those are what
   made it read cheap. Size and colour do the work.
   -------------------------------------------------------------------------- */
.offer { margin-top: 1.5rem; }

.offer__label {
  display: flex;
  flex-wrap: wrap;              /* "Ending August 30th" is long — let it drop
                                   to a second line on narrow screens instead
                                   of squeezing */
  align-items: center;
  gap: 0.55rem;
  margin-bottom: 0.5rem;
  color: var(--deal);
  font-size: clamp(0.75rem, 3vw, 0.8125rem);   /* mobile size — leave as-is */
  font-weight: 800;
  letter-spacing: 0.15em;
  text-transform: uppercase;
}

.offer__sep { opacity: 0.5; }
.offer__urgent { color: var(--text); }

.offer__price {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
  line-height: 1;
}

.offer__was {
  color: var(--text-muted);
  font-size: clamp(1.25rem, 6vw, 1.75rem);
  font-weight: 500;
  text-decoration-thickness: 2px;
  text-decoration-color: var(--deal);
}

/* Kept deliberately below the headline size — it's the second thing you
   read, not the first. */
.offer__now {
  color: var(--deal);
  font-size: clamp(2.25rem, 13.5vw, 3.5rem);
  font-weight: 900;
  letter-spacing: -0.04em;
}

.offer__unit {
  font-size: 0.36em;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}


/* --------------------------------------------------------------------------
   5. HERO — DESKTOP  (still left-aligned, vertically centred)
   -------------------------------------------------------------------------- */
@media (min-width: 56.25em) {
  /* Reference gutter is ~10.4% of viewport width */
  :root { --gutter: clamp(3rem, 10vw, 11rem); }

  /* Asymmetric on purpose. The block is vertically centred between these two
     paddings, so moving 3rem from the bottom to the top slides the whole
     stack down by ~1.5rem without changing the section's height or any of
     the internal spacing — it was sitting too close under the header. */
  .hero__inner {
    justify-content: center;
    min-height: auto;
    padding-top: 10rem;
    padding-bottom: 4rem;
  }

  /* On mobile the crop is much tighter — pull toward the main subject. */
  :root { --hero-photo-position: 58% center; }

  /* Headline runs ~33% of viewport width in the reference. The old 5.5rem
     cap was clamping it on wide screens while the subhead kept growing,
     which collapsed the size contrast between them. */
  /* The base rule's smaller scale is a MOBILE-only decision — restore the
     original desktop size here. Solves to 900px→91px · 1440px→110px, the
     same as before the mobile tightening. Leading and tracking stay at the
     tighter base values; only the size reverts. */
  .hero__title {
    margin-bottom: 1.75rem;
    font-size: clamp(3rem, min(3.75rem + 3.47vw, 15vw, 12vh), 9.5rem);
    font-size: clamp(3rem, min(3.75rem + 3.47vw, 15vw, 12svh), 9.5rem);
  }

  /* Bigger CTA to match the bigger headline */
  .btn--stack {
    padding: 1.25rem 2.625rem 1.35rem;
    font-size: 1.0625rem;
    gap: 0.4rem;
  }
  .btn__sub { font-size: 0.875rem; }

  /* Subhead is roughly a quarter of the headline size in the reference —
     unmistakably subordinate. Wider measure so it sets in 3 lines, not 5. */
  .hero__sub {
    max-width: 36rem;
    margin-bottom: 2.25rem;
    font-size: 1.1875rem;
    line-height: 1.5;
  }

  .offer { margin-top: 1.75rem; }
  .offer__label { font-size: 1.0625rem; letter-spacing: 0.13em; }

  /* Darken toward the left, so a photo stays visible on the right */
  .hero__scrim {
    background:
      linear-gradient(to right,
        rgba(11, 6, 26, 0.90) 0%,
        rgba(11, 6, 26, 0.74) 32%,
        rgba(11, 6, 26, 0.34) 62%,
        rgba(11, 6, 26, 0.18) 100%),
      linear-gradient(to top,
        rgba(11, 6, 26, 0.55) 0%,
        transparent 48%);
  }
}


/* ==========================================================================
   6. HOW IT WORKS — light section
   ========================================================================== */
.how {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--paper);
  color: var(--paper-ink);
}

.kicker {
  margin-bottom: 1.5rem;
  color: #6D28D9;                    /* 7.0:1 on white ✓ AAA */
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.section-title {
  margin-bottom: clamp(3rem, 6vw, 4.5rem);
  margin-left: -0.03em;
  /* Mobile floor and slope raised — the desktop cap is unchanged. */
  font-size: clamp(2.9rem, 10vw, 4.5rem);
  font-weight: 900;
  line-height: 0.92;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}

.section-title__mute { color: var(--paper-muted); }

/* --- Desktop: the proof heading centres ------------------------------------
   Everything else stays left. The page hangs off one flush left gutter —
   hero, step numbers, every card in every rail — and centring a heading
   above a left-aligned grid breaks that spine.

   The proof section is the exception because what sits under it is genuinely
   symmetrical: a full-bleed carousel with no left edge to line up with.

   margin-left on .section-title is a -0.03em optical nudge for a left edge
   that no longer exists once centred, so it's zeroed out here. */
@media (min-width: 56.25em) {
  .proof .section-title {
    margin-left: 0;
    text-align: center;
  }

  /* Follows the heading: centred only where the heading is centred. A little
     more breathing room than mobile, since the type is much larger here. */
  .cycle {
    justify-items: center;
    margin-top: 0.22em;
  }

  .worlds__hint--light { text-align: center; }
}

.steps {
  display: grid;
  gap: 2.75rem;
}

.step {
  padding-top: 1.5rem;
  border-top: 2px solid var(--paper-ink);
}

.step__num {
  display: block;
  margin-bottom: 1.75rem;
  color: #6D28D9;
  font-size: 0.875rem;
  font-weight: 800;
  letter-spacing: 0.14em;
}

.step__title {
  margin-bottom: 0.75rem;
  font-size: clamp(1.375rem, 4.5vw, 1.625rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -0.03em;
}

.step__body {
  max-width: 38ch;
  color: var(--paper-muted);
  font-size: 1.0625rem;
  line-height: 1.6;
  text-wrap: pretty;
}

/* Mobile-only CTA under the last step. Full width, matching the hero
   button's weight so it reads as the same action. */
.how__cta {
  width: 100%;
  margin-top: 2.75rem;
  min-height: 58px;
}

/* The step icon. Sits on the same line as the number, which is what lets the
   whole step collapse to two short lines plus a caption on a phone. */
.step__icon {
  display: block;
  width: 2rem;
  height: 2rem;
  margin-bottom: 0.75rem;
  color: #6D28D9;
}

@media (min-width: 48em) {
  .steps { grid-template-columns: repeat(3, 1fr); gap: 2.5rem; }
  .how__cta { display: none; }
  .step__icon { width: 2.25rem; height: 2.25rem; }
}

/* --- Phones: the whole section in one screen -------------------------------
   The brief was that you should not have to scroll to see all three steps.
   Measured on a 390x664 phone the old section came to ~1010px: 72px of section
   padding top, a 24px kicker, a ~96px two-line title, then three steps at
   ~196px each (a 28px number with a 28px margin, a 2-line title, a 3-line
   body) with 44px gaps, then a 58px button and 72px of padding.

   Everything below is aimed at that budget and nothing else:
   - the number and the icon share a row instead of stacking (saves ~28px/step)
   - the body is one short line now the copy is shorter (saves ~34px/step)
   - gaps and section padding come in, since with icons the steps read as
     separate blocks without needing as much air

   That lands at ~600px, comfortably inside 664px with the header allowed for.
   Desktop keeps the roomier original — it has never had a fitting problem. */
@media (max-width: 47.99em) {
  .how { padding-block: clamp(2.5rem, 7vw, 3.5rem); }

  .how .kicker { margin-bottom: 0.75rem; }

  .how .section-title { margin-bottom: 1.5rem; }

  .steps { gap: 1.25rem; }

  .step {
    display: grid;
    grid-template-columns: auto 1fr;
    grid-template-areas:
      "icon num"
      "icon title"
      "icon body";
    column-gap: 0.875rem;
    align-items: center;
    padding-top: 1rem;
  }

  /* The icon spans all three rows, so the number, title and body stack beside
     it instead of under it — that vertical stacking was most of the height. */
  .step__icon {
    grid-area: icon;
    align-self: center;
    width: 2.25rem;
    height: 2.25rem;
    margin-bottom: 0;
  }

  .step__num   { grid-area: num;   margin-bottom: 0.125rem; font-size: 0.75rem; }
  .step__title { grid-area: title; margin-bottom: 0.125rem; font-size: 1.1875rem; }
  .step__body  { grid-area: body;  font-size: 0.9375rem; line-height: 1.4; }

  .how__cta { margin-top: 1.5rem; min-height: 52px; }
}


/* ==========================================================================
   7. WORLDS — game browser
   Mobile: one horizontal scroll-snap rail per category (native scrolling —
   a JS carousel here would be worse on touch, not better).
   Desktop: the rail becomes a grid and everything is visible at once.
   ========================================================================== */
.worlds {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--indigo-950);
  color: var(--text);
}

/* Kicker + section title were written for the white section — these
   re-tune them for the dark ground. */
.kicker--dark { color: var(--accent); }
.section-title--dark { color: var(--text); }
.section-title__accent { color: var(--accent); }

/* --accent is tuned for the dark ground. On white it lands at 2.90:1 — just
   under the 3:1 large-text floor — so the white sections use --accent-ink
   instead: the same lavender nudged two steps darker, 3.53:1. Close enough
   that it reads as one brand colour across light and dark; the deeper
   --violet-500 did not. */
.proof .section-title__accent,
.reviews .section-title__accent,
.pile .section-title__accent,
.picker .section-title__accent,
.feats .section-title__accent,
.how .section-title__accent { color: var(--accent-ink); }

.worlds__cat { margin-top: clamp(3rem, 7vw, 4.5rem); }
.worlds__cat:first-of-type { margin-top: 0; }

/* The gap down to the rail lives here, not on the hint below it — the hint
   is hidden above 48em, and when it carried the spacing the desktop title
   ended up sitting flush on top of the cards. */
.worlds__cat-title {
  margin-bottom: clamp(1.5rem, 3vw, 2rem);
  font-size: clamp(1.5rem, 5.5vw, 2.5rem);
  font-weight: 900;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.worlds__hint {
  /* Pulls back up into the title's gap so the mobile spacing is unchanged. */
  margin-top: -1.125rem;
  margin-bottom: 1.5rem;
  color: var(--text-muted);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}


/* --- The rail (mobile carousel) -------------------------------------------
   Negative margin + matching padding lets cards bleed to the screen edge
   while the first one still lines up with the section gutter. The 82% card
   width leaves the next card peeking, which is what tells people to swipe. */
.rail {
  display: flex;
  gap: 1rem;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  padding-bottom: 0.5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  /* Snap positions default to the scrollPORT edge, which ignores the padding
     above — so a swiped card would land flush against the screen edge while
     the heading kept its gutter. This pulls the snap line in to match. */
  scroll-padding-inline: var(--gutter);
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.rail::-webkit-scrollbar { display: none; }

.rail__item {
  flex: 0 0 82%;
  max-width: 20rem;
  scroll-snap-align: start;
}


/* --- Card ----------------------------------------------------------------- */
.game {
  display: flex;
  flex-direction: column;
  height: 100%;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.12);
  background: rgba(255, 255, 255, 0.04);
  transition: border-color 0.18s var(--ease), transform 0.18s var(--ease);
  /* The whole card opens the modal (games.js), not just the button — so the
     whole card has to look clickable. Without this the hover lift reads as
     decoration and people keep aiming for the small button. */
  cursor: pointer;
}

/* games.js gives the card tabindex and role="button", so it lands in the tab
   order and needs a visible focus ring like any other control. */
.game:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}

.game:hover {
  border-color: rgba(181, 123, 255, 0.55);
  transform: translateY(-3px);
}

/* Thumbnail. Set --game-img on the .game__thumb to drop a photo in:
       style="--game-img: url('../img/games/zombie.jpg')"
   Until then the gradient below stands in. */
.game__thumb {
  position: relative;
  aspect-ratio: 4 / 3;
  background-image: var(--game-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
}

.game__thumb::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 60% at 68% 28%, rgba(124, 92, 255, 0.55), transparent 70%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
  opacity: 1;
  transition: opacity 0.2s var(--ease);
}

/* Once a real photo is set, drop the placeholder wash to a light tint. */
.game__thumb[style*="--game-img"]::after {
  background: linear-gradient(to top, rgba(11, 6, 26, 0.55), transparent 55%);
}

/* Badge — "Best Seller", "Popular", "New · Trending".
   Colour-coded by what the badge means, not decorated at random: hot things
   burn, crowd-pleasers are mint, horror is blood. Dark ink on the bright
   fills — white on mint or gold is unreadable.

   z-index because .game__thumb::after is generated last and would otherwise
   paint the wash straight over the top of it. */
.game__tag {
  position: absolute;
  z-index: 1;
  top: 0.75rem;
  left: 0.75rem;
  padding: 0.4375rem 0.8125rem;
  border-radius: 999px;
  background: #fff;
  color: var(--ink-dark);
  font-size: 0.6875rem;
  font-weight: 900;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Dark halo under the pill so a bright badge still separates from a bright
     photo once real thumbnails land. */
  box-shadow: 0 2px 12px rgba(11, 6, 26, 0.45);
}

/* New / trending — the loudest one on the page, and deliberately the only
   gradient. It marks the two cards leading each rail; if every badge shouted
   none of them would. */
.game__tag--hot {
  background: linear-gradient(96deg, #FF2D77, #FF7A2F);
  color: #fff;
  text-shadow: 0 1px 2px rgba(120, 12, 48, 0.5);
}

/* Crowd-pleasers — mint is the site's action colour, so it reads as "safe
   pick" without competing with the hot gradient. */
.game__tag--pop { background: var(--mint); }

/* Staff / player picks. */
.game__tag--fav { background: var(--accent-lift); }

/* Horror. */
.game__tag--horror {
  background: #E01235;
  color: #fff;
}

/* Everything else worth flagging — new but not yet trending, team challenge. */
.game__tag--note { background: #FFC53D; }

.game__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 1.125rem 1.125rem 1.25rem;
}

.game__name {
  margin-bottom: 0.4375rem;
  font-size: 1.125rem;
  font-weight: 900;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.game__meta {
  display: flex;
  align-items: center;
  gap: 0.4375rem;
  margin-bottom: 1.125rem;
  color: var(--text-muted);
  font-size: 0.8125rem;
  font-weight: 600;
}

.game__players { color: var(--text-soft); }
.game__dot { opacity: 0.5; }

/* margin-top:auto pins the button to the card bottom, so buttons line up
   across cards even when names wrap to different line counts. */
.game__btn {
  margin-top: auto;
  width: 100%;
}


/* Mobile only: reorder the Escape Rooms rail to
   Mansion → Break-In → Wayfinders → B-Block → Don't Scream → Abyss.
   Every card gets an explicit order so there's no default-0 card sorting
   ahead of the rest. DOM order stays badge-first for the desktop grid, which
   this media query doesn't touch. Mapping (DOM nth-child → visual slot):
     1 Break-In→2  2 Mansion→1  3 B-Block→4  4 Wayfinders→3  5 Don't Scream→5  6 Abyss→6 */
@media (max-width: 47.99em) {
  .worlds__cat + .worlds__cat .rail__item:nth-child(1) { order: 2; }
  .worlds__cat + .worlds__cat .rail__item:nth-child(2) { order: 1; }
  .worlds__cat + .worlds__cat .rail__item:nth-child(3) { order: 4; }
  .worlds__cat + .worlds__cat .rail__item:nth-child(4) { order: 3; }
  .worlds__cat + .worlds__cat .rail__item:nth-child(5) { order: 5; }
  .worlds__cat + .worlds__cat .rail__item:nth-child(6) { order: 6; }
}

/* --- Desktop: rail becomes a grid ----------------------------------------- */
@media (min-width: 48em) {
  .rail {
    display: grid;
    grid-template-columns: repeat(2, 1fr);
    gap: 1.5rem;
    margin-inline: 0;
    padding-inline: 0;
    overflow: visible;
  }
  .rail__item { max-width: none; }
  .worlds__hint { display: none; }
}

@media (min-width: 78em) {
  .rail { grid-template-columns: repeat(4, 1fr); gap: 1.75rem; }
}


/* ==========================================================================
   8. GAME MODAL
   White panel against the dark site on purpose — the switch to plain
   black-on-white is what makes this read as "here are the facts".
   ========================================================================== */
.gmodal {
  position: fixed;
  inset: 0;
  z-index: 150;
  display: grid;
  place-items: center;
  padding: 1rem;
  opacity: 0;
  transition: opacity 0.2s var(--ease);
}

.gmodal[hidden] { display: none; }

/* Closed = transparent to input. See the note on .menu: [hidden] comes back
   on a timer, and .gmodal__scroll would otherwise catch the scroll gesture
   in the gap. */
.gmodal:not(.is-open) { pointer-events: none; }

.gmodal.is-open { opacity: 1; pointer-events: auto; }

.gmodal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(6, 3, 16, 0.82);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}

.gmodal__panel {
  position: relative;
  width: 100%;
  max-width: 40rem;
  max-height: calc(100vh - 2rem);
  max-height: calc(100svh - 2rem);
  overflow: hidden;
  background: var(--paper);
  color: var(--paper-ink);
  transform: translateY(10px) scale(0.985);
  transition: transform 0.24s var(--ease);
}

.gmodal.is-open .gmodal__panel { transform: none; }

.gmodal__scroll {
  max-height: calc(100vh - 2rem);
  max-height: calc(100svh - 2rem);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}

/* Explicit tap target, not just click-outside. Sits on the video, so it
   needs its own contrast rather than relying on the white panel. */
.gmodal__close {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(11, 7, 16, 0.62);
  color: #FFFFFF;
  cursor: pointer;
  transition: background-color 0.16s var(--ease);
}

.gmodal__close:hover { background: rgba(11, 7, 16, 0.85); }


/* --- Trailer slot — always 16:9 landscape --------------------------------- */
.gmodal__video {
  position: relative;
  aspect-ratio: 16 / 9;
  background: #0B0714;
}

.gmodal__video iframe,
.gmodal__video video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.gmodal__video-ph {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 0.875rem;
  height: 100%;
  background:
    radial-gradient(60% 60% at 50% 42%, rgba(124, 92, 255, 0.42), transparent 72%),
    linear-gradient(158deg, var(--indigo-800), var(--indigo-950));
}

.gmodal__play {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  border: 2px solid rgba(255, 255, 255, 0.6);
  border-radius: 50%;
}

.gmodal__play::after {
  content: '';
  margin-left: 4px;
  border-style: solid;
  border-width: 9px 0 9px 15px;
  border-color: transparent transparent transparent rgba(255, 255, 255, 0.85);
}

.gmodal__video-txt {
  color: rgba(255, 255, 255, 0.72);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}


/* --- Modal body ----------------------------------------------------------- */
.gmodal__body { padding: clamp(1.375rem, 4.5vw, 2rem); }

/* Same badge as the card, restated for the white modal. Inline-flex so the
   pill hugs the text instead of spanning the panel. */
.gmodal__tag {
  display: inline-flex;
  margin-bottom: 0.875rem;
  padding: 0.4375rem 0.8125rem;
  border-radius: 999px;
  background: var(--violet-500);
  color: #fff;
  font-size: 0.6875rem;
  font-weight: 900;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.gmodal__tag--hot {
  background: linear-gradient(96deg, #FF2D77, #FF7A2F);
}

.gmodal__title {
  margin-bottom: 1.125rem;
  font-size: clamp(1.625rem, 6vw, 2.125rem);
  font-weight: 900;
  line-height: 1.05;
  letter-spacing: -0.035em;
  text-transform: uppercase;
}

.gspecs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 2rem;
  margin-bottom: 1.25rem;
  padding-bottom: 1.25rem;
  border-bottom: 1px solid rgba(11, 7, 16, 0.12);
}

.gspec { display: grid; gap: 0.1875rem; }

.gspec__k {
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.gspec__v {
  font-size: 0.9375rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.gmodal__desc {
  margin-bottom: 1.75rem;
  color: #33303B;              /* 11.6:1 on white ✓ AAA */
  font-size: 1.0625rem;
  line-height: 1.6;
  text-wrap: pretty;
}

/* "Available in Oakville." — a fact about booking, not part of the pitch,
   so it sits apart from the description and carries its own weight. */
.gmodal__where {
  margin-top: -0.75rem;        /* .gmodal__desc owns the gap; close it up */
  margin-bottom: 1.75rem;
  color: var(--paper-ink);
  font-size: 0.9375rem;
  font-weight: 800;
}

.gmodal__cta { width: 100%; }

@media (min-width: 48em) {
  .gmodal__cta { width: auto; }
}


/* ==========================================================================
   9. PROOF — reel + testimonials
   ========================================================================== */
.proof,
.reviews {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--paper);
  color: var(--paper-ink);
}

/* Dark variant, used only on the two product pages. There the quotes sit
   between a white picker table and a white FAQ, so leaving them white made
   three white sections in a row and the page stopped having a rhythm. On the
   home and city pages .reviews stays white — hence the modifier rather than
   a change to the base. */
.reviews--dark {
  background: var(--indigo-950);
  color: var(--text);
}

.reviews--dark .section-title { color: var(--text); }
.reviews--dark .section-title__accent { color: var(--accent); }

/* The hint carries --light for the white ground; restate it for this one. */
.reviews--dark .worlds__hint--light { color: var(--text-muted); }

.reviews--dark .quote {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.03);
}

.reviews--dark .quote__text { color: var(--text-soft); }
.reviews--dark .quote__name { color: var(--text); }
.reviews--dark .quote__ctx  { color: var(--text-muted); }

/* The white-ground gold goes muddy on indigo — this is the brighter one the
   rest of the dark sections use. */
.reviews--dark .quote__stars { color: #FFC53D; }

/* --- Clip carousel — full-bleed, always moving -----------------------------
   Edge to edge at every width: cards get cut off by both screen edges, which
   is the thing that says "keeps going" before anyone touches it. clips.js
   drifts it left continuously and duplicates the track so the loop is
   seamless; drag and swipe still work and pause the drift. */
.clips {
  /* The track now carries 2.75rem of its own vertical padding for the glow,
     so the gap between a rail and whatever follows it comes mostly from
     there — this only tops it up. */
  margin-bottom: clamp(0.5rem, 2vw, 1rem);
  /* Belt and braces against a sideways scrollbar: the glow spreads 8px past
     the last card, and `hidden` alone still lets a child establish scroll
     width on some engines. `clip` cannot be scrolled at all. */
  overflow: hidden;
  overflow: clip;
}

.clips__track {
  display: flex;
  gap: clamp(0.75rem, 2vw, 1.25rem);
  overflow-x: auto;
  /* Room for the neon glow. A box-shadow paints OUTSIDE the element's box,
     and both this track and .clips above it clip their overflow — so without
     the padding the bloom is sliced flat along the top and bottom edge of
     every card. The value is the shadow's blur plus its spread. */
  padding-block: 2.75rem;
  list-style: none;
  margin: 0;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  cursor: grab;
}

.clips__track::-webkit-scrollbar { display: none; }
.clips__track.is-dragging { cursor: grabbing; }

/* Card width drives how many are in view. 42vw ≈ two-and-a-half on a phone;
   the cap keeps them from ballooning on a wide monitor. */
.clips__item {
  flex: 0 0 clamp(9rem, 42vw, 14.5rem);
}

/* Nothing inside a card should hijack a drag. */
.clips__track img,
.clips__track video { user-select: none; -webkit-user-drag: none; }

.clip { margin: 0; }

/* Softer than 9:16 — a true phone crop gets very tall once three of them are
   side by side, and this is the shape the reference uses. */
/* --- Neon edge -------------------------------------------------------------
   A hairline of white with violet bloom behind it, on every photo and clip
   card on the site. It is box-shadow rather than a border so it costs no
   layout: three stacked shadows, hard ring first, then two blurs at falling
   opacity, which is what makes it read as light coming off the edge instead
   of a coloured outline.

   Both light and dark sections use it. On white the bloom barely registers
   and the ring does the work; on indigo the bloom is the effect. */
/* --- House photo grade -----------------------------------------------------
   One filter on every photo slot so the whole site's imagery reads as one
   set. These are phone photos shot in a dim arena under coloured LEDs, so
   they come in flat and slightly muddy: a little contrast puts the blacks
   back, saturation brings the arena lighting up, and the brightness lift
   stops the dark sections swallowing them.

   The first pass at this was far too timid to see. These are dim phone
   shots, so they need a real lift: saturation well up for the arena LEDs,
   contrast to put the blacks back, brightness to pull them out of the dark
   sections, and a touch of unsharp via a tiny drop-shadow-free contrast
   bump. Stops short of clipping the neon strips, which sit near the top of
   the range already. */
.shot,
.clip__ph,
.parent__shot,
.kidgame__art,
.reasons__photo,
.state__media,
.xp__shot,
.motion__media,
.plans--party .plan__shot,
.teaser__media {
  filter: saturate(1.42) contrast(1.14) brightness(1.1);
}
/* .phero__media is deliberately NOT in that list. It is a full-bleed hero,
   not a content photo in a card — punching its saturation up 42% recolours
   the picture just as surely as desaturating it did. It carries no filter at
   all; see the note on the rule itself. */

.clip__frame,
.shot {
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, 0.85),
    0 0 14px 2px rgba(124, 92, 255, 0.75),
    0 0 34px 8px rgba(124, 92, 255, 0.35);
}

.clip__frame {
  position: relative;
  aspect-ratio: 5 / 8;
  overflow: hidden;
  border-radius: 0.875rem;
  background: var(--indigo-950);
}

.clip__frame iframe,
.clip__frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
  object-fit: cover;
}

/* Poster stand-in. Set --clip-img on the .clips__item to drop a still in:
       style="--clip-img: url('../img/clips/name.jpg')" */
.clip__ph {
  position: absolute;
  inset: 0;
  background-image: var(--clip-img, none);
  background-size: cover;
  background-position: center;
}

.clip__ph::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(70% 45% at 50% 38%, rgba(124, 92, 255, 0.42), transparent 74%),
    linear-gradient(168deg, var(--indigo-700), var(--indigo-950));
}

/* Once a real poster exists the gradient would hide it, so it drops to a
   scrim that just keeps the play button legible. */
.clips__item[style*="--clip-img"] .clip__ph::after {
  background: linear-gradient(180deg, rgba(11, 7, 16, 0.15), rgba(11, 7, 16, 0.55));
}

.clip__play {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  color: #fff;
  cursor: pointer;
}

.clip__play-icon {
  display: grid;
  place-items: center;
  width: clamp(2.375rem, 9vw, 3.25rem);
  height: clamp(2.375rem, 9vw, 3.25rem);
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  border-radius: 50%;
  background: rgba(11, 7, 16, 0.28);
  backdrop-filter: blur(2px);
  transition: transform 0.18s var(--ease), background-color 0.18s var(--ease);
}

/* Triangle drawn with a border so there's no asset to load. The left inset
   optically re-centres it — a triangle's visual mass sits left of its box. */
.clip__play-icon::before {
  content: "";
  margin-left: 0.2rem;
  border-style: solid;
  border-width: 0.5rem 0 0.5rem 0.8rem;
  border-color: transparent transparent transparent #fff;
}

.clip__play:hover .clip__play-icon,
.clip__play:focus-visible .clip__play-icon {
  transform: scale(1.08);
  background: rgba(124, 92, 255, 0.55);
}

/* The rail hint was written for the dark section — restate it for white. */
.worlds__hint--light { color: var(--paper-muted); }

/* Second hint sits above the quote rail, so it needs its own top gap. */
.proof__hint2 { margin-top: clamp(2rem, 5vw, 3rem); }



/* --- Quote cards ---------------------------------------------------------- */
/* Quotes run longer than game cards and have no image to anchor them, so
   they get a wider card and a taller minimum. */
.rail--quotes .rail__item { flex-basis: 86%; max-width: 23rem; }

.quote {
  display: flex;
  flex-direction: column;
  height: 100%;
  margin: 0;
  padding: 1.5rem 1.5rem 1.625rem;
  border: 1px solid rgba(11, 7, 16, 0.14);
  background: #FBFAFC;
}

.quote__stars {
  margin-bottom: 0.875rem;
  color: #FFB300;              /* darker than the menu gold — needs to hold
                                  up on white, not on indigo */
  font-size: 0.9375rem;
  letter-spacing: 0.08em;
}

.quote__text {
  margin: 0 0 1.25rem;
  color: #33303B;              /* 11.6:1 on white ✓ AAA */
  font-size: 1.0625rem;
  line-height: 1.6;
  text-wrap: pretty;
}

/* auto pins attribution to the card bottom so it lines up across cards
   whose quotes run to different lengths. */
.quote__by {
  display: grid;
  gap: 0.1875rem;
  margin-top: auto;
}

.quote__name {
  font-size: 0.9375rem;
  font-weight: 800;
  letter-spacing: -0.01em;
}

.quote__ctx {
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

@media (min-width: 48em) {
  .rail--quotes { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 78em) {
  .rail--quotes { grid-template-columns: repeat(4, 1fr); }
}


/* ==========================================================================
   9b. MOTION — "no standing still"
   The only decorative animation on the site. Free-roam is the product claim,
   so the words move the way the thing they describe does: WALK arrives at a
   walk, RUN comes from further out and lands faster, EXPLORE scales up and
   settles last as the payoff.
   ========================================================================== */
.motion {
  padding-block: clamp(5rem, 12vw, 9rem);
  background: var(--indigo-950);
  color: var(--text);
  /* The words start off-canvas; without this they'd widen the document and
     produce a horizontal scrollbar before they animate in. */
  overflow: hidden;
}

/* Copy first, photo after — on mobile that stacks the image below the words,
   which is the order the section argues in. */
.motion__inner {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
}

.motion__title { margin-bottom: clamp(2rem, 5vw, 3rem); }

/* Photo slot. Set --motion-img on .motion__media to drop a photo in; until
   then the gradient below stands in, same pattern as .game__thumb. */
/* The box is shaped to the photo rather than the photo being cropped to the
   box, so `cover` fills it with nothing trimmed off and no empty bars. The
   current image is 1132x1280; if you swap it for one with a different shape,
   set --motion-ratio alongside --motion-img:
       style="--motion-img: url('…'); --motion-ratio: 16 / 9" */
.motion__media {
  aspect-ratio: var(--motion-ratio, 1132 / 1280);
  background-image: var(--motion-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 0.875rem;
}

.motion__media:not([style*="--motion-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}

.motion__words {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: clamp(0.25rem, 1vw, 0.5rem);
  font-weight: 900;
  line-height: 0.9;
  letter-spacing: -0.05em;
  text-transform: uppercase;
}

.motion__word {
  /* transform-origin left so EXPLORE's scale grows rightward off the shared
     left edge instead of drifting away from it. */
  transform-origin: left center;
  font-size: clamp(3rem, 15vw, 8rem);
  will-change: transform, opacity;
}

/* --- Resting (pre-scroll) state -------------------------------------------
   Only applied once JS has confirmed it can animate — .motion is given
   .is-armed by motion.js. Without that class (JS off, script failed) the
   words are simply visible, so the section never depends on the animation. */
/* Timings are unhurried on purpose. The whole sequence runs ~2.4s end to
   end: fast enough not to hold anyone up, slow enough that the difference
   between WALK's pace and RUN's is actually legible rather than three words
   appearing at once. */
.motion.is-armed .motion__word {
  opacity: 0;
  transition:
    transform 1.25s cubic-bezier(0.16, 1, 0.3, 1),
    opacity 0.7s linear;
}

/* WALK — a walk: short distance, unhurried, arrives first. */
.motion.is-armed .motion__word--walk {
  transform: translateX(-18%);
}

/* RUN — from much further out and faster, so it visibly overtakes the pace
   WALK set. Shorter duration on a longer distance = higher speed. */
.motion.is-armed .motion__word--run {
  transform: translateX(-85%);
  transition-duration: 0.85s, 0.45s;
  transition-delay: 0.5s;
}

/* EXPLORE — no travel, it swells into place. Lands last and slightly larger
   than the other two: the payoff word. */
.motion.is-armed .motion__word--explore {
  transform: scale(0.82);
  transition-duration: 1.4s, 0.8s;
  transition-delay: 1s;
}

.motion.is-armed.is-in .motion__word {
  opacity: 1;
  transform: none;
}

.motion.is-armed.is-in .motion__word--explore {
  transform: scale(1.14);
}

/* Side by side once there's room. The copy column takes the larger share —
   the words are the section, the photo is support. */
@media (min-width: 56.25em) {
  .motion__inner {
    grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
    align-items: center;
  }

  /* Taller crop when it's a column rather than a full-width block. */
  .motion__media { aspect-ratio: 3 / 4; }

  /* The words no longer have the full viewport — a single size would run
     EXPLORE straight out of its column, and .motion clips overflow, so it
     would be cut rather than merely cramped.

     Each word is also a step larger than the last: WALK → RUN → EXPLORE.
     The size ladder does the "payoff" job that the settle-scale does on
     mobile, so the scale is dropped here (below) — doing both would push
     EXPLORE past the column edge. Caps are set from the narrowest desktop
     column (~1100px viewport), where EXPLORE is widest relative to space. */
  .motion__word--walk    { font-size: clamp(2.4rem, 6vw,   5.2rem); }
  .motion__word--run     { font-size: clamp(2.9rem, 7.1vw, 6.1rem); }
  .motion__word--explore { font-size: clamp(3.4rem, 8.4vw, 7.2rem); }

  /* Size ladder replaces the scale-up — EXPLORE settles at its true size. */
  .motion.is-armed.is-in .motion__word--explore { transform: none; }
}

/* Somebody who has asked for less motion gets the composition, not the
   animation — the words are already in place and nothing moves. */
@media (prefers-reduced-motion: reduce) {
  .motion.is-armed .motion__word {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .motion.is-armed .motion__word--explore { transform: scale(1.14); }
}


/* ==========================================================================
   9c. CTA STRIP
   A white band between two dark sections. Shallower padding than a real
   section — it's a divider that happens to sell, so it shouldn't read as
   another stop on the page.
   ========================================================================== */
.strip {
  padding-block: clamp(3.25rem, 7vw, 5rem);
  background: var(--paper);
  color: var(--paper-ink);
}

/* --- Numbers --------------------------------------------------------------
   The strip needed a reason to exist beyond breaking up the dark. Real
   figures do that: they carry weight on their own, and they let the CTA
   underneath read as a conclusion rather than a stray button. */
.stats {
  display: grid;
  gap: 0;
  margin-bottom: clamp(2.25rem, 5vw, 3rem);
  list-style: none;
}

.stat {
  display: grid;
  gap: 0.375rem;
  padding-block: 1.125rem;
  /* Hairlines between rows, not around them — a boxed-in row of stats is
     what makes this kind of band look like a widget. */
  border-top: 1px solid rgba(11, 7, 16, 0.1);
}

.stat:last-child { border-bottom: 1px solid rgba(11, 7, 16, 0.1); }

.stat__num {
  color: var(--paper-ink);
  font-size: clamp(2.25rem, 8vw, 3.25rem);
  font-weight: 900;
  line-height: 0.9;
  letter-spacing: -0.045em;
  font-variant-numeric: tabular-nums;
}

/* Smaller and violet so the digits stay the thing you read first. */
.stat__plus {
  color: var(--accent-ink);
  font-size: 0.62em;
  vertical-align: 0.16em;
}

.stat__label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.stat__stars {
  color: #FFB300;              /* the on-white gold, same as .quote__stars */
  font-size: 0.875rem;
  letter-spacing: 0.04em;
}

/* --- The ask -------------------------------------------------------------- */
.strip__line {
  margin-bottom: 1.5rem;
  font-size: clamp(1.75rem, 5.5vw, 2.5rem);
  font-weight: 900;
  line-height: 1.05;
  letter-spacing: -0.035em;
  text-transform: uppercase;
  text-wrap: balance;
}

/* Deeper violet again — plain --accent is 2.90:1 on white. */
.strip__accent { color: var(--accent-ink); }

.strip__cta {
  display: inline-flex;
  width: auto;
  min-width: 15rem;
}

@media (min-width: 48em) {
  /* Stats go side by side, dividers turn vertical. */
  .stats {
    grid-template-columns: repeat(3, 1fr);
    border-top: 1px solid rgba(11, 7, 16, 0.1);
    border-bottom: 1px solid rgba(11, 7, 16, 0.1);
  }

  .stat {
    gap: 0.625rem;
    padding: 1.75rem clamp(1.25rem, 3vw, 2.5rem);
    border-top: 0;
    border-left: 1px solid rgba(11, 7, 16, 0.1);
  }

  .stat:first-child { border-left: 0; padding-left: 0; }
  .stat:last-child { border-bottom: 0; }

  /* Copy left, button right — the line and the action sit on one baseline
     instead of stacking into a centred column, which is what made the old
     version read as a placeholder. */
  .strip__act {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 2rem;
  }

  .strip__line { margin-bottom: 0; }
  .strip__br { display: none; }
}


/* ==========================================================================
   9c-2. BOOKING MODAL
   Shares the game modal's backdrop/close but its own panel — a form wants
   a narrower column and more breathing room than a video header does.
   ========================================================================== */
.bmodal {
  position: fixed;
  inset: 0;
  /* Was 60, i.e. under .hdr's 100 — the header floated over an open booking
     modal and stayed fully clickable through it. See the stack listing on
     .menu for the full order. */
  z-index: 110;
  display: grid;
  place-items: center;
  padding: 1rem;
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}

/* MUST come with the display:grid above. A class selector beats the browser's
   own [hidden]{display:none}, so without this the closed modal stays a
   full-screen, invisible, click-eating layer over the whole page. */
.bmodal[hidden] { display: none; }

/* And [hidden] only returns on a timer after close, so belt and braces: a
   closed modal never hit-tests. Without this, .bmodal__scroll eats the
   scroll gesture during the gap. See the note on .menu. */
.bmodal:not(.is-open) { pointer-events: none; }

.bmodal.is-open { opacity: 1; pointer-events: auto; }

.bmodal .gmodal__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(8, 5, 12, 0.78);
  backdrop-filter: blur(4px);
}

.bmodal__panel {
  position: relative;
  width: min(34rem, 100%);
  /* svh, not vh. vh resolves to the toolbars-HIDDEN height, so on an iPhone
     90vh computes against ~745px while only ~664px is actually visible — the
     panel ends up taller than the screen and the submit button sits under the
     Safari bar with nothing to scroll. Same bug class as the hero. */
  max-height: min(90vh, 46rem);
  max-height: min(90svh, 46rem);
  overflow: hidden;
  background: var(--paper);
  color: var(--paper-ink);
  transform: translateY(12px);
  transition: transform 0.25s var(--ease);
}

.bmodal.is-open .bmodal__panel { transform: none; }

.bmodal__scroll {
  max-height: inherit;
  overflow-y: auto;
  padding: clamp(1.5rem, 4vw, 2.5rem);
}

.bmodal .gmodal__close {
  position: absolute;
  top: 0.875rem;
  right: 0.875rem;
  z-index: 2;
}

.bmodal__step {
  margin-bottom: 0.5rem;
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.bmodal__title {
  margin-bottom: 1.5rem;
  font-size: clamp(1.5rem, 5vw, 1.875rem);
  font-weight: 900;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}

/* --- Step 1: the two arenas ------------------------------------------------ */
.bloc__grid { display: grid; gap: 0.875rem; }

.bloc {
  display: grid;
  gap: 0.25rem;
  padding: 1.5rem 1.25rem;
  border: 2px solid rgba(11, 7, 16, 0.14);
  background: #fff;
  cursor: pointer;
  text-align: left;
  transition: border-color 0.15s var(--ease), transform 0.15s var(--ease);
}

.bloc:hover {
  border-color: var(--violet-500);
  transform: translateY(-2px);
}

.bloc__city {
  font-size: 1.375rem;
  font-weight: 900;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.bloc__addr { color: var(--paper-muted); font-size: 0.875rem; }

/* Side by side on desktop, stacked on mobile — per the brief. */
@media (min-width: 34em) {
  .bloc__grid { grid-template-columns: 1fr 1fr; }
}

/* --- Step 2: chips, dates, times ------------------------------------------- */
.bfield__label {
  display: block;
  margin: 1.25rem 0 0.5rem;
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.bchips { display: flex; gap: 0.5rem; }
.bchips--wrap { flex-wrap: wrap; }

.bchip {
  padding: 0.5625rem 0.9375rem;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  background: #fff;
  cursor: pointer;
  font-size: 0.9375rem;
  font-weight: 700;
  transition: border-color 0.12s var(--ease), background-color 0.12s var(--ease);
}

.bchip:hover { border-color: var(--violet-500); }

.bchip.is-active {
  border-color: var(--violet-500);
  background: var(--violet-500);
  color: #fff;
}

/* Date strip scrolls sideways — 14 chips never fit a phone. */
/* --- Date strip ------------------------------------------------------------
   Two weeks of dates in a horizontal scroller. The shell is added by book.js
   at runtime and carries the paging arrows either side; the strip itself is
   the scrolling element.
   -------------------------------------------------------------------------- */
.bdates__shell {
  position: relative;
}

.bdates {
  display: flex;
  gap: 0.5rem;
  overflow-x: auto;
  padding-bottom: 0.375rem;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;     /* don't trigger back-swipe on iOS */
}

/* A visible thumb, not a hidden one: on a phone this strip is the only part
   of the form that scrolls sideways, and with no thumb there is nothing to
   say so. Kept slim so it reads as an affordance rather than furniture. */
.bdates {
  scrollbar-width: thin;
  scrollbar-color: rgba(11, 7, 16, 0.28) transparent;
}

.bdates::-webkit-scrollbar { height: 6px; }
.bdates::-webkit-scrollbar-track { background: transparent; }

.bdates::-webkit-scrollbar-thumb {
  background: rgba(11, 7, 16, 0.28);
  border-radius: 99px;
}

.bdates::-webkit-scrollbar-thumb:hover { background: rgba(11, 7, 16, 0.42); }

.bdate { scroll-snap-align: start; }

/* --- Paging arrows ---------------------------------------------------------
   Only shown once book.js confirms the strip actually overflows (.is-scrollable),
   so a short strip that already fits never grows dead controls. Hidden from
   touch pointers, where swiping is the natural gesture and the arrows would
   just cover two date cards. */
.bdates__arrow {
  position: absolute;
  top: 50%;
  z-index: 2;
  display: none;
  place-items: center;
  width: 32px;
  height: 32px;
  margin-top: -0.1875rem;              /* offset the strip's padding-bottom */
  transform: translateY(-50%);
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  border-radius: 99px;
  background: #fff;
  box-shadow: 0 2px 10px rgba(11, 7, 16, 0.14);
  color: var(--paper-ink);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  transition: border-color 0.12s var(--ease), opacity 0.12s var(--ease);
}

.bdates__shell.is-scrollable .bdates__arrow { display: grid; }

.bdates__arrow--prev { left: -0.5rem; }
.bdates__arrow--next { right: -0.5rem; }

.bdates__arrow:hover { border-color: var(--violet-500); }

.bdates__arrow[disabled] {
  opacity: 0;
  pointer-events: none;
}

@media (hover: none) {
  .bdates__shell.is-scrollable .bdates__arrow { display: none; }
}

.bdate {
  display: grid;
  flex: none;
  justify-items: center;
  gap: 0.125rem;
  min-width: 4rem;
  padding: 0.625rem 0.75rem;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  background: #fff;
  cursor: pointer;
  transition: border-color 0.12s var(--ease), background-color 0.12s var(--ease);
}

.bdate:hover { border-color: var(--violet-500); }

.bdate.is-active {
  border-color: var(--violet-500);
  background: var(--violet-500);
  color: #fff;
}

.bdate__dow { font-size: 0.6875rem; font-weight: 700; text-transform: uppercase; opacity: 0.7; }
.bdate__num { font-size: 1.25rem; font-weight: 900; line-height: 1; }
.bdate__mon { font-size: 0.6875rem; font-weight: 600; opacity: 0.7; }

.bmodal__note {
  margin-top: 0.875rem;
  color: var(--paper-muted);
  font-size: 0.8125rem;
  line-height: 1.5;
}

/* Reassurance under the experience choice — nobody should stall here
   worrying they're locking in a specific game. */
.bmodal__note--soft {
  margin-top: 1.125rem;
  text-align: center;
}

/* --- Step 2b: live availability states ------------------------------------
   book.js writes data-state on every time and date button and adds the
   disabled attribute to whatever is gone. Everything below styles those
   states. The rule that matters commercially is "taken": a sold-out slot
   stays on screen because an empty grid reads as a dead business, while a
   grid with three slots crossed out reads as a busy one. So nothing here
   removes a chip, it only kills it visually.
   -------------------------------------------------------------------------- */

/* The time grid is addressed by id, not by a new class: #b-times is the one
   hook every page already ships (index, oakville, brampton, the sub-pages),
   so the layout lands everywhere without touching markup. Keep any future
   grid tweaks on this selector rather than inventing .bchips--grid. */
#b-times {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));
  gap: 0.5rem;
}

/* Two lines in a fixed-width cell, so a slot with a "2 left" tag and one
   without still occupy the same box and the grid stays square. */
.bchip--slot {
  display: grid;
  justify-items: center;
  gap: 0.125rem;
  min-width: 5.5rem;
  text-align: center;
}

.bchip__time { line-height: 1.15; }

.bchip__meta {
  font-family: var(--font-label);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  line-height: 1;
  text-transform: uppercase;
}

/* Scarcity, not a problem. The chip keeps its white fill, its border and its
   hover — only the tag under the time changes colour, so the slot still
   invites the click it is trying to hurry along. */
.bchip--slot[data-state="low"] .bchip__meta { color: var(--accent-ink); }

/* Gone. Visible, readable, obviously not for sale: flat grey fill, dashed
   border so it reads as "not a button" at a glance, and the time itself
   struck through. The strike is carried at 0.6 opacity over paper-ink rather
   than by a lighter colour, which still lands at 5.3:1 on the grey. */
.bchip--slot[data-state="taken"],
.bchip--slot[data-state="taken"]:hover {
  border-color: rgba(11, 7, 16, 0.18);
  border-style: dashed;
  background: #F2F1F5;
  color: var(--paper-muted);
  cursor: not-allowed;
}

.bchip--slot[data-state="taken"] .bchip__time {
  color: var(--paper-ink);
  opacity: 0.6;
  text-decoration: line-through;
}

/* Selection outranks scarcity. Same specificity as the low rule above, so
   this has to stay after it. White at 0.85 on the violet fill is 5.3:1 —
   the meta line is 10px, it needs the full body-text ratio. */
.bchip--slot.is-active .bchip__meta { color: rgba(255, 255, 255, 0.85); }

/* Date chips carry the same three states. .bdate__flag is deliberately sized
   off .bdate__mon: the month and the flag are alternatives in the third slot
   of the stack, and a mismatch there makes the strip jump row height as you
   scroll across sold-out days. */
.bdate__flag {
  font-size: 0.6875rem;
  font-weight: 700;
  line-height: 1.2;
}

.bdate__flag--low { color: var(--accent-ink); }

.bdate[data-state="full"],
.bdate[data-state="full"]:hover {
  border-color: rgba(11, 7, 16, 0.18);
  border-style: dashed;
  background: #F2F1F5;
  color: var(--paper-muted);
  cursor: not-allowed;
}

.bdate[data-state="full"] .bdate__num {
  color: var(--paper-ink);
  opacity: 0.6;
  text-decoration: line-through;
}

/* On the violet fill the lavender accent collapses to 3:1, which is under the
   floor for text this small. Near-white with a lavender cast keeps the "this
   is the accent" read at 5.5:1. */
.bdate.is-active .bdate__flag--low { color: #EFE6FF; }

/* --- Full month calendar for deposit forms ---------------------------------
   The date container ships as .bdates (display:flex; overflow-x:auto) for the
   14-day strip. contact.js swaps that class for .bcal when it renders a month
   grid: a grid nested in a horizontally-scrolling flex row collapses to its
   content width and gets clipped at both edges, which is what broke the first
   pass at this. .bcal is a plain block — the grid inside owns the layout.
   -------------------------------------------------------------------------- */
.bcal {
  display: block;
  overflow: visible;
  /* Undo every strip behaviour — the class is swapped on the SAME element, so
     the scroller rules above would otherwise still apply to the month grid. */
  scroll-snap-type: none;
  padding-bottom: 0;
}

.calendar {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.calendar__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.calendar__arrow {
  display: grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  background: #fff;
  cursor: pointer;
  font-size: 1.125rem;
  padding: 0;
  font-family: inherit;
  transition: border-color 0.12s var(--ease), background-color 0.12s var(--ease);
}

.calendar__arrow:hover {
  border-color: var(--violet-500);
  background: rgba(171, 98, 255, 0.05);
}

.calendar__title {
  flex: 1;
  text-align: center;
  font-size: 0.875rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  margin: 0;
  text-transform: uppercase;
}

.calendar__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: 0.375rem;
  padding: 0.5rem 0;
}

.calendar__day-header {
  text-align: center;
  font-size: 0.6875rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  opacity: 0.65;
  padding: 0.5rem 0;
  line-height: 1;
}

/* min-height, not aspect-ratio alone: in a 7-column grid inside a ~320px modal
   each track is ~40px, and an aspect-ratio box would follow it down to a 40px
   target with the number crammed inside. 44px is the accessible tap floor and
   the cell grows past it on wider panels. */
.calendar__cell {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  width: 100%;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  background: #fff;
  color: inherit;
  font-family: inherit;
  font-size: 0.9375rem;
  font-weight: 700;
  /* colour MUST be in this list. .is-active sets background to violet and the
     number to white; with only background-color transitioning, the number
     snapped to white instantly while the cell was still pale, giving ~120ms of
     white-on-white where the date vanished before the fill caught up. That
     flash is what read as a broken click. */
  transition: border-color 0.12s var(--ease),
              background-color 0.12s var(--ease),
              color 0.12s var(--ease);
  padding: 0;
}

.calendar__cell:not(.calendar__cell--other):not(.calendar__cell--off) {
  cursor: pointer;
}

/* :not(.is-active) is load-bearing. Each :not() adds a class's worth of
   specificity, so this selector scores (0,4,0) against .calendar__cell.is-active
   at (0,2,0) — hovering the SELECTED day therefore repainted its background
   back to the pale tint while .is-active's white text stayed, and the chosen
   date turned white-on-white under the cursor. Selection outranks hover. */
.calendar__cell:not(.calendar__cell--other):not(.calendar__cell--off):not(.is-active):hover {
  border-color: var(--violet-500);
  background: rgba(171, 98, 255, 0.08);
}

.calendar__cell.is-active,
.calendar__cell.is-active:hover {
  border-color: var(--violet-500);
  background: var(--violet-500);
  color: #fff;
}

.calendar__cell--other {
  cursor: default;
  background: transparent;
  border-color: transparent;
}

/* Days the arena is closed, and days already past, are not offered at all —
   they render as an empty cell holding the weekday column in place. No border
   and no fill, so the month reads as "these are the days you can book" rather
   than a grid of mostly-struck-out squares. */
.calendar__cell--off {
  background: transparent;
  border-color: transparent;
  cursor: default;
  pointer-events: none;
}

.calendar__arrow[disabled] {
  opacity: 0.35;
  cursor: not-allowed;
}

.calendar__arrow[disabled]:hover {
  border-color: rgba(11, 7, 16, 0.16);
  background: #fff;
}

.calendar__cell-day {
  display: block;
  line-height: 1;
}

@media (max-width: 47.99em) {
  /* Height holds at the 44px tap floor — only the gap and type tighten, so the
     grid fits a narrow phone without the targets becoming unhittable. */
  .calendar__grid { gap: 0.25rem; }
  .calendar__cell { font-size: 0.875rem; }
  .calendar__day-header { font-size: 0.625rem; }
  .calendar__title { font-size: 0.8125rem; }
}

/* --- Legend ----------------------------------------------------------------
   Only unhidden when live Wix data is actually driving the grid. With the
   static hours fallback every slot is "open" and a key explaining two states
   when only one is on screen is just noise.
   -------------------------------------------------------------------------- */
.blegend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.875rem;
  margin-top: 0.75rem;
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.blegend__item {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}

.blegend__item::before {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border: 1.5px solid;
  flex: none;
}

/* An open slot is a white chip, so the swatch has to be defined by its edge.
   It borrows the hover/selected violet rather than the resting hairline —
   at 8px a 0.16 alpha border is invisible. */
.blegend__item--open::before {
  border-color: var(--violet-500);
  background: #fff;
}

.blegend__item--taken::before {
  border-color: rgba(11, 7, 16, 0.22);
  border-style: dashed;
  background: #F2F1F5;
}

/* --- Loading and fallback notes -------------------------------------------- */
/* A square pulsing dot, not a spinner: the whole modal is hard-cornered and a
   circle would be the only round thing in it. */
.bmodal__note--load {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.bmodal__note--load::before {
  content: "";
  width: 0.375rem;
  height: 0.375rem;
  background: var(--violet-500);
  flex: none;
  animation: bslot-pulse 1.2s ease-in-out infinite;
}

@keyframes bslot-pulse {
  0%, 100% { opacity: 0.25; }
  50%      { opacity: 1; }
}

/* This fires when the Wix call failed and the grid fell back to plain opening
   hours. Nothing is broken for the customer — they can still book, we just
   confirm the slot by phone — so it is an amber advisory, not a red error.
   Ink is a deep warm brown on the cream, 8:1, which stays comfortable. */
.bmodal__note--warn {
  margin-top: 0.75rem;
  padding: 0.625rem 0.75rem;
  border-left: 3px solid #E0A200;
  background: #FDF6E3;
  color: #5A4A1F;
}

/* A decorative loop with no end state to land on, so it is switched off
   outright rather than shortened. The dot keeps its resting appearance. */
@media (prefers-reduced-motion: reduce) {
  .bmodal__note--load::before { animation: none; opacity: 1; }
}

.bmodal__nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.75rem;
}

.bback {
  border: 0;
  background: none;
  color: var(--paper-muted);
  cursor: pointer;
  font-size: 0.9375rem;
  font-weight: 700;
}

.bback:hover { color: var(--paper-ink); }

.bnext[disabled] { opacity: 0.45; cursor: not-allowed; }

/* --- Step 3: contact ------------------------------------------------------- */
.bsummary {
  margin-bottom: 0.5rem;
  padding: 0.875rem 1rem;
  background: rgba(91, 63, 217, 0.08);
  border-left: 3px solid var(--violet-500);
  font-size: 0.9375rem;
  font-weight: 700;
}

.bfield { display: block; }

.bfield__input {
  width: 100%;
  padding: 0.8125rem 1rem;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  background: #fff;
  font: inherit;
  font-size: 1rem;
}

.bfield__input:focus {
  outline: none;
  border-color: var(--violet-500);
}

/* --- Done ------------------------------------------------------------------ */
.bdone { text-align: center; padding-block: 1.5rem; }
.bdone__big { font-size: 3rem; margin-bottom: 0.75rem; }
.bdone__title {
  margin-bottom: 0.625rem;
  font-size: 1.5rem;
  font-weight: 900;
  text-transform: uppercase;
}
.bdone__text { color: var(--paper-muted); line-height: 1.6; }

/* --- Done: state banner + booking card -------------------------------------
   The four .bdone* rules above are UNTOUCHED. gift-cards.html carries the same
   four classes on #cmodal-done and #gb-done, so restyling any of them would
   silently redress two unrelated screens. Everything below hangs off a new
   .bconf__* class and can only reach the booking popup's done step.

   Squares only. .btn is `border-radius: 0 /* reference is square */` and
   .bmodal__note--load's dot is deliberately square because "the whole modal is
   hard-cornered and a circle would be the only round thing in it". A pill
   badge or a punched ticket notch here would break that on the one screen the
   customer stares at longest, so the ticket read is carried by a dashed
   perforation instead — dashed is already this popup's language for a line
   that is not business as usual (.bchip--slot[data-state="taken"]).
   -------------------------------------------------------------------------- */

/* Focused programmatically on arrival so a screen reader starts at the
   headline and a keyboard user is not dumped on <body>. Never tab-reachable,
   so the global :focus-visible ring would only be noise here. */
.bconf:focus { outline: none; }

/* Companion class on .bdone__big. Declares animation ONLY — no size, no
   colour, no margin — so it cannot fight the base rule on specificity. */
.bconf__icon { animation: bconf-pop 0.4s var(--ease) both; }

@keyframes bconf-pop {
  0%   { opacity: 0; transform: scale(0.86) translateY(6px); }
  100% { opacity: 1; transform: none; }
}

/* A one-shot flourish with a resting state to land on, so it is shortened to
   nothing rather than switched off — `both` keeps the end frame applied. */
@media (prefers-reduced-motion: reduce) {
  .bconf__icon { animation-duration: 0.01ms; }
}

/* --- Status banner ---------------------------------------------------------
   Carries the confirmed-vs-request split. data-state is written by book.js,
   the same pattern as .bchip--slot[data-state]. Colour never carries it
   alone: the kicker line says which state this is in words, and the card
   below echoes the same colour on its top edge. */
.bconf__banner {
  margin: 1.25rem 0 1rem;
  padding: 0.875rem 1rem;
  border-left: 4px solid;
  text-align: left;
}

/* Mint is the brand's one "go" colour and already the CTA fill. #1A4A3A is
   the dark green .btn__sub already uses on solid mint; on this tint it is
   9.4:1 ✓ AAA. */
.bconf__banner[data-state="confirmed"] {
  border-left-color: var(--mint);
  background: #E8FBF4;
  color: #1A4A3A;
}

/* The exact triple from .bmodal__note--warn (8:1). Same meaning as there:
   nothing is broken, a human just has to finish the job. */
.bconf__banner[data-state="pending"] {
  border-left-color: #E0A200;
  background: #FDF6E3;
  color: #5A4A1F;
}

.bconf__banner-kicker {
  margin-bottom: 0.1875rem;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.bconf__banner-text {
  font-size: 0.9375rem;
  font-weight: 600;
  line-height: 1.5;
}

/* --- Booking card ----------------------------------------------------------
   The top edge repeats the banner's state colour, which welds the two into
   one block and gives the state a second, non-textual carrier. If data-state
   were ever missing the border falls back to the neutral grey below rather
   than to a wrong colour. */
.bconf__card {
  margin-top: 1.25rem;
  border: 1.5px solid rgba(11, 7, 16, 0.14);
  border-top-width: 3px;
  background: #fff;
  text-align: left;
}

.bconf__card[data-state="confirmed"] { border-top-color: var(--mint); }
.bconf__card[data-state="pending"]   { border-top-color: #E0A200; }

.bconf__kicker {
  padding: 0.75rem 1.125rem 0.6875rem;
  border-bottom: 1px solid rgba(11, 7, 16, 0.10);
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* --- Manifest rows ---------------------------------------------------------
   Same key/value convention as .gspec__k / .gspec__v. The label is fixed
   width and the value takes the rest with min-width: 0, so a long value wraps
   inside its own right-aligned column instead of dropping to a stranded line
   of its own on a narrow phone. Separators are SOLID — dashed is reserved for
   the perforation below, or the two read as the same rule at two weights. */
.bconf__rows { padding: 0 1.125rem; }

.bconf__row {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding-block: 0.625rem;
  border-bottom: 1px solid rgba(11, 7, 16, 0.08);
}

.bconf__row:last-child { border-bottom: 0; }

.bconf__k {
  flex: none;
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.bconf__v {
  flex: 1;
  min-width: 0;
  color: var(--paper-ink);
  font-size: 0.9375rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  text-align: right;
}

/* --- Price stub ------------------------------------------------------------
   Torn off the manifest by a dashed rule, and tinted with the same violet
   wash as .bsummary one step earlier so it reads as kin to the box the
   customer saw before they submitted. Contrast on this tint (#F2F0FC over
   white): #6D28D9 6.3:1, #1A4A3A 8.9:1, --violet-500 5.9:1, --paper-muted
   5.0:1 — all pass at their sizes. */
.bconf__price {
  padding: 1.125rem 1.125rem 1.25rem;
  border-top: 2px dashed rgba(11, 7, 16, 0.20);
  background: rgba(91, 63, 217, 0.08);
}

/* Names the pricing MODEL, not the amount. This is the one element that stops
   a $179.94 per-player estimate reading as a $649 flat group total: the tag
   changes word AND colour with data-model, and the border follows the text
   colour for free. */
.bconf__price-tag {
  display: inline-block;
  margin-bottom: 0.625rem;
  padding: 0.1875rem 0.4375rem;
  border: 1px solid currentColor;
  color: #6D28D9;
  font-family: var(--font-label);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.bconf__price[data-model="flat"] .bconf__price-tag { color: #1A4A3A; }

/* margin-left: auto rather than justify-content, so when the pair is too wide
   for a narrow phone the amount wraps to its own line and STAYS right-aligned
   instead of jumping to the left edge. */
.bconf__price-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.375rem 1rem;
}

.bconf__price-label {
  flex: none;
  font-size: 0.9375rem;
  font-weight: 700;
}

.bconf__price-amt {
  margin-left: auto;
  color: var(--violet-500);
  font-size: 1.75rem;
  font-weight: 900;
  letter-spacing: -0.02em;
}

.bconf__price-note {
  margin-top: 0.4375rem;
  color: var(--paper-muted);
  font-size: 0.8125rem;
  line-height: 1.5;
}


/* ==========================================================================
   9c-3. SUB-PAGE HERO + PRICING PLANS + CONTACT MODAL
   Shared by birthday-parties, corporate-events and gift-cards.
   ========================================================================== */
/* Same photo → duotone → scrim stack as the landing hero, so sub-pages don't
   read as a flat colour block bolted under the header. Set --hero-photo on
   an individual .phero to give a page its own image:
       <section class="phero" style="--hero-photo: url('...')"> */
.phero {
  position: relative;
  isolation: isolate;
  padding: clamp(8.5rem, 20vw, 13rem) 0 clamp(4.5rem, 10vw, 7rem);
  background: var(--indigo-950);
  color: var(--text);
  overflow: hidden;
}

.phero__media {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: var(--hero-photo);
  background-size: cover;
  background-position: var(--hero-photo-position);
  background-repeat: no-repeat;
  /* No filter. This used to be saturate(0.28) — 72% of the colour thrown
     away — because a purple mix-blend-mode grade sat on top and needed a
     near-greyscale base to tint cleanly. That grade is gone, so the
     desaturation was left recolouring the photo on its own: it is what
     turned the arena floor black and white. The photo now renders exactly
     as shot. Legibility is the scrim's job, not this element's. */
}

/* Darken the photo evenly, and nothing else.
   Neutral black at a flat opacity: it lowers the brightness of every pixel by
   the same amount and leaves every hue exactly where it was. No gradient, no
   blend mode, no colour. A gradient is what failed here — it went transparent
   in the upper half, so the subhead landed on a white tablecloth and vanished,
   while a purple-tinted version of the same idea recoloured the picture. One
   number, applied uniformly, does the whole job.

   0.42 is the level where the white subtext clears WCAG AA against the
   brightest thing in either hero photo (the tablecloth on birthday, the lit
   wall on team) while the arena still reads as a lit, colourful room. */
.phero__grade {
  position: absolute;
  inset: 0;
  z-index: 2;
  background: rgba(0, 0, 0, 0.42);
}

/* Kept as an element with no styling of its own — the darkening above is
   uniform, so there is nothing left for a second layer to do. Retained so the
   markup and the z-index stack are unchanged. */
.phero__scrim {
  position: absolute;
  inset: 0;
  z-index: 3;
}

/* Kept alongside the flat darken, not instead of it. The darken sets the
   floor; this handles the local case the flat value can't — a specular
   highlight, a balloon, the lit edge of a headset — where one small patch is
   far brighter than the frame average. Softer than it was, since it is no
   longer carrying legibility on its own. */
.phero__inner :is(h1, .kicker, .phero__sub) {
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}

.phero__inner {
  position: relative;
  z-index: 4;
}

/* Near-white, not --text-soft. That token is #CFC7E0, tuned for the site's
   flat dark sections; over a photograph it is the first thing to disappear —
   it was sitting on a white tablecloth in the birthday hero at roughly 1.2:1.
   The muted look was never the point, legibility is, and this is the one
   place on the site where the background underneath is a photograph. */
.phero__sub {
  max-width: 38rem;
  margin-top: 1.5rem;
  color: rgba(255, 255, 255, 0.94);
  font-size: clamp(1rem, 2.4vw, 1.1875rem);
  line-height: 1.6;
  text-wrap: pretty;
}

.phero__cta { margin-top: 2rem; }

/* --- Plans ----------------------------------------------------------------- */
.plans {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--paper);
  color: var(--paper-ink);
}

.plans__grid {
  display: grid;
  gap: 1.25rem;
}

.plan {
  display: flex;
  flex-direction: column;
  padding: 2rem 1.75rem;
  border: 1px solid rgba(11, 7, 16, 0.14);
  background: #FBFAFC;
}

/* The middle tier is the one most people should pick — say so. */
.plan--featured {
  position: relative;
  border: 2px solid var(--violet-500);
  background: #fff;
}

.plan__flag {
  position: absolute;
  top: -0.8125rem;
  left: 1.5rem;
  padding: 0.25rem 0.75rem;
  border-radius: 999px;
  background: var(--violet-500);
  color: #fff;
  font-size: 0.6875rem;
  font-weight: 900;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.plan__name {
  margin-bottom: 0.375rem;
  font-size: 1.25rem;
  font-weight: 900;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.plan__price {
  margin-bottom: 0.25rem;
  font-size: clamp(2rem, 6vw, 2.5rem);
  font-weight: 900;
  letter-spacing: -0.04em;
}

.plan__per {
  color: var(--paper-muted);
  font-size: 0.8125rem;
  margin-bottom: 1.25rem;
}

.plan__list {
  display: grid;
  gap: 0.625rem;
  margin-bottom: 1.75rem;
  list-style: none;
}

.plan__item {
  position: relative;
  padding-left: 1.5rem;
  color: #33303B;
  font-size: 0.9375rem;
  line-height: 1.5;
}

.plan__item::before {
  content: "✓";
  position: absolute;
  left: 0;
  color: var(--violet-500);
  font-weight: 900;
}

.plan .btn { margin-top: auto; width: 100%; }

.plans__intro {
  max-width: 42rem;
  margin: -1.5rem 0 2.5rem;
  color: #33303B;
  font-size: clamp(1rem, 2.3vw, 1.0625rem);
  line-height: 1.65;
  text-wrap: pretty;
}

.plans__notes { margin-top: 2rem; }

/* No tint, no gradient — a hairline rule and the type doing the work. A
   coloured wash behind a single line of text is the thing that made this
   read as a template banner rather than part of the page. */
.plans__deal {
  display: inline-block;
  margin-bottom: 1rem;
  padding: 0.75rem 1.125rem;
  border: 1px solid rgba(11, 7, 16, 0.16);
  color: var(--paper-ink);
  font-size: 0.9375rem;
}

.plans__deal strong { font-weight: 900; }

/* --- The weekday saving, above the plans ----------------------------------
   It used to be one line of fine print under the cards, which is after the
   decision has already been made. Booking Monday-Thursday is worth 10% and
   that is a reason to choose a date, so it goes where the dates are being
   considered.

   Loud enough to notice, quiet enough to stay professional: a violet-tinted
   bar with a solid badge, no shouting caps in the sentence itself. The badge
   carries the number so the eye lands on "Save 10%" before reading the rest. */
.plans__deal--banner {
  display: flex;
  align-items: center;
  gap: 0.875rem;
  width: 100%;
  margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
  padding: 0.9375rem 1.125rem;
  border: 1.5px solid rgba(124, 92, 255, 0.35);
  border-radius: 0.625rem;
  background: linear-gradient(100deg, rgba(124, 92, 255, 0.14), rgba(124, 92, 255, 0.05));
}

.plans__deal-badge {
  flex: none;
  padding: 0.375rem 0.6875rem;
  border-radius: 999px;
  background: var(--violet-500);
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 900;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  white-space: nowrap;
}

.plans__deal-text {
  color: var(--paper-ink);
  font-size: clamp(0.9375rem, 2.4vw, 1rem);
  line-height: 1.45;
}

/* On a dark plans section the ink colour has to flip or the text disappears. */
.plans--dark .plans__deal--banner { border-color: rgba(181, 123, 255, 0.4); }
.plans--dark .plans__deal-text    { color: var(--text); }


/* ==========================================================================
   CLICK TO CALL — floating, on the two package pages
   Two arenas means two numbers, so a single tel: link would send half the
   callers to the wrong building. This opens to a labelled choice instead.

   Built on <details>/<summary> so it needs no JavaScript: the open/closed
   state is the element's own, which also means it survives with scripting off
   and needs no ARIA wiring of its own.
   ========================================================================== */
.callfab {
  position: fixed;
  right: max(1rem, env(safe-area-inset-right));
  bottom: calc(1rem + env(safe-area-inset-bottom));
  /* Below the modals (110-150) and the menu (120) so it can never sit on top
     of an open overlay, but above ordinary page content. */
  z-index: 90;
}

.callfab__btn {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.8125rem 1.125rem;
  border-radius: 999px;
  background: #0B0714;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.14);
  color: #fff;
  cursor: pointer;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.08em;
  list-style: none;                /* kills the default disclosure triangle */
  text-transform: uppercase;
  -webkit-tap-highlight-color: transparent;
}

/* Safari draws the marker via a pseudo-element that list-style alone misses. */
.callfab__btn::-webkit-details-marker { display: none; }

.callfab__btn svg { width: 1.125rem; height: 1.125rem; }

.callfab__btn:hover { background: #17102A; }

.callfab[open] .callfab__btn {
  background: var(--violet-500);
  box-shadow: 0 6px 20px rgba(124, 92, 255, 0.45);
}

.callfab__menu {
  position: absolute;
  right: 0;
  bottom: calc(100% + 0.625rem);
  display: grid;
  gap: 1px;
  min-width: 13.5rem;
  overflow: hidden;
  border-radius: 0.75rem;
  background: rgba(255, 255, 255, 0.14);   /* shows through as hairlines */
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
}

.callfab__item {
  display: grid;
  gap: 0.125rem;
  padding: 0.75rem 1rem;
  background: #0B0714;
  color: #fff;
  text-decoration: none;
}

.callfab__item:hover { background: #17102A; }

.callfab__city {
  color: var(--accent);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.callfab__num { font-size: 1rem; font-weight: 800; }

/* The label is the first thing to go on a narrow screen — the handset alone
   is unambiguous, and a smaller target leaves more of the page usable. */
@media (max-width: 26.5em) {
  .callfab__btn { padding: 0.8125rem; }
  .callfab__label { display: none; }
}

.plans__fine {
  max-width: 44rem;
  color: var(--paper-muted);
  font-size: 0.875rem;
  line-height: 1.6;
}

.plans__fine a, .plans__deal a { color: var(--accent-ink); font-weight: 700; }

@media (min-width: 56.25em) {
  .plans__grid { grid-template-columns: repeat(3, 1fr); align-items: stretch; }

  /* Four birthday tiers: two-up here, four-up only once there's real room —
     four columns at 900px would squeeze each card past readable. */
  .plans__grid--4 { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 78em) {
  .plans__grid--4 { grid-template-columns: repeat(4, 1fr); gap: 1rem; }
  .plans__grid--4 .plan { padding: 1.75rem 1.25rem; }
  .plans--party .plans__grid--4 .plan { padding: 0; }
}

/* --- SEO prose under the plans --------------------------------------------- */
.seo {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--indigo-950);
  color: var(--text);
}

/* White variant. The birthday page ends dark-on-dark-on-black otherwise, and
   this block is a long read — it wants the same white ground the .faq
   sections use. Scoped as a modifier so the team page keeps its dark one. */
.seo--light {
  background: var(--paper);
  color: var(--paper-ink);
}

.seo--light .section-title--dark { color: var(--paper-ink); }
.seo--light .section-title__accent { color: var(--accent-ink); }
.seo--light .faq__list { border-top-color: rgba(11, 7, 16, 0.14); }
.seo--light .faq__item { border-bottom-color: rgba(11, 7, 16, 0.14); }
.seo--light .faq__q::after { color: var(--violet-500); }
.seo--light .faq__a { color: #33303B; }
.seo--light .faq__a a { color: var(--accent-ink); }
.seo--light .seo__intro { color: var(--paper-muted); }

.seo .faq__list { max-width: 50rem; }

/* Long-form answers: multiple paragraphs per question. */
.faq__a + .faq__a { margin-top: 0; padding-top: 0; }

/* --- Contact modal --------------------------------------------------------- */
.cmodal {
  position: fixed;
  inset: 0;
  /* Was 60. Peer of .bmodal, and like it now sits above .hdr's 100. */
  z-index: 110;
  display: grid;
  place-items: center;
  padding: 1rem;
  opacity: 0;
  transition: opacity 0.25s var(--ease);
}

/* Same trap as .bmodal — see the note there. Same deferred-teardown gap too,
   and .cmodal__panel is the overflow-y:auto element that would swallow the
   scroll during it. */
.cmodal[hidden] { display: none; }
.cmodal:not(.is-open) { pointer-events: none; }

.cmodal.is-open { opacity: 1; pointer-events: auto; }

.cmodal__panel {
  position: relative;
  width: min(30rem, 100%);
  /* svh — see the note on .bmodal__panel. This one holds the deposit checkout,
     so the pay button being reachable matters even more. */
  max-height: min(90vh, 42rem);
  max-height: min(90svh, 42rem);
  overflow-y: auto;
  background: var(--paper);
  color: var(--paper-ink);
  padding: clamp(1.5rem, 4vw, 2.25rem);
  transform: translateY(12px);
  transition: transform 0.25s var(--ease);
}

.cmodal.is-open .cmodal__panel { transform: none; }

.cmodal textarea.bfield__input {
  min-height: 7rem;
  resize: vertical;
}

/* Answers the question everyone has before typing anything. */
.cmodal__reply {
  margin: -0.75rem 0 1.25rem;
  color: var(--paper-ink);
  font-size: 0.9375rem;
}


/* ==========================================================================
   9c-3a2. DEPOSIT CHECKOUT — the "Pay deposit" view of the contact modal
   Reached from the tab pair above the inquiry form, which only appears when
   the link that opened the modal named a package (data-price). Everything
   reuses .bfield / .bmodal__nav / .bdone so the two views feel like one
   surface rather than two designs sharing a panel.

   VISUAL DESIGN ONLY. No card data is collected, nothing is transmitted:
   the card inputs are `disabled` and carry no name attributes, so they are
   inert and unsubmittable by construction, not just by convention. The
   .cpay__card block is sized to be swapped for Stripe Elements without the
   surrounding layout moving.
   ========================================================================== */
.cpay__tabs {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.375rem;
  margin-bottom: 1.5rem;
  padding: 0.3125rem;
  border-radius: 999px;
  background: rgba(11, 7, 16, 0.06);
}

.cpay__tab {
  padding: 0.625rem 0.5rem;
  border: none;
  border-radius: 999px;
  background: none;
  color: var(--paper-muted);
  cursor: pointer;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  transition: background 0.18s ease, color 0.18s ease;
}

.cpay__tab.is-active {
  background: #fff;
  box-shadow: 0 1px 3px rgba(11, 7, 16, 0.14);
  color: var(--paper-ink);
}

/* --- The money, stated plainly ---------------------------------------------
   The one thing a person needs from this screen is "how much is leaving my
   account right now". So the deposit row is the only one at full size and
   full ink; the total above it and the balance below are context. */
.cpay__summary {
  margin-bottom: 0.5rem;
  padding: 1.25rem;
  border: 1.5px solid rgba(11, 7, 16, 0.12);
  border-radius: 0.5rem;
  background: linear-gradient(180deg, rgba(124, 92, 255, 0.06), rgba(124, 92, 255, 0));
}

.cpay__row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  padding-block: 0.5rem;
}

.cpay__row + .cpay__row { border-top: 1px solid rgba(11, 7, 16, 0.08); }

.cpay__label {
  color: var(--paper-muted);
  font-size: 0.875rem;
}

.cpay__val {
  color: var(--paper-ink);
  font-size: 1rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;   /* so the digits don't shuffle on swap */
}

/* The deposit row — the headline number. */
.cpay__row--muted .cpay__label { color: var(--paper-ink); font-weight: 700; }

.cpay__row--muted .cpay__val {
  color: var(--accent-ink);
  font-size: clamp(1.5rem, 6vw, 1.875rem);
  font-weight: 900;
  letter-spacing: -0.03em;
}

.cpay__pct {
  display: inline-block;
  margin-left: 0.375rem;
  padding: 0.125rem 0.4375rem;
  border-radius: 999px;
  background: var(--violet-500);
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  vertical-align: 0.1em;
}

.cpay__row--due .cpay__val { color: var(--paper-muted); font-weight: 700; }

.cpay__note {
  margin-top: 0.875rem;
  color: var(--paper-muted);
  font-size: 0.8125rem;
  line-height: 1.55;
}

/* --- Card block — the Stripe Elements slot -------------------------------- */
.cpay__card { margin-top: 1.25rem; }

.cpay__cardrow { position: relative; }

.cpay__cardnum { padding-right: 5.75rem; }   /* clears the brand marks */

.cpay__brands {
  position: absolute;
  top: 50%;
  right: 0.75rem;
  display: flex;
  gap: 0.25rem;
  transform: translateY(-50%);
  pointer-events: none;
}

.cpay__brand {
  padding: 0.1875rem 0.3125rem;
  border: 1px solid rgba(11, 7, 16, 0.16);
  border-radius: 0.1875rem;
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.5625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
}

.cpay__cardsplit {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

/* The disabled state is doing double duty here — it is also the honest
   signal that this is a mock-up. Keep it legible rather than greyed out
   into illegibility, since it is on screen as a design. */
.cpay__card .bfield__input:disabled {
  background: rgba(11, 7, 16, 0.03);
  color: var(--paper-muted);
  cursor: not-allowed;
  -webkit-text-fill-color: var(--paper-muted);   /* iOS ignores color here */
  opacity: 1;                                     /* ...and dims it further */
}

.cpay__secure {
  display: flex;
  align-items: flex-start;
  gap: 0.375rem;
  margin-top: 0.625rem;
  color: var(--paper-muted);
  font-size: 0.75rem;
  line-height: 1.45;
}

.cpay__secure svg { flex: none; margin-top: 0.1875rem; }

.cpay__submit { font-variant-numeric: tabular-nums; }

/* The one place in the gift card popup that says something went wrong. It
   replaces alert(): an alert steals focus, cannot be styled, and on iOS is
   silently dropped if it fires too soon after a redirect — which is exactly
   when this needs to be seen.

   Red rather than the amber used for the booking fallback, because unlike
   that case the customer's action did not go through and they have to do
   something. #B3261E on #FDECEA is 6.9:1. role="alert" in the markup makes
   a screen reader announce it the moment it is unhidden. */
.cpay__err {
  margin-top: 1rem;
  padding: 0.75rem 0.875rem;
  border-left: 3px solid #B3261E;
  background: #FDECEA;
  color: #7A1C14;
  font-size: 0.875rem;
  line-height: 1.5;
}

@media (max-width: 26.5em) {
  /* Expiry and CVC side by side get too narrow to type into on the
     smallest phones once the placeholder is accounted for. */
  .cpay__cardsplit { grid-template-columns: 1fr; }
}


/* ==========================================================================
   9c-3b. CITY HERO
   "Brampton's #1 Free-Roam VR Arena." is a much longer headline than the
   home page's "Step Into Another World." and a much longer subhead. At the
   shared sizes the longest word hit the right edge and the paragraph took
   over the screen — both step down here.
   ========================================================================== */
/* These are the base .hero__title / .hero__sub formulas scaled down ~10%,
   NOT numbers picked from nowhere — the city headline is a longer string, so
   it needs a nudge to stop the longest word touching the right edge, and
   nothing more than that. Keeping the same shape of formula means the city
   hero still responds to viewport width and height exactly like the home
   hero does. */
/* 13vw, not 14vw. The city headlines are the longest single words on the site:
   "BRAMPTON'S" and "OAKVILLE'S" are 9 caps plus an apostrophe, which at Inter
   Black measures ~6.55em. At 14vw on a 390px phone that solved to 54.6px and
   the word ran ~12px past the content box — clipped flat by the overflow-x on
   html/body, which is what cut the headline off at the screen edge. "FREE-ROAM"
   cleared by only 2px, so the whole line was on the edge of breaking too.
   13vw gives both a real margin at every phone width. */
/* "BRAMPTON'S" is 10 characters and has to fit one line at 360px. The governing
   term is the vw one: at 390px, 8vw ≈ 31px, which fits with margin to spare.
   The previous 11vw (≈43px) is what made the headline swallow the viewport in
   portrait — the rem term never got a chance to win. Ceiling stays high so
   desktop is untouched. */
.hero__title--city {
  font-size: clamp(2.5rem, min(3.05rem + 2.8vw, 13vw, 10.4vh), 7.9rem);
  font-size: clamp(2.5rem, min(3.05rem + 2.8vw, 13vw, 10.4svh), 7.9rem);
  /* Belt and braces: if a longer city name is ever added, break it rather than
     let it run off the screen. */
  overflow-wrap: break-word;
}

.hero__sub--city {
  font-size: clamp(1rem, 4.6vw, 1.3125rem);
}

@media (min-width: 56.25em) {
  /* Same 10% step down from the desktop base rule. */
  .hero__title--city { font-size: clamp(2.7rem, min(3.4rem + 3.1vw, 13.5vw, 10.8vh), 8.55rem); }
  .hero__title--city { font-size: clamp(2.7rem, min(3.4rem + 3.1vw, 13.5vw, 10.8svh), 8.55rem); }
  .hero__sub--city   { font-size: 1.0625rem; }
}


/* ==========================================================================
   9c-3c. MAP — embedded Google Map on the city pages
   ========================================================================== */
.findus {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--indigo-950);
  color: var(--text);
}

.findus__grid {
  display: grid;
  gap: 2rem;
  margin-top: 2.5rem;
}

.findus__map {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border-radius: 0.875rem;
  background: var(--indigo-800);
}

.findus__map iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

.findus__addr {
  font-style: normal;
  font-size: 1.125rem;
  font-weight: 800;
  line-height: 1.5;
}

.findus__meta {
  margin-top: 1.25rem;
  color: var(--text-soft);
  font-size: 0.9375rem;
  line-height: 1.7;
}

.findus__meta strong { color: var(--text); }

.findus__cta { margin-top: 1.75rem; }

@media (min-width: 56.25em) {
  .findus__grid { grid-template-columns: 1.2fr 1fr; align-items: center; gap: 3rem; }
  .findus__map { aspect-ratio: 16 / 11; }
}


/* ==========================================================================
   9c-3d. COMPARE — free-roam vs booth VR
   ========================================================================== */
.compare {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--indigo-950);
  color: var(--text);
}

.compare__intro {
  max-width: 44rem;
  margin: -1rem 0 clamp(2.5rem, 6vw, 4rem);
  color: var(--text-soft);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.65;
  text-wrap: pretty;
}

/* --- Paired rows -----------------------------------------------------------
   One row per dimension with both answers on it, because that's what makes it
   a comparison — the previous two-column version put "thumbstick" and "your
   body is the controller" in separate boxes with nothing connecting them.

   No cards, no ticks, no red crosses. The contrast is carried by type: the
   booth column is muted and set at normal weight, ours is white and bold,
   with a mint rule down its edge. That reads as a verdict without shouting. */
.vs {
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}

/* Column heads only make sense once the row is actually two columns. Below
   that the inline .vs__tag labels do the same job. */
.vs__heads { display: none; }

.vs__row {
  display: grid;
  gap: 0.4375rem;
  padding-block: clamp(1.25rem, 3vw, 1.625rem);
  border-bottom: 1px solid rgba(255, 255, 255, 0.10);
}

/* The last row's rule doubles up with the section's own bottom edge and reads
   as a heavier line than the five above it. */
.vs__row:last-child { border-bottom: none; }

.vs__dim {
  color: var(--accent);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

/* NO tick/cross marks and NO highlighted panel behind our column. A tinted
   column with green ticks down it is the single most recognisable comparison-
   table layout on the internet — it is what every SaaS pricing page and every
   generated "us vs them" block looks like, so reaching for it makes the
   section read as boilerplate no matter how good the words are.

   The contrast is carried by TYPE instead, and pushed much harder than a bold
   weight: theirs is small, dim and set at normal weight; ours is a size step
   up, white, and heavy. Scanning the column, the difference is obvious before
   anything is read — which is the job the ticks were doing, done with the
   thing the rest of the page already uses. */
.vs__them,
.vs__us {
  line-height: 1.45;
  text-wrap: pretty;
}

.vs__them {
  color: var(--text-muted);
  font-size: clamp(0.875rem, 2.1vw, 0.9375rem);
}

.vs__us {
  color: var(--text);
  font-size: clamp(1.0625rem, 2.8vw, 1.25rem);
  font-weight: 800;
  letter-spacing: -0.011em;
}

/* Inline row labels — phones only, where there are no column heads to
   tell you which line is which. */
.vs__tag {
  display: inline-block;
  margin-right: 0.5rem;
  color: var(--text-muted);
  font-family: var(--font-label);
  font-size: 0.625rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  vertical-align: 0.08em;
}

.vs__us .vs__tag { color: var(--mint); }

@media (min-width: 56.25em) {
  .vs__heads,
  .vs__row {
    display: grid;
    grid-template-columns: minmax(0, 10rem) minmax(0, 1fr) minmax(0, 1fr);
    gap: clamp(1.5rem, 3vw, 2.5rem);
    align-items: baseline;
  }

  .vs__heads {
    padding-block: 1.125rem;
    border-bottom: 1px solid rgba(255, 255, 255, 0.14);
  }

  .vs__head {
    font-family: var(--font-label);
    font-size: 0.6875rem;
    font-weight: 800;
    letter-spacing: 0.16em;
    text-transform: uppercase;
  }

  .vs__head--them { color: var(--text-muted); }

  .vs__head--us { color: var(--mint); }

  /* One hairline runs the full height of our column — the head and every row
     share it, so it reads as a single continuous edge rather than a border
     drawn six times. No fill behind it: the type is doing the work. */
  .vs__heads .vs__head--us,
  .vs__us {
    padding-left: clamp(1rem, 2vw, 1.5rem);
    border-left: 1px solid rgba(67, 239, 186, 0.45);
  }

  /* Reading across a six-row table is where the eye loses its line. Lifting
     the whole row on hover keeps the dimension, the booth answer and ours
     tied together while you scan. Pointer devices only — on touch there is no
     hover, and a sticky :hover state left behind by a tap reads as a bug. */
  .vs__row { transition: background-color 0.15s var(--ease); }

  @media (hover: hover) {
    .vs__row:hover { background: rgba(255, 255, 255, 0.025); }
  }

  .vs__tag { display: none; }
}
@media (min-width: 48em) {
  .compare__grid { grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: start; }
}

/* ==========================================================================
   9c-3a2. EXPERIENCE PAGES — VR Escape Rooms / VR Action Games

   These two pages reuse the landing page's own components — the .hero shell
   and the .worlds card rail — so they read as the same site rather than as
   two pages someone styled separately. Only the three components below are
   new, and each exists because nothing on the landing page did that job.
   ========================================================================== */

/* --- Feature ticks ---------------------------------------------------------
   White band straight after the video section — the light break stops the
   dark run and makes the eight facts read as a spec strip rather than as
   more marketing. No boxes and no icons: a tick and a label, nothing else,
   so eight of them scan in about two seconds. */
.feats {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--paper);
  color: var(--paper-ink);
}

.feats__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1.125rem 1.5rem;
  margin-top: clamp(2rem, 5vw, 3rem);
  list-style: none;
}

.feat {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  font-size: clamp(0.9375rem, 2.4vw, 1.0625rem);
  font-weight: 800;
  line-height: 1.3;
}

/* Mint disc, dark tick — the same mint as the CTAs, so the ticks read as the
   brand's "yes" colour. Drawn from two borders rather than a glyph: no icon
   font to load and it stays crisp at any size. */
.feat__tick {
  position: relative;
  flex: none;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  background: var(--mint);
}

.feat__tick::after {
  content: '';
  position: absolute;
  top: 45%;
  left: 50%;
  width: 0.3125rem;
  height: 0.625rem;
  transform: translate(-50%, -50%) rotate(45deg);
  border-right: 2px solid var(--ink-dark);
  border-bottom: 2px solid var(--ink-dark);
}

@media (min-width: 40em) {
  .feats__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* --- Rich variant ----------------------------------------------------------
   The plain grid is eight single words, which reads as a spec sheet: true,
   but it never says what any of it is like to stand in. The rich variant
   gives each point a sentence, so the section can argue the thing that
   actually sells a VR escape room — that you are somewhere a real room could
   never be. Two columns rather than three: these need the measure. */
/* Sits under the heading, which carries a big bottom margin of its own, so
   this pulls back up against it rather than floating in the gap. */
.feats__intro {
  max-width: 44rem;
  margin: clamp(-3.5rem, -5vw, -2.25rem) 0 clamp(2.5rem, 5vw, 3.5rem);
  color: var(--paper-muted);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.65;
  text-wrap: pretty;
}

.feats__grid--rich {
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1.75rem, 4vw, 2.5rem);
}

@media (min-width: 48em) {
  .feats__grid--rich { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* The tick lines up with the first line of the heading rather than the
   middle of the whole block, which is what centring would do once there is
   a paragraph under it. */
.feat--rich {
  align-items: start;
  gap: 0.875rem;
}

.feat--rich .feat__tick { margin-top: 0.125rem; }

.feat__body {
  display: grid;
  gap: 0.375rem;
}

.feat__name {
  font-size: clamp(1.0625rem, 2.6vw, 1.1875rem);
  font-weight: 800;
  letter-spacing: -0.015em;
}

.feat__note {
  color: var(--paper-muted);
  font-size: clamp(0.9375rem, 2.2vw, 1rem);
  font-weight: 400;
  line-height: 1.6;
  text-wrap: pretty;
}

@media (min-width: 62em) {
  .feats__grid { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1.5rem 2rem; }
}


/* --- Spec chips ------------------------------------------------------------
   Facts, colour-coded by what they mean: mint = anyone can do this, yellow =
   step up, red = genuinely frightening. The colour is the information. */
.chip {
  display: inline-flex;
  align-items: center;
  padding: 0.3125rem 0.6875rem;
  border: 1px solid rgba(255, 255, 255, 0.20);
  color: var(--text-soft);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* The ramp is mint -> lavender -> amber -> magenta -> brick, and it has to
   survive being scanned rather than read. "High intensity" and "Intense" both
   sat on amber, which made two different tiers look like one label somebody
   had typed two ways. Magenta puts a real step between them and still reads
   as below horror's brick red, which is the only tier above it. */
.chip--family   { border-color: rgba(67, 239, 186, 0.55); color: var(--mint); }
.chip--moderate { border-color: rgba(181, 123, 255, 0.60); color: var(--accent); }
.chip--intense  { border-color: rgba(255, 212, 59, 0.55);  color: var(--deal); }
.chip--extreme  { border-color: rgba(255, 45, 119, 0.60);  color: #FF6FA0; }
.chip--horror   { border-color: rgba(255, 107, 107, 0.60); color: #FF8B8B; }

/* On the white picker table the lavender and mint need the darker mixes. */
.picker .chip { border-width: 1px; }
.picker .chip--family   { color: #0E8F6A; border-color: rgba(14, 143, 106, 0.45); }
.picker .chip--moderate { color: var(--accent-ink); border-color: rgba(165, 102, 255, 0.45); }
.picker .chip--intense  { color: #8A6400; border-color: rgba(138, 100, 0, 0.40); }
.picker .chip--extreme  { color: #C2185B; border-color: rgba(194, 24, 91, 0.42); }
.picker .chip--horror   { color: #C22C2C; border-color: rgba(194, 44, 44, 0.42); }


/* --- Single-statement band -------------------------------------------------
   One idea, at a size you can't scroll past. Deliberately the opposite shape
   to everything around it: no grid, no cards, almost no words. */
.state {
  position: relative;
  isolation: isolate;
  padding-block: clamp(5rem, 14vw, 10.5rem);
  background: var(--indigo-900);
  color: var(--text);
  overflow: hidden;
}

.state::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    radial-gradient(60% 80% at 88% 18%, rgba(124, 92, 255, 0.42), transparent 68%),
    radial-gradient(50% 70% at 4% 96%, rgba(11, 6, 26, 0.80), transparent 72%);
}

.state__lead {
  max-width: 20ch;
  margin-left: -0.035em;
  font-size: clamp(2.4rem, 8.4vw, 6rem);
  font-weight: 900;
  line-height: 0.96;
  letter-spacing: -0.045em;
  text-transform: uppercase;
  text-wrap: balance;
}

.state__accent { color: var(--accent); }

.state__note {
  max-width: 34rem;
  margin-top: clamp(1.75rem, 4vw, 2.5rem);
  color: var(--text-soft);
  font-size: clamp(1rem, 2.5vw, 1.1875rem);
  line-height: 1.65;
  text-wrap: pretty;
}

/* --- Free-roam split: copy beside a photo ----------------------------------
   The section used to be a single centred column of type. It carries a photo
   now, so from 56.25em up the copy takes the left and the picture the right —
   the same breakpoint the rest of the site uses to go two-column. Below that
   they stack copy-first: the headline is the thing being said, and a tall
   portrait shot above it would bury the argument under a picture. */
.state__grid {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  align-items: center;
}

@media (min-width: 56.25em) {
  .state__grid { grid-template-columns: 1.05fr 0.95fr; }

  /* Both caps exist to stop a full-bleed column running to a silly measure.
     In half the width they fight instead: the lead wraps two words early and
     the note leaves a ragged hole down the middle. The column is the measure
     here. */
  .state__grid .state__lead,
  .state__grid .state__note { max-width: none; }
}

/* The kicker introduces the whole section here rather than labelling a row,
   so it sits a step up from the 0.8125rem it uses everywhere else. Scoped to
   .state, so every other kicker on the site is untouched. */
.state .kicker--dark {
  font-size: clamp(0.9375rem, 2.1vw, 1.0625rem);
  letter-spacing: 0.16em;
}

.state__cta { margin-top: clamp(2rem, 4vw, 2.75rem); }

/* Photo slot, same contract as .motion__media: the URL rides in data-bg-src
   and lazybg.js writes --state-img once the section is within about two
   screens, so a 200KB shot is never fetched for someone who stops short of
   it. Until then the gradient below stands in, which is why an unset photo
   reads as deliberate rather than broken.

   4/5 because the source is a tall phone photo (3024x4032). A squarer box
   crops heads off; the full 3/4 leaves a column too long to sit beside the
   copy without dragging the section out. */
.state__media {
  aspect-ratio: 4 / 5;
  background-image: var(--state-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  /* The one knob worth knowing about in this section. The box is 4/5 and the
     source is a tall 3/4 phone photo, so a band gets cropped off somewhere —
     this picks which. Lower percentages keep the top of the frame, higher
     ones slide the picture up and keep more of the bottom. 50% was the
     setting that stopped cutting the kneeling figure. */
  background-position: center 50%;
  background-repeat: no-repeat;
  border-radius: 0.875rem;
}

.state__media:not([style*="--state-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}


/* --- Suited-for strip ------------------------------------------------------
   A dark band between two white sections (.how above, .feats below). It is a
   list of who books this, not a section in its own right, so the padding is
   shallow the way .strip's is — it should read as a beat between two stops,
   not as another stop.

   The items are a <ul> because they are a list, and they wrap into a centred
   row rather than sitting in a grid: five items of very different lengths in
   fixed tracks leave holes at every breakpoint, and this way the row simply
   reflows. */
.suited {
  padding-block: clamp(2.25rem, 5vw, 3.25rem);
  background: var(--indigo-950);
  color: var(--text);
  text-align: center;
}

.suited__label {
  margin-bottom: clamp(1.25rem, 3vw, 1.75rem);
  color: var(--accent-ink);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.suited__list {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.625rem clamp(0.625rem, 2vw, 1rem);
  margin: 0;
  padding: 0;
  list-style: none;
}

.suited__item {
  padding: 0.5rem 1.125rem;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.03);
  color: var(--text-soft);
  font-family: var(--font-label);
  font-size: clamp(0.875rem, 2.1vw, 1rem);
  font-weight: 700;
  letter-spacing: 0.02em;
}

/* Desktop is a different composition, not a wider version of the phone one.
   Centred pills in a shallow band read as a stray strip from another site:
   every other section on these pages starts hard against the same left edge,
   so the one centred thing in the middle of them looks misplaced rather than
   deliberate — and five pills centred in a 1800px wrap leaves the band
   looking thin and accidental.

   So from 56.25em the label becomes a real heading holding the left edge,
   and the five names take the rest of the row as equal cells split by
   hairlines. Equal cells, not pills: the names run from 7 to 23 characters,
   and hugging pills at those lengths makes the row look like it was dropped
   in rather than set. The rule between each is what gives every name its own
   space without drawing five boxes.

   Phones keep the pills. A five-cell row at 390px would be unreadable, and
   the centred stack is already the right answer there. */
@media (min-width: 56.25em) {
  .suited {
    padding-block: clamp(3.5rem, 7vw, 5.25rem);
    text-align: left;
  }

  .suited__inner {
    display: grid;
    grid-template-columns: minmax(11rem, 15rem) 1fr;
    gap: clamp(2rem, 5vw, 3.5rem);
    align-items: center;
  }

  /* Title size, so it sits in the page's type hierarchy instead of reading
     as a caption floating above a row of chips. */
  .suited__label {
    margin-bottom: 0;
    color: var(--text);
    font-family: var(--font);
    font-size: clamp(1.75rem, 3.2vw, 2.5rem);
    font-weight: 900;
    line-height: 1.02;
    letter-spacing: -0.03em;
    text-transform: none;
  }

  .suited__list {
    display: grid;
    grid-template-columns: repeat(5, 1fr);
    gap: 0;
  }

  /* The divider is a left border on every cell but the first, so there is a
     rule BETWEEN names and none hanging off either end of the row. */
  .suited__item {
    padding: 0.375rem clamp(0.75rem, 1.6vw, 1.25rem);
    border: 0;
    border-left: 1px solid rgba(255, 255, 255, 0.16);
    border-radius: 0;
    background: none;
    color: var(--text);
    font-size: clamp(0.9375rem, 1.15vw, 1.0625rem);
    line-height: 1.25;
    text-align: left;
    text-wrap: balance;
  }

  .suited__item:first-child {
    padding-left: 0;
    border-left: 0;
  }
}


/* --- Picker table ----------------------------------------------------------
   Replaces the "why choose us" and "perfect for" card grids with the thing
   people are actually trying to work out on this page: which one do we book.
   A table, because it's tabular data and pretending otherwise helps nobody. */
.picker {
  padding-block: clamp(4rem, 10vw, 7.5rem);
  background: var(--paper);
  color: var(--paper-ink);
}

.picker__intro {
  max-width: 40rem;
  margin: -1.75rem 0 clamp(1.75rem, 4vw, 2.5rem);
  color: var(--paper-muted);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.6;
  text-wrap: pretty;
}

/* This was 11px of muted grey uppercase — technically present, practically
   invisible, so people never discovered the table scrolls sideways and just
   assumed the columns they could see were all of them. It is now a pill in the
   brand violet with a moving arrow: the only cue that the table has more to it,
   so it has to actually read as one. */
.picker__hint {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
  padding: 0.5rem 0.875rem;
  border-radius: 999px;
  background: rgba(124, 92, 255, 0.12);
  color: #6D28D9;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

/* The arrow slides, because a static arrow next to "scroll" is still just
   text. Short, small and looping — enough to catch the eye in peripheral
   vision without becoming the loudest thing on screen. */
.picker__hint::after {
  content: "\2192";
  font-size: 1rem;
  animation: picker-nudge 1.4s ease-in-out infinite;
}

@keyframes picker-nudge {
  0%, 100% { transform: translateX(0); }
  50%      { transform: translateX(0.3125rem); }
}

@media (prefers-reduced-motion: reduce) {
  .picker__hint::after { animation: none; }
}

.picker__scroll {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

.picker__table {
  width: 100%;
  min-width: 46rem;
  border-collapse: collapse;
  text-align: left;
}

.picker__table th,
.picker__table td {
  padding: 1.0625rem 1.25rem 1.0625rem 0;
  border-bottom: 1px solid rgba(11, 7, 16, 0.12);
  vertical-align: middle;
}

.picker__table thead th {
  padding-bottom: 0.75rem;
  border-bottom-color: rgba(11, 7, 16, 0.32);
  color: var(--paper-muted);
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.picker__table tbody tr:last-child th,
.picker__table tbody tr:last-child td { border-bottom: 0; }

.picker__room {
  font-size: 1.0625rem;
  font-weight: 900;
  letter-spacing: -0.018em;
}

.picker__age {
  font-size: 1.0625rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;
}

.picker__for {
  color: var(--paper-muted);
  font-size: 0.9375rem;
  line-height: 1.5;
}

@media (min-width: 48em) {
  .picker__hint { display: none; }
  .picker__table { min-width: 0; }
}



/* --- Closing CTA band ------------------------------------------------------ */
/* White, deliberately. The FAQ above it is --indigo-950 and so was this, so
   the page's last ask arrived on the same ground as everything before it and
   had nothing to separate it. Flipping to paper makes the closing CTA the one
   thing on the page that changes colour. */
/* Dark. This is the last thing before the footer on both product pages, and
   on white it left the page ending on white-white-black. Dark here puts a
   real closing beat between the white FAQ and the footer's true black, and
   the mint button finally has a ground it can sit on rather than fade into.
   Only these two pages use .close, so no modifier is needed. */
.close {
  padding-block: clamp(4.5rem, 11vw, 8rem);
  background: var(--indigo-950);
  color: var(--text);
  text-align: center;
}

.close .section-title { margin-left: 0; text-align: center; }
.close .section-title--dark { color: var(--text); }

/* Back to the brighter lavender now the ground is dark — --accent-ink was
   the substitution this section needed only while it was white. */
.close .section-title__accent { color: var(--accent); }

.close__sub {
  max-width: 34rem;
  margin: -1.5rem auto 2rem;
  color: var(--text-soft);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.6;
  text-wrap: pretty;
}

/* Mint on white is a pale fill — the dark label on it is still 13.6:1, but the
   button's own edge goes soft. A hairline of the deeper action colour keeps it
   reading as a button rather than a highlighted word. */
.close__cta {
  width: auto;
  min-width: 16rem;
}

/* Sits above the CTA, not below it: the button is the thing this section
   exists for, and anything under it reads as an afterthought. These are the
   escape hatches for someone who got here and decided this particular page
   is not what they wanted. */
.close__links {
  max-width: 38rem;
  margin: -0.75rem auto 2rem;
  color: var(--text-muted);
  font-size: clamp(0.9375rem, 2.2vw, 1rem);
  line-height: 1.6;
  text-wrap: pretty;
}

.close__links a { color: var(--accent); font-weight: 700; }

/* Same job on the gift-cards page, which is a light section. */
.gifts__note {
  max-width: 42rem;
  margin: clamp(2rem, 4vw, 2.5rem) auto 0;
  color: var(--text-soft);
  font-size: clamp(0.9375rem, 2.2vw, 1rem);
  line-height: 1.6;
  text-align: center;
  text-wrap: pretty;
}

.gifts__note a { color: var(--accent); font-weight: 700; }


/* ==========================================================================
   9c-3d8. KIDS GAMES — the shortlist a parent actually wants
   The main worlds rail is eleven action titles and six escape rooms sorted
   by theme, which is the wrong sort for someone booking a 9th birthday.
   This is the same card shape, pre-filtered to what works for kids, with
   player counts on the face so nobody has to open anything.
   ========================================================================== */
.kidgames {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--indigo-950);
  color: var(--text);
}

.kidgames .section-title { text-align: center; margin-left: 0; }

/* The rail above this is a horizontal scroller whose bottom padding is sized
   for the card glow, not for separating the next block — so this line was
   landing almost directly under the cards. It is a different thought ("want
   the full list?"), not a caption on them, and needs to read that way. */
/* This element also carries .kidgames__intro, whose shorthand sets a NEGATIVE
   top margin (-2rem) to tuck the intro under the section heading. On the outro
   that pulled it back UP toward the cards, cancelling most of the gap added
   here — so the top margin has to be stated as a single value that wins, not
   as an addition to a rule that is subtracting.

   The inline style="margin-bottom: 0" in the markup then removed all space
   BELOW it, leaving the line stranded against the cards with a dead band
   underneath. Both ends are set explicitly here instead. */
.kidgames__intro {
  max-width: 42rem;
  margin: -2rem auto clamp(2.5rem, 5vw, 3.5rem);
  color: var(--text-soft);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.6;
  text-align: center;
}

/* MUST stay below .kidgames__intro. The outro element carries BOTH classes,
   they are both single-class selectors, so specificity ties and source order
   decides the winner. This rule used to sit above it, which meant the intro's
   `margin: -2rem auto …` shorthand overwrote the margin-top set here every
   time — the line was pulled 2rem UP into the last row of cards, landing on
   top of the card captions. Nothing about the value was wrong; it simply never
   applied. Do not move this block. */
.kidgames__outro {
  margin-top: clamp(3.5rem, 8vw, 5rem);
  margin-bottom: 0;
}

.kidgames__rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(13rem, 62vw);
  gap: clamp(1rem, 2.5vw, 1.5rem);
  /* Same reason as .parents__grid — no negative-margin bleed, or the page
     scrolls sideways on a phone. */
  /* Same treatment as .parents__grid, and for the same two reasons: the glow
     needs var(--glow-reach) above or the horizontal scroller shaves it flat
     (2.5rem was 2px short of the 42px it actually paints), and the rail is bled
     to the bezel so the first card's left glow is cut at the screen edge rather
     than mid-content. Margin and padding are equal and opposite, so the margin
     box is exactly viewport-wide and nothing scrolls sideways. */
  margin-inline: calc(var(--gutter) * -1);
  padding: var(--glow-reach) var(--gutter) 1.5rem;
  scroll-padding-inline: var(--gutter);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  list-style: none;
}

.kidgames__rail::-webkit-scrollbar { display: none; }
.kidgames__item { scroll-snap-align: start; }

@media (min-width: 62em) {
  .kidgames__rail {
    grid-auto-flow: row;
    grid-template-columns: repeat(4, 1fr);
    grid-auto-columns: auto;
    overflow-x: visible;
    scroll-snap-type: none;
    /* Ten cards over four columns is three rows now, not one. The gap was set
       for a single row of cards sitting side by side, so between rows it left
       the captions of one row almost touching the artwork of the next. Rows
       need more air than columns do here because each cell ends in two lines
       of text rather than a hard edge. */
    row-gap: clamp(2.5rem, 5vw, 3.5rem);
    align-items: start;
    /* Bleed off, as on .parents__grid — otherwise the 4-up grid hangs a gutter
       past .wrap on each side. */
    margin-inline: 0;
    padding-inline: 0;
  }
}

/* 4/3, matching .game__thumb — these carry the same 16:9 YouTube key art the
   game cards do, and `cover` on a 16:9 source crops it to the centre 75%,
   which is the framing the rest of the site already uses. A portrait box
   would cut it to the middle 42% and lose the title art. */
.kidgame__art {
  aspect-ratio: 4 / 3;
  margin-bottom: 0.875rem;
  border-radius: 0.875rem;
  background-image: var(--kid-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  background-position: center;
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, 0.85),
    0 0 14px 2px rgba(124, 92, 255, 0.75),
    0 0 34px 8px rgba(124, 92, 255, 0.35);
}

.kidgame__art:not([style*="--kid-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}

.kidgame__name {
  margin-bottom: 0.25rem;
  font-size: clamp(1.0625rem, 2.4vw, 1.1875rem);
  font-weight: 800;
  letter-spacing: -0.02em;
}

.kidgame__meta {
  color: var(--text-muted);
  font-size: 0.9375rem;
  line-height: 1.45;
}


/* ==========================================================================
   9c-3d7. PARTY PACKAGES — photo above, headline in neon
   The birthday page's plans carry a photo and a two-line neon headline that
   the other pages' plans do not, so everything here hangs off .plans--party
   rather than touching .plan itself. Team events and gift cards use the same
   .plan component and must not move.
   ========================================================================== */
/* The photo IS the top of the card, so ALL of .plan's padding comes off and
   .plan__inner puts it back around the text only. Zeroing just padding-top
   left the card's 1.75rem side padding in place, which inset the photo.

   NO overflow:hidden here, deliberately. .plan has no border-radius, so
   there are no corners to clip the photo to — and .plan__flag sits at
   top:-0.8125rem, i.e. deliberately OUTSIDE the card's top edge, so
   clipping the overflow slices the "Most Popular" badge in half. */
.plans--party .plan { padding: 0; }

.plans--party .plan__shot {
  aspect-ratio: 16 / 10;
  margin: 0 0 1.5rem;
  background-image: var(--party-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  background-position: center;
  /* Nothing clips this any more, so it can carry the same neon edge as every
     other photo on the site. Inset ring plus outward bloom — the inset half
     is what reads against a light card. */
  box-shadow:
    inset 0 0 0 2px rgba(255, 255, 255, 0.55),
    0 0 18px 2px rgba(124, 92, 255, 0.45);
}

/* The badge has to clear the photo it now sits on top of. */
.plans--party .plan__flag { z-index: 2; }

/* --- Package actions -------------------------------------------------------
   Three buttons under each package, in the order people actually decide in:
   pay a little now, ask a question, or send a full enquiry. They stack and
   go full width so the tap targets stay large on a phone, where nearly all
   of this traffic is. */
.plan__actions {
  display: grid;
  gap: 0.625rem;
  margin-top: auto;
  padding-top: 1.25rem;
}

/* nowrap is right for a header CTA but wrong here: these labels are long,
   and an unwrappable label sets the card's minimum width, which pushed the
   four-column grid wider than the viewport and made the whole page look
   zoomed in. */
.plan__actions .btn {
  width: 100%;
  white-space: normal;
  padding-inline: 1rem;
  text-align: center;
}

/* Deposit is the primary action: it is the smallest commitment on the card
   and the one that actually holds a date, so it gets the mint. */
.btn--deposit {
  background-color: var(--mint);
  color: var(--ink-dark);
}

.btn--deposit:hover { background-color: var(--mint-lift); }

/* WhatsApp in its own brand green rather than black or purple. Black would
   read as a third neutral next to the outline button and lose the "this is
   a different channel" signal; purple would compete with the site's own
   accent. Green is the one colour people already associate with the app,
   and it clears 4.6:1 against white text. */
.btn--whatsapp {
  background-color: #1DA851;
  color: #fff;
}

.btn--whatsapp:hover { background-color: #179144; }

/* The enquiry form is the slowest of the three, so it is the quiet one. */
.btn--ghost {
  background-color: transparent;
  color: var(--paper-ink);
  box-shadow: inset 0 0 0 1px rgba(11, 7, 16, 0.28);
}

.btn--ghost:hover { background-color: rgba(11, 7, 16, 0.05); }

/* White on dark — the counterpart to .btn--ghost, which is a light-section
   button. Used under the parent reviews, where a mint --primary would read as
   the section's main call to action; this is a sideways step to the FAQ, not
   the thing we want people clicking most. */
.btn--light {
  background-color: #fff;
  color: var(--paper-ink);
}

.btn--light:hover { background-color: rgba(255, 255, 255, 0.88); }

.parents__cta {
  display: flex;
  justify-content: center;
  margin-top: clamp(2rem, 5vw, 3rem);
}

.plans--party .plan__shot:not([style*="--party-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}

/* Matches .plan's original 1.75rem sides so the party cards line up with the
   team-event and gift-card ones; only the top is gone, where the photo is. */
.plans--party .plan__inner {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: 0 1.75rem 2rem;
}

/* The headline the whole card is scanned for: duration on one line, capacity
   on the next in the accent, both big enough to read from a scroll. text-shadow
   rather than a filter so the glow costs nothing to repaint. */
/* The clamp floor put this at 1.375rem on a phone against .plan__name's flat
   1.25rem — a 10% gap, which at the same weight and casing reads as no gap at
   all. "2 HOURS VR PARTY / UP TO 12 KIDS" is what the card is selling; "GOLD"
   is just the label for it. Lift the floor so the hierarchy is legible on the
   narrow layout. Desktop is unchanged: the vw term passes 1.5rem at ~441px,
   so every viewport above that resolves exactly as before. */
.plans--party .plan__headline {
  margin-bottom: 0.875rem;
  font-size: clamp(1.5rem, 3.4vw, 1.75rem);
  font-weight: 900;
  letter-spacing: -0.03em;
  line-height: 1.05;
  text-transform: uppercase;
}

.plans--party .plan__hours { display: block; color: var(--paper-ink); }

/* The tier name is a label, not a headline. On the narrow layout it was
   competing with the line above it at nearly the same size; drop it a step so
   the separation reads. Mobile only — the 4-up desktop grid already has the
   width for both to sit at their natural sizes. */
@media (max-width: 47.99em) {
  .plans--party .plan__name { font-size: 1.0625rem; }
}

.plans--party .plan__kids {
  display: block;
  color: var(--accent-ink);
  text-shadow: 0 0 18px rgba(124, 92, 255, 0.45);
}


/* ==========================================================================
   9c-3d6. PARENT REVIEWS — photo above the quote
   The site's other quote cards are text only, which is right where the
   quote is the evidence. Here the photo IS the evidence: a parent reading
   this wants to see other people's kids in the room, and the words confirm
   what the picture already showed. So the picture goes first and gets the
   same neon edge as every other photo on the site.
   ========================================================================== */
.parents {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--indigo-950);
  color: var(--text);
  text-align: center;
}

.parents__sub {
  max-width: 40rem;
  margin: -2rem auto clamp(2.5rem, 5vw, 3.5rem);
  color: var(--text-soft);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.6;
}

/* Phones get a swipeable rail, not a stack. Three tall photo-and-quote cards
   stacked is a very long scroll for something a parent skims, and a rail
   makes it obvious there is more than one without any of them being read.

   scroll-snap rather than the clips.js drift: these are meant to be read,
   and text that slides away mid-sentence is worse than no motion at all.
   The negative margin plus matching padding lets the rail bleed to the
   screen edges while the first card still lines up with the section's
   left margin. */
.parents__grid {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(16rem, 78vw);
  gap: clamp(1rem, 3vw, 1.5rem);
  /* Bled to the screen edges, matching what .rail has always done. The comment
     that used to sit here rejected a bleed because it made the element wider
     than the screen — but that only happens when the negative margin is NOT
     matched by equal padding. Bled by exactly --gutter from inside .wrap
     (which already pads by --gutter), the margin box measures exactly the
     viewport width, so the document cannot scroll sideways. Without this the
     first card's left glow is cut flat mid-content at x=22px, which reads as
     damage; at the bezel it reads as "the rail continues", like every other
     rail on the site. */
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
  /* Keeps the snap line on the gutter — otherwise a swiped card lands flush
     against the bezel while the heading above it keeps its margin. */
  scroll-padding-inline: var(--gutter);
  /* Top padding is not decoration: overflow-x:auto clips vertically too, and
     the photos carry a glow that paints var(--glow-reach) outside their box.
     This was a hand-copied 2.5rem (40px) against a 42px reach, so the neon
     edge was shaved by 2px along the top of every card. Tied to the token now,
     so it cannot drift out of step with the shadow again. */
  padding-block: var(--glow-reach) 1.5rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  text-align: left;
}

.parents__grid::-webkit-scrollbar { display: none; }
.parents__grid > .parent { scroll-snap-align: start; }

/* From 48em there is room for real columns, so it stops being a rail. */
@media (min-width: 48em) {
  .parents__grid {
    grid-auto-flow: row;
    grid-template-columns: repeat(2, 1fr);
    grid-auto-columns: auto;
    overflow-x: visible;
    scroll-snap-type: none;
    /* The mobile edge bleed has to come back off, or the grid hangs a gutter
       past .wrap on both sides at desktop widths. */
    margin-inline: 0;
    padding-inline: 0;
  }
}

@media (min-width: 62em) { .parents__grid { grid-template-columns: repeat(3, 1fr); } }

.parent {
  display: flex;
  flex-direction: column;
  margin: 0;
}

/* Same photo slot contract as everywhere else: data-bg / data-bg-src, with
   the gradient standing in until lazybg.js promotes it. */
.parent__shot {
  aspect-ratio: 4 / 3;
  margin-bottom: 1.25rem;
  border-radius: 0.875rem;
  background-image: var(--parent-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  background-position: center;
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, 0.85),
    0 0 14px 2px rgba(124, 92, 255, 0.75),
    0 0 34px 8px rgba(124, 92, 255, 0.35);
}

.parent__shot:not([style*="--parent-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}

.parent__stars {
  margin-bottom: 0.75rem;
  color: #FFC53D;
  font-size: 0.9375rem;
  letter-spacing: 0.08em;
}

.parent__text {
  margin: 0 0 1rem;
  color: var(--text-soft);
  font-size: 1.0625rem;
  line-height: 1.6;
  text-wrap: pretty;
}

.parent__by {
  display: grid;
  gap: 0.1875rem;
  margin-top: auto;
}

.parent__name { font-size: 0.9375rem; font-weight: 800; letter-spacing: -0.01em; }

.parent__ctx {
  color: var(--text-muted);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}


/* ==========================================================================
   9c-3d5. VERSUS — traditional escape rooms against VR ones
   Two columns that argue with each other, with a VS badge on the seam. The
   left column is deliberately plain and grey and the right one carries the
   ticks and the accent, so the answer is legible before a word is read.

   Not built on .vs (the home page's booth-vs-free-roam table): that one is
   row-per-dimension with both answers on each row, which is right when the
   two lists are parallel. These lists are not — the VR column has six
   capabilities the traditional column simply does not have, so pairing them
   row-by-row would invent six fake opposites.
   ========================================================================== */
.versus {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--indigo-950);
  color: var(--text);
}

.versus__intro {
  max-width: 46rem;
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
  color: var(--text-soft);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.65;
  text-wrap: pretty;
}

.versus__grid {
  display: grid;
  gap: 1.25rem;
  position: relative;
}

@media (min-width: 48em) {
  .versus__grid {
    grid-template-columns: 1fr 1fr;
    gap: clamp(1.5rem, 3vw, 2.5rem);
    align-items: start;
  }
}

.versus__col {
  padding: clamp(1.5rem, 3.5vw, 2.25rem);
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 0.875rem;
  background: rgba(255, 255, 255, 0.03);
}

/* The winning column gets the accent border and a lift, so the comparison
   resolves at a glance instead of asking the reader to weigh two lists. */
.versus__col--vr {
  border-color: rgba(124, 92, 255, 0.55);
  background: rgba(124, 92, 255, 0.10);
}

.versus__head {
  margin-bottom: 1.25rem;
  padding-bottom: 1rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.12);
  font-size: clamp(1.125rem, 2.8vw, 1.375rem);
  font-weight: 900;
  letter-spacing: -0.02em;
  line-height: 1.2;
}

.versus__col--vr .versus__head { color: var(--accent); }

.versus__list {
  display: grid;
  gap: 0.8125rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.versus__item {
  display: grid;
  grid-template-columns: 1.25rem 1fr;
  gap: 0.75rem;
  align-items: start;
  color: var(--text-soft);
  font-size: clamp(0.9375rem, 2.2vw, 1.0625rem);
  line-height: 1.5;
}

/* Marks are drawn from ::before rather than typed into the markup: a screen
   reader announcing "check mark" before every line is noise, and the meaning
   is already carried by which column the line is in. */
.versus__item::before {
  content: "\2022";                     /* bullet */
  color: var(--text-muted);
  font-size: 0.9375rem;
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
}

.versus__col--vr .versus__item { color: var(--text); }

.versus__col--vr .versus__item::before {
  content: "\2713";                     /* check */
  color: var(--mint);
}

/* The badge sits on the seam between the columns on desktop and between the
   stacked cards on mobile, which is the one place it reads as "versus"
   rather than as a heading for whichever card follows it. */
.versus__badge {
  justify-self: center;
  align-self: center;
  width: 3.25rem;
  height: 3.25rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 900;
  letter-spacing: 0.06em;
}

@media (min-width: 48em) {
  .versus__badge {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 1;
  }
}

.versus__note {
  max-width: 46rem;
  margin-top: clamp(2rem, 4vw, 2.75rem);
  color: var(--text-muted);
  font-size: clamp(0.9375rem, 2.2vw, 1rem);
  line-height: 1.65;
  text-wrap: pretty;
}

.versus__cta { margin-top: clamp(2rem, 4vw, 2.75rem); }


/* ==========================================================================
   9c-3d4. EXPERIENCE — scroll-swapped copy beside a pinned photo
   Three claims that each need a picture, in a section that would be three
   near-identical rows if they were laid out flat. Instead the photo column
   pins and the copy scrolls past it, swapping the shot as each one reaches
   the middle of the screen.

   The whole effect is one class (.is-active) toggled by xp.js. With no JS
   every step and every photo is simply visible in order, which is also
   exactly what phones get — see the note on the grid below.
   ========================================================================== */
.xp {
  padding-block: clamp(4.5rem, 11vw, 8rem);
  background: var(--indigo-950);
  color: var(--text);
}

/* display:contents lifts .xp__copy and .xp__shot out of their <li> and into
   this grid directly, which is the whole trick: the copy can stack down the
   column while every shot shares ONE cell and pins as a group. Without it
   each photo would be trapped in its own list item and could only pin to
   that item's height. */
.xp__list {
  display: grid;
  grid-template-columns: 1fr;
  margin: 0;
  padding: 0;
  list-style: none;
}

.xp__step { display: contents; }

/* One column, so the photo and the copy are PINNED TOGETHER as one panel and
   both cross-fade. The earlier version left the copy in normal flow, which
   meant the text scrolled up over a photo that was standing still — the
   thing that looked broken.

   Nothing pinned can create scroll length, so .xp__rail does that job: it is
   an empty box per step, the only thing actually in flow, and its height is
   the distance you travel before the next claim takes over. That separation
   is what keeps the panel compact — the runway is not made of padding around
   the text, so the text can sit tight under the photo. */
.xp__copy {
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  grid-column: 1;
  grid-row: 1 / -1;
  position: sticky;
  /* margin-top is doing the important job here. Both the photo and the copy
     share one grid cell, so at rest they both sit at the cell's top edge and
     the text renders UNDERNEATH the photo — which is why the first step
     looked like it had no words. Offsetting the copy by the photo's height
     puts it below the picture before sticky ever engages, so step one reads
     correctly the moment the section scrolls in. */
  /* 32svh clears the photo, which is 30svh tall pinned at 13svh. Paired with
     the svh on .xp__shot above so the two stay in register — see the note
     there. The vh line is the fallback for browsers without svh, where the
     photo's own fallback is also vh, so the pair stays consistent. */
  margin-top: 32vh;
  margin-top: 32svh;
  top: 45vh;
  top: 45svh;
  align-self: start;
  opacity: 0;
  transition: opacity 0.45s var(--ease);
  pointer-events: none;
}

.xp__copy.is-active {
  opacity: 1;
  pointer-events: auto;
}

/* The runway — the distance you travel before the next claim takes over.
   This was 76vh, and two things made that far too long on a phone:

   1. vh resolves to the toolbars-HIDDEN height, so 76vh was ~566px on a phone
      whose visible height is ~664px. A comfortable thumb swipe moves ~250-350px,
      so one step cost roughly two swipes before the band even reacted.
   2. The observer band is only 10% tall (rootMargin -45%/-45%), so the swap
      does not fire the instant a step's rail enters the viewport — the rail has
      to travel until it crosses the centre strip. That lag sits ON TOP of the
      rail height, which is why it read as three scrolls rather than two.

   34svh is ~226px on a phone: one unhurried swipe advances exactly one step,
   with the centre band crossing partway through the gesture so the photo has
   swapped by the time the thumb lifts. svh not vh, so the number means what it
   says regardless of the toolbar. Three steps now cost ~680px total instead of
   ~1700px.

   Desktop is untouched: from 62em the observer watches .xp__copy instead and
   .xp__rail is display:none, so this height is not in play there. */
.xp__rail {
  grid-column: 1;
  min-height: 42vh;
  min-height: 34svh;
}

/* --- Runway for the LAST step ----------------------------------------------
   Steps 1 and 2 stay on screen while the rail BELOW them scrolls past. Step 3
   has no rail below it: its own rail is the last thing in the grid, so the
   moment that rail clears, step 3's sticky range is exhausted, the panel
   unsticks and the copy scrolls away — taking the number and the heading off
   the top of the screen first, which is what you were seeing.

   Fixed by lengthening the LAST rail rather than by padding .xp__list or
   adding a trailing row. .xp__copy and .xp__shot span `grid-row: 1 / -1`, so a
   grid item's sticky range is bounded by its grid AREA — container padding
   sits outside the row tracks and extends nothing, which is why padding-bottom
   made no difference here. The rails are the tracks, so making the last one
   taller is the one lever that reliably grows that area, and it reuses the
   exact mechanism that already holds steps 1 and 2.

   Roughly double a normal rail: one rail's worth to cross the observer band
   and activate, plus one to dwell there like the other two.

   Only below 62em: from there up, .xp__rail is display:none and the copy is in
   normal flow, so there is nothing pinned and this would be dead space. */
@media (max-width: 61.99em) {
  .xp__step:last-child .xp__rail {
    min-height: 84vh;
    min-height: 68svh;
  }
}

.xp__num {
  display: block;
  margin-bottom: 0.875rem;
  color: var(--accent);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.18em;
}

.xp__title {
  margin-bottom: 0.875rem;
  font-size: clamp(2.125rem, 7.4vw, 2.75rem);
  font-weight: 900;
  letter-spacing: -0.035em;
  line-height: 1.05;
  text-wrap: balance;
}

.xp__body {
  max-width: 34rem;
  color: var(--text-soft);
  font-size: clamp(1.0625rem, 4.2vw, 1.1875rem);
  line-height: 1.6;
  text-wrap: pretty;
}

/* Photo slot, same data-bg contract as everywhere else. Until a real URL is
   dropped into data-bg-src the gradient below stands in, so the section is
   presentable with no photos at all. */
/* Every shot occupies the same cell — column 1, all rows — so the three
   stack in one place and the sticky offset is measured against the whole
   list rather than one step. */
/* svh alongside the vh fallbacks: the photo, the copy's offset and the copy's
   sticky top are one interlocking set — the copy's margin-top has to clear the
   photo's height exactly, or the text renders under the picture on step one.
   Measuring some of them against the toolbars-hidden viewport and the rail
   against the visible one is what pulls them out of register on a phone. */
.xp__shot {
  grid-column: 1;
  grid-row: 1 / -1;
  position: sticky;
  top: 13vh;
  top: 13svh;
  align-self: start;
  height: 30vh;
  height: 30svh;
  opacity: 0;
  transition: opacity 0.45s var(--ease);
  background-image: var(--xp-img, none);
  background-color: var(--indigo-800);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  border-radius: 0.875rem;
}

.xp__shot:not([style*="--xp-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}

.xp__shot.is-active { opacity: 1; }

/* No JS: nothing ever gets .is-active, so without this the column would be
   three invisible photos. Show the first and let the rest stay hidden. */
.xp--noscript .xp__shot:first-of-type { opacity: 1; }

/* Desktop puts the copy and the pinned photo side by side. The mechanism is
   identical — same grid, same sticky cell — only the column count and the
   heights change, so the swap behaves the same at every width. */
@media (min-width: 62em) {
  .xp__list {
    grid-template-columns: 1fr 1fr;
    column-gap: clamp(3rem, 6vw, 6rem);
  }

  /* Two columns: the photo is BESIDE the copy, not over it, so the copy goes
     back into normal flow and carries its own scroll length. The rail is not
     needed and would only add empty rows. */
  .xp__rail { display: none; }

  .xp__copy {
    grid-row: auto;
    position: static;
    margin-top: 0;
    justify-content: center;
    min-height: 78vh;
    opacity: 1;
    pointer-events: auto;
  }

  .xp__shot {
    grid-column: 2;
    width: 100%;
    height: 78vh;
  }
}

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


/* ==========================================================================
   9c-3d3. WHY CHOOSE — the reasons grid above the SEO prose
   A search-led section that still has to look like something. The copy
   below it is deliberately paragraphs (that block earns its rankings by
   answering real queries in real sentences); this one carries the same
   keywords in a shape people will actually read, so the page has both.
   ========================================================================== */
.reasons {
  padding-block: clamp(4rem, 10vw, 7rem);
  background: var(--paper);
  color: var(--paper-ink);
}

.reasons__intro {
  max-width: 46rem;
  margin-bottom: clamp(2.5rem, 5vw, 3.5rem);
  color: var(--paper-muted);
  font-size: clamp(1rem, 2.4vw, 1.125rem);
  line-height: 1.65;
  text-wrap: pretty;
}

/* From 62em the section splits: reasons on the left, one tall photo holding
   the right. Below that the photo drops out entirely rather than stacking —
   a six-cell grid plus a full-width picture is a very long scroll on a phone,
   and the grid is what the section is for. */
.reasons__split { display: grid; }

/* Shown at every width. It reads as a header image on a phone and moves
   beside the reasons on desktop — hiding it below 62em left that section
   as six grey cells and nothing else. */
.reasons__photo {
  aspect-ratio: 16 / 10;
  margin-bottom: clamp(2rem, 4vw, 2.5rem);
  /* Inset by exactly however much the glow overshoots the gutter, so the bloom
     lands inside the viewport instead of being sliced by the overflow-x: clip
     on html/body. Symmetric, because in LTR the left overshoot is unreachable
     overflow and gets clipped too — both edges need the room.

     max() so this can never go negative: the element must never become WIDER
     than its column, which is what would put the sideways pan back. Resolves
     to 20px at 390px, ~2px at 800px, and exactly 0px from 900px up where
     --gutter becomes clamp(3rem, 10vw, 11rem). Desktop is therefore untouched
     by construction, including the sticky two-column layout below. */
  margin-inline: max(0px, calc(var(--glow-reach) - var(--gutter)));
  border-radius: 0.875rem;
  background-image: var(--reasons-img, none);
  background-color: var(--indigo-800);
  /* contain, not cover, on phones — see the mobile override below. */
  background-size: cover;
  background-position: center;
  box-shadow:
    0 0 0 2px rgba(255, 255, 255, 0.85),
    0 0 14px 2px rgba(124, 92, 255, 0.6),
    0 0 34px 8px rgba(124, 92, 255, 0.28);
}

.reasons__photo:not([style*="--reasons-img"]) {
  background-image:
    radial-gradient(72% 55% at 62% 30%, rgba(124, 92, 255, 0.5), transparent 72%),
    linear-gradient(158deg, var(--indigo-700), var(--indigo-950));
}

/* --- Phones: no photo at all ----------------------------------------------
   Hidden below 48em, by request. Every way of fitting it here was a trade:
   `cover` cropped the sides off a portrait shot, `contain` replaced the crop
   with empty bars, and the neon glow reaches 42px past the box against a 22px
   gutter, so on a phone it could only ever render as a cut edge.

   The section stands on its own without it — it is a six-cell numbered grid
   and the photo was a companion, not the content. Kept in the markup rather
   than removed so the desktop layout (where it is the tall sticky column
   beside the reasons) is untouched and nothing is lost from the page source. */
@media (max-width: 47.99em) {
  .reasons__photo { display: none; }
}

@media (min-width: 62em) {
  .reasons__split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.68fr);
    gap: clamp(2rem, 4vw, 3.5rem);
    align-items: start;
  }

  /* Tall and pinned beside the reasons — only the shape and position change
     from the mobile version above. */
  .reasons__photo {
    position: sticky;
    top: 7rem;                    /* clears the sticky header */
    aspect-ratio: 3 / 4;
    margin-bottom: 0;
  }
}

.reasons__grid {
  display: grid;
  gap: 1px;
  /* The 1px gap plus a background is the oldest grid-rule trick there is,
     and it beats borders here: borders double up between neighbours and
     leave the outer edge drawn on three sides at some breakpoints. This
     way every seam is exactly one pixel, however the columns reflow. */
  background: rgba(11, 7, 20, 0.12);
  border: 1px solid rgba(11, 7, 20, 0.12);
}

@media (min-width: 40em) {
  .reasons__grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 62em) {
  .reasons__grid { grid-template-columns: repeat(3, 1fr); }
}

.reason {
  padding: clamp(1.5rem, 3.5vw, 2.25rem);
  background: var(--paper);
}

/* Numbered rather than iconed. Icons for "professional game hosts" and
   "new games added regularly" would be two shrugging pictograms; a number
   says "there is a list of these" and gets out of the way. */
.reason__num {
  display: block;
  margin-bottom: 0.875rem;
  color: var(--accent-ink);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.18em;
}

.reason__title {
  margin-bottom: 0.625rem;
  font-size: clamp(1.125rem, 2.6vw, 1.3125rem);
  font-weight: 800;
  letter-spacing: -0.015em;
  line-height: 1.2;
}

.reason__body {
  color: var(--paper-muted);
  font-size: clamp(0.9375rem, 2.2vw, 1rem);
  line-height: 1.6;
  text-wrap: pretty;
}

.reasons__cta { margin-top: clamp(2.5rem, 5vw, 3.5rem); }


/* ==========================================================================
   9c-3d2. PHOTO RAIL — proof between the hero and the packages
   Reuses the .clips drift machinery (see clips.js); only the card shape and
   the section chrome differ.
   ========================================================================== */
.shots {
  padding-block: clamp(3.5rem, 8vw, 5.5rem);
  background: var(--indigo-950);
  color: var(--text);
}

/* Same size as every other section heading on the site. It used to run about
   half that, which made the photo rails read as a subsection of the block
   above them rather than as a stop of their own. Values match
   .section-title (main.css:1012) — if that moves, move this. */
.shots__line {
  margin-bottom: clamp(1.75rem, 4vw, 2.5rem);
  margin-left: -0.03em;
  font-size: clamp(2.9rem, 10vw, 4.5rem);
  font-weight: 900;
  line-height: 0.92;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}


/* Landscape cards, wider than the tall clip cards — two-and-a-bit in view on
   a phone, four-ish on a desktop. */
.clips--shots .clips__item--shot {
  flex: 0 0 clamp(15rem, 62vw, 24rem);
}

.shot {
  aspect-ratio: 4 / 3;
  border-radius: 0.875rem;
  background-image: var(--shot-img, none);
  background-size: cover;
  /* These cards are landscape and most of the photos are portrait phone
     shots, so `cover` throws away a lot of height. Set --shot-pos next to
     --shot-img to choose what survives — `center 20%` keeps faces in:
         style="--shot-img: url('…'); --shot-pos: center 20%" */
  background-position: var(--shot-pos, center);
  background-color: var(--indigo-800);
}

/* Placeholder wash until a real photo is set. Six hues so a rail of empty
   cards still reads as six distinct photos rather than one long block.
   :not() keeps these off any card that HAS a photo, so the real image wins
   without needing a specificity fight. */
.clips__item--shot:not([style*="--shot-img"]):nth-child(6n+1) .shot {
  background-image: linear-gradient(140deg, #7C5CFF, #241452);
}
.clips__item--shot:not([style*="--shot-img"]):nth-child(6n+2) .shot {
  background-image: linear-gradient(140deg, #FF2D77, #4A0E2A);
}
.clips__item--shot:not([style*="--shot-img"]):nth-child(6n+3) .shot {
  background-image: linear-gradient(140deg, #1FA97C, #123A2E);
}
.clips__item--shot:not([style*="--shot-img"]):nth-child(6n+4) .shot {
  background-image: linear-gradient(140deg, #FFC53D, #4A3410);
}
.clips__item--shot:not([style*="--shot-img"]):nth-child(6n+5) .shot {
  background-image: linear-gradient(140deg, #2FB6FF, #0E2E4A);
}
.clips__item--shot:not([style*="--shot-img"]):nth-child(6n+6) .shot {
  background-image: linear-gradient(140deg, #FF7A2F, #4A1B10);
}


/* ==========================================================================
   9c-3e. GIFT CARDS — 3D card hero, tier art, gift idea cards
   ========================================================================== */

/* The hero, the amounts and the gift ideas each painted their own ground —
   near-black, indigo, near-black — so three opaque boxes stacked drew two
   straight rules across the page, and a straight full-width edge is the one
   thing the eye never misses. The run gets ONE ground now, painted here; the
   three sections paint no colour of their own, and the indigo comes back as
   a wash that has faded to nothing before either of its own edges.

   The fine print stays outside — that break is meant to be hard. */
.giftflow { background: #05030A; }    /* near-black, warmer than pure #000 */

/* The wash straddles each seam — half of it in the section above, half in
   the section below — so the colour is already moving as it crosses the
   line, and both ends of the move land in padding instead of behind a
   heading. The four 0.5 stops are one number, the midpoint where the halves
   meet; move one and a line comes back. Every stop is --indigo-950's own
   channels at a different alpha, so if that token moves, move these.

   Lengths in rem, not percentages: the fade has to hold still against the
   edge it softens, and these boxes swing by hundreds of pixels between a
   phone and a desktop. The 16/84 stops make it a smoothstep — a straight
   ramp running into flat colour is a kink in the brightness curve, and the
   eye paints a Mach band along a kink. If stripes ever show, shorten the
   ramp: spreading the same few 8-bit steps over more pixels is what shows
   them. */
.giftflow .ghero {
  background:
    linear-gradient(to bottom,
      rgba(18, 10, 51, 0)    calc(100% - 5rem),
      rgba(18, 10, 51, 0.16) calc(100% - 2.5rem),
      rgba(18, 10, 51, 0.5)  100%);
}

.giftflow .plans--dark {
  background:
    linear-gradient(to bottom,
      rgba(18, 10, 51, 0.5)  0,
      rgba(18, 10, 51, 0.84) 2.5rem,
      var(--indigo-950)      5rem,
      var(--indigo-950)      calc(100% - 5rem),
      rgba(18, 10, 51, 0.84) calc(100% - 2.5rem),
      rgba(18, 10, 51, 0.5)  100%);
}

/* .gifts used to be the third section of the blend. The FAQ now sits between
   it and the amounts block, so the run is only ghero + amounts and .gifts is
   outside the wrapper carrying its own near-black again. The rule stays,
   scoped to the wrapper, so moving .gifts back inside restores the blend
   without touching CSS. */
.giftflow .gifts {
  background:
    linear-gradient(to bottom,
      rgba(18, 10, 51, 0.5)  0,
      rgba(18, 10, 51, 0.16) 2.5rem,
      rgba(18, 10, 51, 0)    5rem);
}

/* The run now ends at the amounts block, so its lower fade has nothing to
   fade into — square it off against the white FAQ below. */
.giftflow > .plans--dark:last-child {
  background:
    linear-gradient(to bottom,
      rgba(18, 10, 51, 0.5)  0,
      rgba(18, 10, 51, 0.84) 2.5rem,
      var(--indigo-950)      5rem);
}

.ghero {
  position: relative;
  isolation: isolate;
  padding: clamp(7.5rem, 17vw, 11rem) 0 clamp(4rem, 9vw, 6rem);
  background: #05030A;                 /* near-black, warmer than pure #000 */
  color: var(--text);
  overflow: hidden;
  text-align: center;
}

/* One soft violet bloom behind the card so it doesn't float on flat black. */
.ghero__glow {
  position: absolute;
  z-index: 0;
  top: 34%;
  left: 50%;
  width: min(52rem, 120%);
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(circle, rgba(124, 92, 255, 0.4), transparent 62%);
  filter: blur(28px);
}

.ghero__inner { position: relative; z-index: 1; }

.ghero .kicker--dark,
.ghero__title { text-align: center; margin-left: 0; }

.ghero__sub {
  max-width: 34rem;
  margin: 0 auto;
  color: var(--text-soft);
  font-size: clamp(1rem, 2.4vw, 1.1875rem);
  line-height: 1.6;
  text-wrap: pretty;
}

/* --- The 3D card ----------------------------------------------------------- */
.gcard__stage {
  perspective: 1100px;
  margin: clamp(2.5rem, 6vw, 4rem) auto 0;
  width: min(30rem, 92%);
}

.gcard {
  position: relative;
  aspect-ratio: 1.586;                 /* real credit-card ratio */
  transform-style: preserve-3d;
  /* gift.js writes --rx/--ry as you move; the transition only smooths the
     RETURN to rest, so tracking still feels immediate. */
  transform: rotateX(var(--rx, 8deg)) rotateY(var(--ry, -14deg));
  transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
  cursor: grab;
}

.gcard.is-live { transition: none; }

.gcard__face {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  padding: clamp(1.25rem, 4vw, 1.75rem);
  border-radius: 1.125rem;
  overflow: hidden;
  background:
    linear-gradient(135deg, #7C5CFF 0%, #5B3FD9 42%, #1B1140 100%);
  box-shadow:
    0 2px 0 rgba(255, 255, 255, 0.22) inset,
    0 30px 60px -18px rgba(0, 0, 0, 0.8),
    0 0 70px -20px rgba(124, 92, 255, 0.7);
}

/* Sheen that slides as the card turns — sold by the same --ry the card uses. */
.gcard__shine {
  position: absolute;
  inset: -40%;
  background: linear-gradient(
    115deg,
    transparent 38%,
    rgba(255, 255, 255, 0.32) 48%,
    rgba(255, 255, 255, 0.06) 56%,
    transparent 64%);
  transform: translateX(calc(var(--ry, -14deg) * 1.4));
  pointer-events: none;
}

.gcard__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.gcard__logo {
  font-size: clamp(1.125rem, 3.6vw, 1.5rem);
  font-weight: 900;
  letter-spacing: -0.03em;
  text-transform: uppercase;
}

.gcard__logo-mark { color: #C79BFF; }
.gcard__logo-word { margin-left: 0.14em; color: #fff; }

.gcard__chip {
  width: clamp(2rem, 6vw, 2.75rem);
  height: clamp(1.5rem, 4.5vw, 2rem);
  border-radius: 0.3125rem;
  background: linear-gradient(150deg, #FFD776, #E0A93C);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5) inset;
}

.gcard__label {
  margin-top: auto;
  color: rgba(255, 255, 255, 0.72);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.gcard__value {
  margin-top: 0.125rem;
  font-size: clamp(1.5rem, 5.5vw, 2.25rem);
  font-weight: 900;
  letter-spacing: -0.035em;
  line-height: 1;
  text-transform: uppercase;
}

.gcard__bottom {
  display: flex;
  justify-content: space-between;
  margin-top: 1rem;
  color: rgba(255, 255, 255, 0.66);
  font-size: 0.6875rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.ghero__hint {
  margin-top: 1.5rem;
  color: var(--text-muted);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

/* A card that lurches around is a nuisance, not a delight, for anyone who
   asked for less motion — it sits still at a nice angle instead. */
@media (prefers-reduced-motion: reduce) {
  .gcard { transition: none; }
  .ghero__hint { display: none; }
}

/* --- Dark variant of the plans grid ---------------------------------------- */
.plans--dark { background: var(--indigo-950); color: var(--text); }
.plans--dark .plan {
  border-color: rgba(255, 255, 255, 0.14);
  background: rgba(255, 255, 255, 0.03);
}
.plans--dark .plan--featured {
  border-color: var(--accent);
  background: rgba(124, 92, 255, 0.12);
}
.plans--dark .plan__flag { background: var(--accent); color: var(--ink-dark); }
.plans--dark .plan__price,
.plans--dark .plan__name { color: var(--text); }
.plans--dark .plan__per { color: var(--text-muted); }
.plans--dark .plan__item { color: var(--text-soft); }
.plans--dark .plan__item::before { color: var(--mint); }

/* --- Tier art -------------------------------------------------------------- */
.plan__art {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1.9;
  margin-bottom: 1.25rem;
  border-radius: 0.75rem;
  overflow: hidden;
  background-image: var(--gift-img, none);
  background-size: cover;
  background-position: center;
}

.plan__art::before {
  content: "";
  position: absolute;
  inset: 0;
}

.plan__art--a::before { background: linear-gradient(135deg, #7C5CFF, #2A1B63); }
.plan__art--b::before { background: linear-gradient(135deg, #FF2D77, #7C5CFF); }
.plan__art--c::before { background: linear-gradient(135deg, #FFC53D, #FF2D77); }

/* Once a real photo is set the gradient steps back to a scrim. */
.plan__art[style*="--gift-img"]::before {
  background: linear-gradient(180deg, rgba(11, 6, 26, 0.2), rgba(11, 6, 26, 0.62));
}

.plan__art-emoji {
  position: relative;
  font-size: clamp(1.75rem, 6vw, 2.5rem);
  line-height: 1;
}

.plan__art-amt {
  position: absolute;
  right: 0.75rem;
  bottom: 0.5rem;
  color: #fff;
  font-size: 1rem;
  font-weight: 900;
  letter-spacing: -0.02em;
}

/* --- The gift card replica ------------------------------------------------
   An emoji on a gradient did not read as a gift card — it read as a coloured
   box. This draws an actual card instead: rounded plastic, a bright sweep of
   gloss across it, the brand top-left and the amount bottom-right, exactly
   where they sit on a physical one. Sized as a fraction of its container so
   it scales with the card slot rather than needing its own breakpoints.

   Everything is drawn in CSS — no image request, and it re-colours per tier
   from the existing .plan__art--a/b/c gradients underneath. */
.giftcard {
  position: relative;
  display: block;
  width: 82%;
  aspect-ratio: 1.6;
  overflow: hidden;
  border-radius: 0.625rem;
  background: linear-gradient(145deg, rgba(255, 255, 255, 0.22), rgba(255, 255, 255, 0.04));
  box-shadow:
    0 8px 22px rgba(0, 0, 0, 0.45),
    inset 0 0 0 1px rgba(255, 255, 255, 0.34);
  /* The tilt is what makes it read as an object sitting on the tile rather
     than a panel painted onto it. */
  transform: rotate(-4deg);
}

/* The gloss. A single diagonal highlight, clipped by the card's overflow —
   the one detail that separates "glossy plastic" from "flat rectangle". */
.giftcard__shine {
  position: absolute;
  top: -60%;
  left: -30%;
  width: 55%;
  height: 220%;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.42), transparent);
  transform: rotate(22deg);
}

.giftcard__brand {
  position: absolute;
  top: 0.5rem;
  left: 0.625rem;
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.5625rem;
  font-weight: 900;
  letter-spacing: 0.1em;
}

.giftcard__brand em { font-style: normal; opacity: 0.72; }

/* The package name, which is the whole point of the replica — the card in the
   picture says what you are buying. */
.giftcard__name {
  position: absolute;
  bottom: 0.5rem;
  left: 0.625rem;
  max-width: 62%;
  color: #fff;
  font-size: clamp(0.75rem, 2.4vw, 0.9375rem);
  font-weight: 900;
  letter-spacing: -0.02em;
  line-height: 1.05;
  text-transform: uppercase;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

.giftcard__amt {
  position: absolute;
  right: 0.625rem;
  bottom: 0.4375rem;
  color: #fff;
  font-size: clamp(0.9375rem, 3vw, 1.25rem);
  font-weight: 900;
  letter-spacing: -0.03em;
  text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}

/* --- Kid-games filter ------------------------------------------------------
   Pill toggles above the birthday games rail. Sits on the dark ground, so the
   inactive state is an outline and the active one is solid white — the same
   inversion the reference uses, and the highest contrast available here. */
.kidfilter {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: clamp(1.5rem, 4vw, 2rem);
}

.kidfilter__btn {
  padding: 0.5625rem 1.125rem;
  border: 1.5px solid rgba(255, 255, 255, 0.3);
  border-radius: 999px;
  background: transparent;
  color: var(--text);
  cursor: pointer;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  transition: background-color 0.15s var(--ease), color 0.15s var(--ease),
              border-color 0.15s var(--ease);
}

.kidfilter__btn:hover { border-color: rgba(255, 255, 255, 0.6); }

.kidfilter__btn.is-active {
  border-color: #fff;
  background: #fff;
  color: var(--paper-ink);
}

/* --- Gift purchase: quantity stepper and paired fields -------------------- */
.gbqty {
  display: inline-flex;
  align-items: center;
  margin-top: 0.5rem;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  border-radius: 0.5rem;
  overflow: hidden;
}

.gbqty__btn {
  width: 2.75rem;
  height: 2.75rem;            /* 44px — a real touch target */
  border: 0;
  background: #fff;
  color: var(--paper-ink);
  cursor: pointer;
  font-size: 1.25rem;
  font-weight: 700;
  line-height: 1;
}

.gbqty__btn:hover { background: rgba(11, 7, 16, 0.05); }

.gbqty__val {
  min-width: 3rem;
  color: var(--paper-ink);
  font-size: 1rem;
  font-weight: 800;
  font-variant-numeric: tabular-nums;   /* width does not jump 9 -> 10 */
  text-align: center;
}

/* Delivery date and time sit together — they are one decision. */
.gbsplit {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.75rem;
}

@media (max-width: 26.5em) {
  .gbsplit { grid-template-columns: 1fr; }
}

/* The asterisk on genuinely required fields. */
.gbreq { color: #C81E4A; }

.cpay__val--soft {
  color: var(--paper-muted);
  font-size: 0.9375rem;
  font-weight: 700;
}

/* --- "Who is the gift card for?" ------------------------------------------ */
.giftfor {
  margin: 1.25rem 0 0;
  padding: 0;
  border: 0;
}

.giftfor__opts {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

/* The whole tile is the control, not just the dot beside it — a 2-option
   choice on a phone should have a thumb-sized target. */
.giftfor__opt {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0.625rem;
  border: 1.5px solid rgba(11, 7, 16, 0.16);
  border-radius: 0.5rem;
  background: #fff;
  cursor: pointer;
  font-size: 0.9375rem;
  font-weight: 700;
  text-align: center;
  transition: border-color 0.15s var(--ease), background-color 0.15s var(--ease);
}

/* The native radio is hidden but still focusable and still the source of
   truth — :has() styles the tile from the input's own checked state, so the
   control stays a real radio for screen readers and keyboards. */
.giftfor__opt input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.giftfor__opt:has(input:checked) {
  border-color: var(--violet-500);
  background: rgba(124, 92, 255, 0.1);
}

.giftfor__opt:has(input:focus-visible) {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

/* :has() is Safari 15.4+ / Chrome 105+. Below that the tile cannot show which
   option is selected, so show the real radio instead of hiding it — an
   unstyled but working control beats a styled one with no visible state. */
@supports not selector(:has(input)) {
  .giftfor__opt {
    justify-content: flex-start;
    gap: 0.5rem;
    text-align: left;
  }
  .giftfor__opt input {
    position: static;
    width: auto;
    height: auto;
    opacity: 1;
    pointer-events: auto;
  }
}

/* --- Gift idea cards ------------------------------------------------------- */
.gifts {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: #05030A;
  color: var(--text);
}

.gifts .section-title { text-align: center; margin-left: 0; }

.gifts__grid {
  display: grid;
  gap: 1.25rem;
  margin-top: clamp(2rem, 5vw, 3rem);
}

.gift {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.12);
  border-radius: 1rem;
  background: rgba(255, 255, 255, 0.03);
  transition: transform 0.2s var(--ease), border-color 0.2s var(--ease);
}

.gift:hover { transform: translateY(-4px); border-color: rgba(181, 123, 255, 0.6); }

/* Art block. Set --gift-img on the .gift to drop a real photo in; the
   gradient + emoji stand in until then. */
.gift__art {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  background-image: var(--gift-img, none);
  background-size: cover;
  background-position: center;
}

.gift__art::before { content: ""; position: absolute; inset: 0; }

.gift--a .gift__art::before { background: linear-gradient(140deg, #1FA97C, #123A2E); }
.gift--b .gift__art::before { background: linear-gradient(140deg, #7C5CFF, #241452); }
.gift--c .gift__art::before { background: linear-gradient(140deg, #FF7A2F, #4A1B10); }
.gift--d .gift__art::before { background: linear-gradient(140deg, #FF2D77, #4A0E2A); }
.gift--e .gift__art::before { background: linear-gradient(140deg, #FFC53D, #4A3410); }
.gift--f .gift__art::before { background: linear-gradient(140deg, #2FB6FF, #0E2E4A); }

.gift[style*="--gift-img"] .gift__art::before {
  background: linear-gradient(180deg, rgba(11, 6, 26, 0.15), rgba(11, 6, 26, 0.6));
}

.gift__emoji {
  position: relative;
  font-size: clamp(2.5rem, 9vw, 3.5rem);
  line-height: 1;
  filter: drop-shadow(0 6px 14px rgba(0, 0, 0, 0.45));
}

.gift__body { padding: 1.5rem 1.375rem 1.75rem; }

.gift__title {
  margin-bottom: 0.625rem;
  font-size: 1.0625rem;
  font-weight: 900;
  letter-spacing: -0.015em;
  line-height: 1.25;
}

.gift__text {
  color: var(--text-soft);
  font-size: 0.9375rem;
  line-height: 1.6;
  text-wrap: pretty;
}

.gifts__cta { margin-top: 2.5rem; text-align: center; }
.gifts__cta .btn { width: auto; min-width: 15rem; }

@media (min-width: 40em) {
  .gifts__grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 62em) {
  .gifts__grid { grid-template-columns: repeat(3, 1fr); gap: 1.5rem; }
}


/* ==========================================================================
   9c-3e2. WISH — video-background rotating hook
   ========================================================================== */
.wish {
  position: relative;
  isolation: isolate;
  display: grid;
  place-items: center;
  min-height: clamp(20rem, 52vw, 34rem);
  padding-block: clamp(4rem, 9vw, 7rem);
  overflow: hidden;
  background: var(--indigo-950);   /* shows while the video loads */
  color: #fff;
  text-align: center;
}

/* Bleeds 1px past every edge. The section's height comes from a vw clamp, so
   it regularly lands on a fractional pixel and an inset:0 child rounds down —
   which showed as a hairline of bare background along the top edge. .wish has
   overflow:hidden, so the overhang is clipped. */
.wish__video {
  position: absolute;
  inset: -1px;
  z-index: -2;
  width: calc(100% + 2px);
  height: calc(100% + 2px);
  object-fit: cover;
}

/* Keeps the words legible over any frame of the footage. Matches the video's
   1px bleed — if only one layer overhangs, the other one's short edge shows. */
.wish__scrim {
  position: absolute;
  inset: -1px;
  z-index: -1;
  background:
    linear-gradient(to bottom, rgba(8,5,14,0.6), rgba(8,5,14,0.45) 45%, rgba(8,5,14,0.72)),
    radial-gradient(80% 70% at 50% 45%, transparent, rgba(8,5,14,0.55));
}

.wish__lead {
  font-size: clamp(1.125rem, 3.4vw, 1.75rem);
  font-weight: 600;
  letter-spacing: -0.01em;
}

/* The big display line without the rotation — for pages that have one fixed
   statement instead of a cycling list. Same type as .wish__rotator so the
   two variants of the section read as the same component. */
.wish__line {
  margin-top: 0.25rem;
  color: var(--accent-lift);
  font-size: clamp(2.25rem, 8vw, 5rem);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  text-shadow: 0 2px 24px rgba(0, 0, 0, 0.45);
}


.wish__rotator {
  display: inline-grid;
  justify-items: center;
  margin-top: 0.25rem;
  font-size: clamp(2.25rem, 8vw, 5rem);
  font-weight: 900;
  line-height: 1;
  letter-spacing: -0.04em;
  text-transform: uppercase;
  /* the accent word glows just enough to lift off the footage */
  text-shadow: 0 2px 24px rgba(0,0,0,0.45);
}

/* All words share one grid cell, so the box is sized to the widest/tallest
   and nothing reflows as they swap. */
.rotator__word {
  grid-area: 1 / 1;
  color: var(--accent-lift);
  opacity: 0;
  transform: translateY(0.35em);
  transition: opacity 0.4s var(--ease), transform 0.4s var(--ease);
}

/* The first word ships with .is-on in the markup, so with JS off (or before
   it runs) it stays visible on its own — no separate fallback rule needed. */
.rotator__word.is-on {
  opacity: 1;
  transform: none;
}

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


/* ==========================================================================
   9c-3f. PILE — "the battle is real" photo strip
   Slim white band + a horizontal pile of overlapping, tilted photos that
   bleeds off both edges. Placeholder gradients until real photos land.
   ========================================================================== */
.pile {
  padding-block: clamp(3rem, 7vw, 4.5rem);
  background: var(--paper);
  color: var(--paper-ink);
  overflow: hidden;              /* the tilted pile runs past both edges */
}

/* .reviews follows and is also white; the fractional-pixel boundary let the
   dark body show through as a hairline. Overlap by 1px to close it — same
   colour, so invisible. (See .plans + .prose for the full reasoning.) */
.pile + .reviews { margin-top: -1px; }

/* Matches the site's standard .section-title scale so it reads at the same
   size as every other heading. */
.pile__title {
  font-size: clamp(2.9rem, 10vw, 4.5rem);
  font-weight: 900;
  line-height: 0.92;
  letter-spacing: -0.045em;
  text-transform: uppercase;
}

/* Full-bleed row. It's wider than the page and centred, so the outer photos
   are cut off by the screen edges — the "torn from a longer roll" look. */
.pile__strip {
  margin-top: clamp(3.5rem, 8vw, 5.5rem);
}

.pile__row {
  display: flex;
  justify-content: center;
  width: max-content;
  min-width: 100%;
  margin-inline: auto;
  padding-inline: 1rem;
}

/* Each photo is a tilted card with a thick white border and a soft drop —
   the classic "photo pile" polaroid look. Negative margin overlaps them. */
.pile__photo {
  flex: 0 0 auto;
  width: clamp(9rem, 22vw, 15rem);
  aspect-ratio: 4 / 5;
  margin: 0;
  margin-inline: -1.25rem;       /* overlap */
  padding: 0.5rem 0.5rem 1.75rem;/* fat bottom edge = polaroid */
  background: #fff;
  border-radius: 2px;
  box-shadow: 0 10px 26px -8px rgba(11, 6, 26, 0.45);
  background-clip: padding-box;
  transition: transform 0.2s var(--ease), box-shadow 0.2s var(--ease);
}

/* The image itself — gradient placeholder until --pile-img is set. */
.pile__photo::before {
  content: "";
  display: block;
  height: 100%;
  border-radius: 1px;
  background-image: var(--pile-img, none);
  background-size: cover;
  background-position: center;
}

.pile__photo:not([style*="--pile-img"]):nth-child(8n+1)::before { background-image: linear-gradient(140deg,#7C5CFF,#241452); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+2)::before { background-image: linear-gradient(140deg,#FF2D77,#4A0E2A); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+3)::before { background-image: linear-gradient(140deg,#1FA97C,#123A2E); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+4)::before { background-image: linear-gradient(140deg,#FFC53D,#4A3410); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+5)::before { background-image: linear-gradient(140deg,#2FB6FF,#0E2E4A); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+6)::before { background-image: linear-gradient(140deg,#FF7A2F,#4A1B10); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+7)::before { background-image: linear-gradient(140deg,#B25CFF,#2A1B63); }
.pile__photo:not([style*="--pile-img"]):nth-child(8n+8)::before { background-image: linear-gradient(140deg,#43EFBA,#123A2E); }

/* Alternating tilt so the row reads as a scattered pile, not a fan. Even
   cards sit slightly higher; the centre-ish ones lift so the middle of the
   strip is the focal point. */
.pile__photo:nth-child(odd)  { transform: rotate(-5deg); }
.pile__photo:nth-child(even) { transform: rotate(4deg) translateY(-0.75rem); }
.pile__photo:nth-child(3n)   { transform: rotate(-2.5deg) translateY(0.5rem); }
.pile__photo:nth-child(5)    { transform: rotate(3deg) translateY(-1.25rem); z-index: 2; }

.pile__photo:hover {
  transform: rotate(0) translateY(-0.5rem) scale(1.03);
  box-shadow: 0 18px 40px -10px rgba(11, 6, 26, 0.55);
  z-index: 3;
}

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


/* ==========================================================================
   9c-4. PROSE — the indexable text block
   ========================================================================== */
.prose {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--paper);
  color: var(--paper-ink);
}

/* On the party and team pages .prose follows .plans directly and both are
   white, so their vertical paddings stacked into a ~15rem void that read as
   a broken page rather than a section break. Same ground = one gap, not two.

   The -1px overlap is not cosmetic padding-fiddling: <body> is dark, and
   these sections' heights resolve from vw-based clamps, so at plenty of
   viewport widths the boundary lands on a fractional pixel and the browser
   leaves a sub-pixel seam — through which the dark body shows as a hairline
   right above the heading. Overlapping the two boxes by a pixel makes a gap
   impossible. They're the same colour, so the overlap itself is invisible. */
.plans + .prose {
  padding-top: 0;
  margin-top: -1px;
}

/* Exactly the same fault, in the pairs later sections created. .picker and
   .reviews are both white on the two product pages, and .pile and .reviews
   are both white on the home page, so each pair stacked ~8.5rem onto ~8.5rem
   and left a screen-deep hole in the middle of the page. Same -1px overlap,
   same reason: same ground, one gap. */
/* Every pair below changed ground once .reviews went dark and .faq went
   white on the product pages, so none of them can use the -1px overlap: an
   overlap between DIFFERENT colours is a hairline of the wrong one, and
   padding-top:0 jams the next heading against the edge it should clear.
   Different grounds separate themselves. They only need less air than two
   full paddings stacked — not none. */
.reviews--dark + .faq,
.faq + .close { padding-top: clamp(3rem, 7vw, 5rem); }

/* The action page's table-into-quotes pair gets more than the others: the
   picker ends on a scrollbar and a table row, so the reviews heading was
   landing right on top of it with only the ground change between them. */
.picker + .reviews--dark { padding-top: clamp(4rem, 9vw, 6rem); }

/* The escape page keeps its quotes white, so this pair is same-ground and
   COULD take the -1px overlap. It used to, and the result was the reviews
   heading sitting immediately under the room table with no air at all —
   technically seamless, visually cramped. Same ground means they do not need
   two full paddings, but they do need one: the table above ends in a row of
   data, and a heading has to read as the start of something new rather than
   another row. :not() keeps this off the action page, where the same two
   sections are white-then-dark and handled above. */
.picker + .reviews:not(.reviews--dark) {
  padding-top: clamp(3rem, 7vw, 4.5rem);
}

/* Kids-games sits on the same indigo as the FAQ block below it, so the two
   stacked ~8.5rem paddings into one dead screen. Same ground, one gap. */
/* Only when BOTH are dark. Once .seo--light went white this pair stopped
   sharing a ground, and a -1px overlap plus zero top padding jammed the
   heading against the dark block above it with no air at all. */
.kidgames + .seo:not(.seo--light) {
  padding-top: 0;
  margin-top: -1px;
}


/* White quotes into the dark comparison. The colour change does separate them,
   but "TRADITIONAL ESCAPE ROOMS VS VR ESCAPE ROOMS" is a four-line headline on
   a phone and it was starting almost immediately at the colour boundary — the
   heading needs room to land, not just a change of ground behind it. */
.reviews + .versus { padding-top: clamp(4rem, 9vw, 6rem); }
.versus + .faq     { padding-top: clamp(3rem, 7vw, 5rem); }

/* .pile is NOT in that list, deliberately. It ends in a full-bleed photo
   strip rather than in text, so it needs real space beneath it — collapsing
   the gap ran the strip straight into the next heading. Same ground, but the
   photos are the reason this pair is the exception. */

/* Dark essay into white FAQ: different grounds, so no overlap — the colour
   change is the separator and only needs enough air to breathe. Both sides
   were running clamp(4.5rem, 11vw, 8.5rem), which stacked to ~17rem. */
.prose + .faq { padding-top: clamp(3rem, 7vw, 5rem); }

/* .reasons and .prose have DIFFERENT grounds (white then dark), so this one
   cannot use the overlap trick — the -1px would show. The boundary is a
   colour change and reads on its own, so it just needs less air, not none. */
.reasons + .prose { padding-top: clamp(3rem, 7vw, 5rem); }

/* The things-to-do essay is the page's search-facing block and it sits
   between the white reasons grid and the dark FAQ. Dark ground ties it to
   the FAQ underneath and stops the page finishing on three white sections in
   a row. Scoped by id on purpose: the same .prose component stays white on
   the party and team pages, where it follows a white pricing table and the
   rule above depends on it. */
.prose#things-to-do {
  background: var(--indigo-950);
  color: var(--text);
}

.prose#things-to-do .section-title { color: var(--text); }

/* --accent clears 3:1 on the dark ground, so the heading accent goes back to
   the brighter lavender the dark sections use. */
.prose#things-to-do .section-title__accent { color: var(--accent); }

.prose#things-to-do .prose__h3 { color: var(--text); }
.prose#things-to-do .prose__body p { color: var(--text-soft); }
.prose#things-to-do .prose__body a { color: var(--accent); }

/* Capped at a readable measure even though the section is full width —
   this is the one block on the page meant to be read as paragraphs. */
.prose__body { max-width: 44rem; }

.prose__body p {
  margin-bottom: 1.5rem;
  color: #33303B;                /* 11.6:1 on white ✓ AAA */
  font-size: clamp(1rem, 2.3vw, 1.0625rem);
  line-height: 1.7;
  text-wrap: pretty;
}

.prose__h3 {
  margin: 2.5rem 0 0.875rem;
  font-size: clamp(1.1875rem, 3vw, 1.375rem);
  font-weight: 900;
  letter-spacing: -0.02em;
}

.prose__body a {
  color: var(--accent-ink);
  font-weight: 700;
}


/* ==========================================================================
   9d. FAQ — native details/summary accordions
   ========================================================================== */
/* White ground. The FAQ used to close the page on a third dark section in a
   row (prose, faq, footer), which made the whole bottom third read as one
   long block. On white it separates from both neighbours, and the footer's
   true black lands as a deliberate ending rather than another shade.

   Everything below is restated for the light ground. .faq__list--light still
   exists for the gift-cards fine print, which sits inside a .plans section
   rather than a .faq one and so does not inherit any of this. */
.faq {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--paper);
  color: var(--paper-ink);
}

/* The markup carries .section-title--dark on these headings, which sets the
   light-on-dark colour. Override it here rather than editing five pages. */
.faq .section-title--dark { color: var(--paper-ink); }
.faq .section-title__accent { color: var(--accent-ink); }

/* The accordion component is shared: .faq (white, five pages) and .seo
   (dark, the party and team pages) both use .faq__list. The component's own
   rules stay light-on-dark, which is what .seo needs; everything the WHITE
   section needs is restated here, scoped to .faq so it cannot reach .seo.
   Getting this wrong paints dark grey answers onto a dark background and
   the text simply disappears. */
.faq .faq__list { border-top-color: rgba(11, 7, 16, 0.14); }
.faq .faq__item { border-bottom-color: rgba(11, 7, 16, 0.14); }
.faq .faq__q::after { color: var(--violet-500); }
.faq .faq__a { color: #33303B; }              /* 11.6:1 on white AAA */
.faq .faq__a a { color: var(--accent-ink); }

.faq__list {
  max-width: 46rem;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}

/* Full width on desktop — the row rules should span the section, not sit in
   a narrow column off to one side. The ANSWER prose still gets a measure cap
   (below): 140-character lines are a genuine readability problem, and the
   accordion rows are what actually read as "full width". */
@media (min-width: 56.25em) {
  .faq .section-title,
  .seo .section-title { text-align: center; margin-left: 0; }

  .faq__list,
  .seo .faq__list { max-width: none; }

  .faq__q { font-size: 1.3125rem; padding-block: 1.5rem; }
  .faq__a { max-width: 62rem; font-size: 1.0625rem; }
}

.faq__item { border-bottom: 1px solid rgba(255, 255, 255, 0.14); }

.faq__q {
  position: relative;
  padding: 1.25rem 2.75rem 1.25rem 0;
  cursor: pointer;
  font-size: clamp(1.0625rem, 2.6vw, 1.1875rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  list-style: none;               /* removes the default triangle marker */
}

.faq__q::-webkit-details-marker { display: none; }

/* The +/× indicator. A plus drawn from two strokes so it can rotate into a
   close mark instead of being swapped out. */
.faq__q::after {
  content: "+";
  position: absolute;
  right: 0.25rem;
  top: 50%;
  color: var(--accent);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1;
  transform: translateY(-50%);
  transition: transform 0.2s var(--ease);
}

.faq__item[open] .faq__q::after { transform: translateY(-50%) rotate(45deg); }

.faq__a {
  margin: 0;
  padding: 0 2.75rem 1.5rem 0;
  color: var(--text-soft);
  font-size: 1rem;
  line-height: 1.65;
  text-wrap: pretty;
}

.faq__a a { color: var(--accent); }

/* Accordion list restated for a white section (gift cards fine print). */
.faq__list--light { border-top-color: rgba(11, 7, 16, 0.14); }
.faq__list--light .faq__item { border-bottom-color: rgba(11, 7, 16, 0.14); }
.faq__list--light .faq__a { color: #33303B; }
.faq__list--light .faq__q::after { color: var(--violet-500); }


/* ==========================================================================
   9d-2. GIFT CARD BALANCE CHECK
   One input at the foot of the gift card fine print. Someone with a card in
   a drawer wants one number, and asking them to phone for it loses the visit.

   Deliberately quiet: a bordered card rather than a full-width band, so it
   reads as a utility and does not compete with the buy buttons above it.
   ========================================================================== */
.gcbal {
  max-width: 34rem;
  margin: clamp(2.5rem, 6vw, 3.5rem) auto 0;
  padding: clamp(1.5rem, 4vw, 2rem);
  border: 1.5px solid rgba(11, 7, 16, 0.14);
  background: #FAF9FC;
}

.gcbal__title {
  font-size: 1.125rem;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: 0.01em;
}

.gcbal__sub {
  margin-top: 0.375rem;
  color: var(--paper-muted);
  font-size: 0.875rem;
  line-height: 1.55;
}

/* Visible label rather than a placeholder standing in for one: the
   placeholder here is a code format, and it has to stay on screen while the
   field is being typed into. */
.gcbal__label {
  display: block;
  margin-top: 1.25rem;
  margin-bottom: 0.375rem;
  font-size: 0.75rem;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
}

.gcbal__row {
  display: flex;
  gap: 0.5rem;
}

/* Codes are read off a phone screen and typed in a character at a time, so
   they get a monospaced-ish treatment: tabular figures and a wide letter
   space make a transposed character obvious before the submit. */
.gcbal__input {
  flex: 1 1 auto;
  min-width: 0;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.gcbal__btn { flex: none; }

.gcbal__out {
  margin-top: 0.875rem;
  padding: 0.75rem 0.875rem;
  border-left: 3px solid var(--paper-muted);
  background: #fff;
  font-size: 0.9375rem;
  line-height: 1.5;
}

/* Three outcomes, three colours, all measured on their own background.
   A balance is the good news the whole box exists to deliver, so it is the
   only one that gets weight. */
.gcbal__out[data-state="ok"] {
  border-left-color: #1B7A3E;
  background: #EDF7F0;
  color: #12532B;                /* 7.4:1 */
  font-weight: 700;
}

.gcbal__out[data-state="error"] {
  border-left-color: #B3261E;
  background: #FDECEA;
  color: #7A1C14;                /* 6.9:1 */
}

.gcbal__out[data-state="busy"] { color: var(--paper-muted); }

@media (max-width: 30em) {
  /* Side by side, the button squeezes the field down to about eight
     characters of a sixteen character code. Stacked is the only readable
     option once the button label is accounted for. */
  .gcbal__row { flex-direction: column; }
  .gcbal__btn { width: 100%; }
}


/* ==========================================================================
   9e. FOOTER
   ========================================================================== */
/* True black, not the indigo-black used elsewhere. Ending the page on pure
   black reads sharper than another shade of the section above it. */
.foot {
  padding-block: clamp(3.5rem, 8vw, 5.5rem) 2rem;
  background: #000;
  color: #fff;
}

.foot__grid {
  display: grid;
  gap: 2.5rem;
  margin-bottom: clamp(2.5rem, 6vw, 4rem);
}

.foot__tag {
  max-width: 22rem;
  margin-top: 1rem;
  color: rgba(255, 255, 255, 0.62);
  font-size: 0.9375rem;
  line-height: 1.6;
}

/* --- Socials --------------------------------------------------------------- */
.foot__social {
  display: flex;
  gap: 0.625rem;
  margin-top: 1.25rem;
}

/* Real brand marks rather than outlines — on a black footer, full-colour
   Instagram and YouTube glyphs are recognisable at a glance and stop the
   row reading as three identical grey circles. No ring around them: the
   glyphs carry their own shape. */
.foot__soc {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  transition: transform 0.15s var(--ease), opacity 0.15s var(--ease);
}

.foot__soc:hover { transform: translateY(-2px); }
.foot__soc:focus-visible { outline-offset: 6px; }

.foot__soc svg { width: 100%; height: 100%; display: block; }

.foot__head {
  margin-bottom: 0.875rem;
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent);
}

.foot__addr {
  margin-bottom: 0.625rem;
  font-style: normal;
  color: rgba(255, 255, 255, 0.82);
  font-size: 0.9375rem;
  line-height: 1.55;
}

.foot__hours {
  margin-top: 0.625rem;
  color: rgba(255, 255, 255, 0.55);
  font-size: 0.8125rem;
  line-height: 1.6;
}

.foot__nav {
  display: grid;
  gap: 0.5rem;
  margin-bottom: 1rem;
  list-style: none;
}

.foot__link {
  color: rgba(255, 255, 255, 0.82);
  font-size: 0.9375rem;
  text-decoration: none;
}

.foot__link:hover { color: #fff; text-decoration: underline; }

.foot__link--more {
  display: inline-block;
  margin-top: 0.625rem;
  color: var(--accent);
  font-weight: 700;
}

.foot__fine {
  padding-top: 1.5rem;
  border-top: 1px solid rgba(255, 255, 255, 0.14);
  color: rgba(255, 255, 255, 0.5);
  font-size: 0.8125rem;
  line-height: 1.6;
}

@media (min-width: 48em) {
  .foot__grid { grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 2rem; }
}


/* ==========================================================================
   10. TEASERS — birthdays + corporate
   ========================================================================== */
.teasers {
  padding-block: clamp(4.5rem, 11vw, 8.5rem);
  background: var(--indigo-950);
  color: var(--text);
}

/* Extra room under the heading — these are two big photographic blocks and
   they were crowding the title. */
.teasers__title { margin-bottom: clamp(4.5rem, 12vw, 7.5rem); }

.teasers__grid {
  display: grid;
  /* Stacked on phones, so the two cards need real separation or they read as
     one tall block. Side by side from 48em up, where a tighter gap is right. */
  gap: 3.5rem;
}

.teaser {
  position: relative;
  isolation: isolate;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: 24rem;
  padding: clamp(1.5rem, 5vw, 2.5rem);
  overflow: hidden;
  color: var(--text);
  text-decoration: none;
}

/* Photo slot. Drop an image in with --teaser-img on the .teaser__media:
       style="--teaser-img: url('../img/teasers/birthday.jpg')"
   The gradient below stands in until then, and doubles as the scrim
   once a photo exists. */
.teaser__media {
  position: absolute;
  /* -1px, not 0. The card sits in a grid track that often lands on a
     fractional pixel, so a photo sized with `cover` rounds down and leaves a
     hairline of section background showing at the right and bottom edges.
     Bleeding a pixel past every side removes the seam; .teaser's
     overflow:hidden clips the overhang. */
  inset: -1px;
  z-index: -2;
  background-image: var(--teaser-img, none);
  background-size: cover;
  /* Which part of the photo survives the crop. These cards are wide and short,
     so a tall shot loses a lot top and bottom — set --teaser-pos next to
     --teaser-img to pick what stays, e.g. `center top` to keep faces:
         style="--teaser-img: url('…'); --teaser-pos: center top" */
  background-position: var(--teaser-pos, center);
  transition: transform 0.4s var(--ease);
}

/* Same -1px bleed as .teaser__media. This layer is the scrim, so when
   fractional-pixel rounding leaves it short of an edge you get a *lighter*
   hairline — un-scrimmed photo — rather than a dark one. Both layers have to
   overhang or one of them shows a seam. */
.teaser::before {
  content: '';
  position: absolute;
  inset: -1px;
  z-index: -1;
}

/* Two different grades so the pair doesn't read as one repeated block —
   birthdays warmer/violet, corporate cooler/indigo. */
.teaser--birthday::before {
  background:
    linear-gradient(to top, rgba(11, 6, 26, 0.94) 12%, rgba(11, 6, 26, 0.35) 62%, transparent 100%),
    radial-gradient(80% 70% at 72% 22%, rgba(181, 123, 255, 0.55), transparent 70%),
    linear-gradient(158deg, var(--violet-500), var(--indigo-900));
}

.teaser--corporate::before {
  background:
    linear-gradient(to top, rgba(11, 6, 26, 0.94) 12%, rgba(11, 6, 26, 0.35) 62%, transparent 100%),
    radial-gradient(80% 70% at 72% 22%, rgba(67, 239, 186, 0.34), transparent 70%),
    linear-gradient(158deg, var(--indigo-600), var(--indigo-950));
}

/* Once a real photo is set, the bottom layer of those gradients has to go —
   it's fully opaque and would paint straight over the image. What stays is
   the scrim that keeps the copy readable plus a light colour tint, so the
   pair still reads warm-vs-cool rather than as one repeated block.

   --teaser-img is set on the .teaser itself rather than on .teaser__media so
   this attribute selector can see it; custom properties inherit, so
   .teaser__media still picks it up. */
/* Scrim only, essentially no colour. The violet radial was tinting the whole
   card purple over a photo that already has its own lighting in it. */
.teaser--birthday[style*="--teaser-img"]::before {
  background:
    linear-gradient(to top, rgba(8, 5, 14, 0.92) 12%, rgba(8, 5, 14, 0.42) 58%, rgba(8, 5, 14, 0.10) 100%),
    radial-gradient(80% 70% at 72% 22%, rgba(181, 123, 255, 0.10), transparent 70%);
}

.teaser--corporate[style*="--teaser-img"]::before {
  background:
    linear-gradient(to top, rgba(8, 5, 14, 0.92) 12%, rgba(8, 5, 14, 0.42) 58%, rgba(8, 5, 14, 0.10) 100%),
    radial-gradient(80% 70% at 72% 22%, rgba(67, 239, 186, 0.10), transparent 70%);
}

.teaser:hover .teaser__media { transform: scale(1.04); }

.teaser__kick {
  margin-bottom: 0.75rem;
  color: var(--accent);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}

.teaser--corporate .teaser__kick { color: var(--mint); }

.teaser__title {
  margin-bottom: 0.875rem;
  font-size: clamp(1.75rem, 6.5vw, 2.375rem);
  font-weight: 900;
  line-height: 1.03;
  letter-spacing: -0.035em;
  text-transform: uppercase;
}

.teaser__desc {
  max-width: 34ch;
  margin-bottom: 1.375rem;
  color: var(--text-soft);
  font-size: 1rem;
  line-height: 1.55;
  text-wrap: pretty;
}

/* Underline sits under the text only, not the arrow, so the arrow can
   slide on hover without dragging a rule with it. */
.teaser__link {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.teaser__arrow { transition: transform 0.2s var(--ease); }
.teaser:hover .teaser__arrow { transform: translateX(4px); }


/* --- Sleek text link -------------------------------------------------------
   The same shape as .teaser__link, promoted to its own class so it can be
   used outside a teaser card. It is a link that reads as an action without
   pulling the weight of a filled button: the rails already end in a wall of
   game cards, and a mint block under them would compete with the header CTA
   for the same eye. Arrow slides on hover, matching the teasers. */
.textlink {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  color: var(--accent-ink);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 800;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color 0.16s var(--ease);
}

.textlink:hover { color: var(--text); }

.textlink__arrow { transition: transform 0.2s var(--ease); }
.textlink:hover .textlink__arrow { transform: translateX(4px); }

/* On a white ground the lavender drops below the 4.5:1 body-text floor. */
.reasons .textlink,
.proof .textlink,
.pile .textlink { color: var(--accent-ink); }

/* Sits under a rail, so it needs to clear the cards without looking
   detached from them. */
.worlds__more { margin-top: clamp(1.25rem, 3vw, 1.75rem); }


@media (prefers-reduced-motion: reduce) {
  .textlink__arrow { transition: none; }
  .textlink:hover .textlink__arrow { transform: none; }
}

@media (min-width: 48em) {
  .teasers__grid { grid-template-columns: repeat(2, 1fr); gap: 1.5rem; }
  .teaser { min-height: 28rem; }
}




/* --------------------------------------------------------------------------
   11. SHORT VIEWPORTS  (landscape phones, small laptop windows)
   clamp() only scales against WIDTH, so a short-but-wide viewport would
   push the button and the price below the fold.
   -------------------------------------------------------------------------- */
/* --- 11a. Phones — fit one svh screen, without squashing anything ----------
   Why this is keyed to max-WIDTH and svh terms, not to max-height:

   In a media query, `height` resolves against the LARGE viewport (toolbars
   hidden) — the same reference 100vh uses. In portrait that number is ~647px
   on an iPhone SE, ~713px on a 13 mini, ~745px on a 14/15/16 and ~827px on a
   Plus. So the old `max-height: 44em` (704px) gate fired on the small phones
   and NEVER on the tall ones — which is exactly why the live phone and the
   desktop preview disagreed: the preview is tall enough that no squeeze fires,
   and a tall iPhone is too, but the phone has ~80px less USABLE height because
   of the toolbar. max-height simply cannot express "phone".

   So: device class comes from max-width, and real visible height comes from
   svh terms inside the min()/clamp() calls. svh is both stable and honest, so
   the type scales continuously with what is actually on screen, with no
   threshold to snap across and no reflow while scrolling.

   Each svh line is preceded by a vh fallback, per the convention used
   throughout this file — without it the whole declaration is dropped in a
   browser with no svh support.
   -------------------------------------------------------------------------- */
@media (max-width: 47.99em) {
  /* .btn--stack is the ONLY item in the hero stack with min-height: 0, which
     disables its automatic minimum size. In a column flex container that made
     it the only shrinkable item, so it absorbed 100% of any overflow and
     collapsed to its padding floor — a ~44% height loss — while the text
     blocks kept their full size. That is the "why is the button so skinny"
     report. It is never allowed to carry the overflow again; the sizes below
     are what give way instead. */
  .btn--stack { flex-shrink: 0; }

  .hero__inner {
    padding-top: calc(clamp(4.5rem, 12vh, 10.5rem)  + env(safe-area-inset-top));
    padding-top: calc(clamp(4.5rem, 12svh, 10.5rem) + env(safe-area-inset-top));
    padding-bottom: calc(1.75rem + env(safe-area-inset-bottom));
  }

  /* The base formula's 11.5svh guard was dead code: it resolved to ~76px
     against a 664px screen while the 15.5vw term resolved to ~60px, so the vw
     term always won and the headline never scaled down on a short phone.
     9.1svh is the largest value that makes the guard live without touching a
     390x844 viewport. */
  .hero__title {
    margin-bottom: clamp(0.75rem, min(3.6vw, 2vh), 1.5rem);
    margin-bottom: clamp(0.75rem, min(3.6vw, 2svh), 1.5rem);
    font-size: clamp(2.75rem, min(3.4rem + 3.1vw, 15.5vw, 9.1vh), 8.75rem);
    font-size: clamp(2.75rem, min(3.4rem + 3.1vw, 15.5vw, 9.1svh), 8.75rem);
  }

  .hero__sub {
    margin-bottom: clamp(0.875rem, min(4.2vw, 2.3vh), 1.75rem);
    margin-bottom: clamp(0.875rem, min(4.2vw, 2.3svh), 1.75rem);
    font-size: clamp(1rem, min(5.2vw, 2.6vh), 1.4375rem);
    font-size: clamp(1rem, min(5.2vw, 2.6svh), 1.4375rem);
    line-height: 1.4;
  }

  /* --- Brampton headline ONLY -----------------------------------------------
     Scoped to --bra, NOT to --city: Oakville's headline is correct as it is and
     must not move. "BRAMPTON'S" is the one word that overflows — at the 15.5vw
     set by .hero__title above it measures ~399px on a 390px screen. 12.2vw is
     the widest value that keeps it on one line from 320px up.

     Must stay AFTER the .hero__title rule above: the two score equal
     specificity (0,1,0) once --bra is on the same element, so source order is
     what decides, and the base city rule sits ~3300 lines earlier in the file.

     margin-top pushes the headline down so the gap under it (to the subhead)
     stops reading as a hole — .hero__inner is space-between, so height taken
     above the title comes straight out of the gap below it. */
  .hero__title--bra {
    margin-top: clamp(1.5rem, 7vw, 3.5rem);
    font-size: clamp(2rem, min(12.2vw, 9.1vh), 4.5rem);
    font-size: clamp(2rem, min(12.2vw, 9.1svh), 4.5rem);
  }


  /* Same closing margin as .hero__sub above, so the block below the facts sits
     exactly where it sat below the paragraph. The row height scales against
     svh too — on a short phone the paragraph would have wrapped to fewer
     effective pixels, so the facts have to give the same ground back rather
     than holding 150px while everything else shrinks around them. */
  /* Rows come down from 4.7svh to 3.6svh because a one-line .hero__sub--tight
     now sits above them. The pair together has to occupy what the old
     paragraph did — the sentence takes ~28px of that, so the three rows give
     back roughly the same. */
  .hero__facts {
    grid-auto-rows: clamp(2.125rem, min(6.6vw, 3.6vh), 2.75rem);
    grid-auto-rows: clamp(2.125rem, min(6.6vw, 3.6svh), 2.75rem);
    margin-bottom: clamp(0.875rem, min(4.2vw, 2.3vh), 1.75rem);
    margin-bottom: clamp(0.875rem, min(4.2vw, 2.3svh), 1.75rem);
  }


  .offer { margin-top: clamp(0.75rem, min(3.6vw, 2vh), 1.5rem); }
  .offer { margin-top: clamp(0.75rem, min(3.6vw, 2svh), 1.5rem); }
  .offer__label { margin-bottom: 0.375rem; }
  .offer__now { font-size: clamp(2.25rem, min(13.5vw, 6.5vh), 3.5rem); }
  .offer__now { font-size: clamp(2.25rem, min(13.5vw, 6.5svh), 3.5rem); }
}

/* --- 11b. Short phones (SE, 8, mini) --------------------------------------
   Paired with a phone width, 44em is exactly the right discriminator: it
   catches the SE/8/mini class and correctly leaves the 14/15/16 class to 11a
   alone. At ~553 visible px no type scale holds a four-line headline, a
   five-line subhead, a full-size button and a $30 price row — so the subhead
   gives, not the CTA. */
@media (max-width: 47.99em) and (max-height: 44em) {
  .hero__inner {
    padding-top: calc(clamp(3.5rem, 12vh, 10.5rem)  + env(safe-area-inset-top));
    padding-top: calc(clamp(3.5rem, 12svh, 10.5rem) + env(safe-area-inset-top));
    padding-bottom: calc(1.5rem + env(safe-area-inset-bottom));
  }

  .hero__sub {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

/* --------------------------------------------------------------------------
   11c. SHORT VIEWPORTS  (landscape phones, small laptop windows)
   clamp() only scales against WIDTH, so a short-but-wide viewport would
   push the button and the price below the fold.

   Unchanged from the original except for the min-width: 48em gate, which stops
   it reaching portrait phones — where it was overriding the subhead to a flat
   1rem and fighting 11a. (The original `(max-height: 44em), (max-height:
   44rem)` comma list was two identical conditions, since em and rem are both
   16px in a media query, so dropping one changes nothing.)
   -------------------------------------------------------------------------- */
@media (min-width: 48em) and (max-height: 44em) {
  .hero__inner {
    padding-top: calc(4.5rem + env(safe-area-inset-top));
    padding-bottom: calc(2rem + env(safe-area-inset-bottom));
  }
  /* No font-size override — the base rule's min(…, 12vh) already scales the
     headline down on short viewports, and doing it again here dropped the
     type to phone size on wide-but-short desktop windows. Spacing only. */
  .hero__title { margin-bottom: 0.875rem; }
  .hero__sub   { margin-bottom: 1.5rem; font-size: 1rem; line-height: 1.45; }
  .btn         { min-height: 52px; }
  .offer       { margin-top: 1.25rem; }
  .offer__now  { font-size: clamp(1.875rem, 6.5vh, 2.75rem); }
  .offer__now  { font-size: clamp(1.875rem, 6.5svh, 2.75rem); }
}

@media (min-width: 48em) and (max-height: 32em) {
  .hero__sub {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin-bottom: 1.125rem;
  }
}


/* --------------------------------------------------------------------------
   12. REDUCED MOTION
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }

  /* Kill the glitch outright rather than freezing it mid-burst — a
     near-zero duration would leave the offset layers stranded on an
     arbitrary frame. The neon glow stays; it isn't motion. */
  .glitch::before,
  .glitch::after {
    animation: none !important;
    opacity: 0 !important;
  }
}
