/* ==========================================================================
   StoreKit — Summer Release 2026
   Implemented from Figma: Summer Release 2026 / Desktop - 3 (node 18:2265)

   Approach: the Figma frame is absolutely positioned on a 1440px canvas.
   Collage regions ("stages") keep that absolute composition but are converted
   to container-query units (cqw), so 1cqw = 1% of the stage width and the whole
   arrangement scales proportionally at any viewport. Every coordinate below is
   the original Figma px ÷ 14.4. The tile grid is a real CSS grid so it reflows.
   ========================================================================== */

/* --- Fonts ----------------------------------------------------------------
   Attila Sans Classic for display, Residenz Grotesk for UI. Subset to Latin-1
   + Latin Extended-A + the punctuation and arrows the page uses, which takes
   the four faces from 198KB to 96KB. Latin Extended-A is kept deliberately:
   the lead form takes typed names, and a subset without it would tofu on
   José or Łukasz.

   Two things about these files worth knowing before editing:

   1. Each OTF declares its OWN family name — "Attila Sans Classic Black" is a
      separate family from "Attila Sans Classic Semibold", not a weight of one
      family. @font-face lets us re-declare them under a single family, which
      is what the shared `font-family` below is doing.

   2. The weights the faces report internally (800 / 600 / 900 / 645) are not
      the weights the CSS asks for (500, 600, 700, 900 for display; 400, 500,
      700 for UI). So each face claims a RANGE covering the requests nearest
      to it. Without that, a request for 700 display would find no face at 700
      and the browser would synthesise a fake bold — smeared, heavier than the
      real Black, and visibly wrong next to it.
--------------------------------------------------------------------------- */

/* Display — 500/600 land on Semibold, 700/900 on Black. */
@font-face {
  font-family: "Attila Sans Classic";
  src: url("assets/fonts/AttilaSansClassic-Semibold.woff2") format("woff2");
  font-weight: 400 650;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Attila Sans Classic";
  src: url("assets/fonts/AttilaSansClassic-Black.woff2") format("woff2");
  font-weight: 651 900;
  font-style: normal;
  font-display: swap;
}

/* UI — 400/500 land on Medium, 700 on Bold. */
@font-face {
  font-family: "Residenz Grotesk";
  src: url("assets/fonts/ResidenzGrotesk-Medium.woff2") format("woff2");
  font-weight: 400 550;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Residenz Grotesk";
  src: url("assets/fonts/ResidenzGrotesk-Bold.woff2") format("woff2");
  font-weight: 551 900;
  font-style: normal;
  font-display: swap;
}

:root {
  --black: #000;
  --white: #fff;
  --hairline: 1px solid var(--white);

  --font-display: "Attila Sans Classic", "Arial Black", "Helvetica Neue", Impact, sans-serif;
  --font-ui: "Residenz Grotesk", "Inter", -apple-system, BlinkMacSystemFont, "Helvetica Neue", Arial, sans-serif;

  /* The design is drawn in cqw off the stage width, so it scales with the
     viewport indefinitely — on a 27" display everything was ~70% oversized.
     This caps the layout at a 14" MacBook Pro's 1512px: below that it stays
     fluid as before, above it the page holds its size and centres on black. */
  --page-max: 1512px;

  --page-margin: 52px;   /* Figma left/right margin */
  --gutter: 38px;        /* tile gap, and the vertical rhythm between rows */

  /* 3 columns at the same 305.5px tile width the 4-column layout produced:
     (3 x 305.5) + (2 x 38) = 992.5. Everything that aligns to the grid — the
     tile grid and the section headers — reads from this. */
  --grid-width: 992.5px;

  /* How long the kiosk stage stays pinned while the build scrubs. This is the
     dial for the pace of the 120-frame sequence — bigger = slower scrub. */
  --kiosk-pin: 1000px;

  /* Feature carousel. All three cards share these, so one translate of
     (--card-w + --card-gap) advances exactly one slide. */
  --card-w: min(962px, calc(100vw - 48px));
  --card-h: min(640px, calc(100vh - 48px));
  --card-gap: 32px;
}

*, *::before, *::after { box-sizing: border-box; }

/* overflow-x here is not decoration: without it, body's overflow-x propagates
   up to the viewport and body itself is left `visible`, so nothing actually
   clips and the hero's bleed makes the page scroll sideways. Setting it on the
   root stops that propagation, leaving body to clip at its own box. */
html { background: var(--black); overflow-x: clip; }

/* Body deliberately stays viewport-wide and does the clipping; the cap is
   applied to each top-level block instead (see the rule below). That is what
   lets the hero collage bleed into the black margins on a wide display without
   ever producing a scrollbar — clip on the capped blocks would just cut it off
   at the page edge again. Note this needs overflow-x on BOTH html and body:
   body's alone propagates up to the viewport and leaves body itself `visible`,
   so nothing clips; the root's alone does not clip the viewport. Together, the
   root absorbs the propagation and body clips at its own viewport-wide box. */
body {
  margin: 0;
  background: var(--black);
  color: var(--white);
  font-family: var(--font-ui);
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden: `hidden` makes body a scroll container, which silently
     breaks `position: sticky` — the kiosk stage and One More Thing rely on it. */
  overflow-x: clip;
}

/* The cap sits on the blocks, not the body. Below --page-max they fill the
   viewport as before; above it they hold their size and centre on black. */
.stage--hero,
.features,
.omt,
.kiosk-pin { max-width: var(--page-max); margin-inline: auto; }

img { display: block; max-width: 100%; }


/* ==========================================================================
   STAGES — absolute collage regions, scaled via container query units
   ========================================================================== */

.stage {
  container-type: inline-size;
  position: relative;
  width: 100%;
  overflow: hidden;
}

/* Hero ends flush with the bottom of the wide halftone band (top 1544 + height
   630 = 2174) so the features section butts against the video. Was 2424 before
   the band was raised 250px to sit closer to the burger. */
/* Was trimmed to 2174 while the hero pinned — the phones ended 600px up, so the
   space below them was dead. Unpinned they sit where they are designed to, and
   the sub-text needs that room back: the Figma canvas is 2424, and the lede runs
   to 2446, so this is that plus a --page-margin's breathing room underneath.
   Still shorter overall than the old 2174 + 600px of runway. */
.stage--hero {
  aspect-ratio: 1440 / 2500;
  /* The collage is drawn to bleed past the canvas edges — the halftone bands and
     the phone renders all run off one side or another. Don't clip them to the
     stage: on a display wider than --page-max they spill into the black margins
     instead of being sliced at the page edge. The viewport clip on <html> is
     what stops them ever reaching a scrollbar. */
  overflow: visible;
}

/* Kiosk close: the Figma frame runs 1440 x 1582, but the page is cut to the
   bottom edge of the halftone band (band top 452 + height 630 = 1082), so the
   band is the last thing on the page and the kiosk render is clipped by it. */
.stage--kiosk { aspect-ratio: 1440 / 1082; }

.stage > * { position: absolute; }

/* Stacking order inside a stage — set explicitly so it doesn't rely on source
   order. Mirrors the Figma layer list, where the phone renders sit *above* the
   display type: halftone texture → type → product renders. */
.dither                    { z-index: 0; }
.hero-title, .hero-lede    { z-index: 1; }
.ph, .kiosk-frames         { z-index: 2; }


/* --- Hero phone renders --------------------------------------------------- */
/* Collage renders intentionally overflow the stage — opt out of the reset clamp */

/* All three renders are VP9-with-alpha videos, so they need display:block and a
   transparent background to composite over the halftone behind them. */
.ph {
  display: block;
  height: auto;
  max-width: none;
  background: transparent;
  pointer-events: none;
}

/* --phone-y is the parallax drift only — a few percent of the stage width as
   the hero scrolls past, written by JS. It defaults to 0, which is the designed
   composition, so the collage is correct with or without script.
   Translate is listed before rotate so each keeps its designed angle. */
/* menu-phone.webm is a 4:5 frame where the old square PNG was 1:1, and it
   carries much more empty space above the handset (12% of the frame vs 4.7%).
   These numbers are solved from the two assets' opaque bounding boxes so the
   phone itself renders at exactly the size and position it always did — a
   straight swap would have dropped it 127px at 1440. It does sit ~7.4cqw taller,
   which is the art, not the box: this shot shows more of the handset. */
.ph--2 {
  left: -15.690cqw; top: -9.274cqw; width: 88.807cqw;
  transform: translate3d(0, var(--phone-y, 0px), 0);
}

.ph--4 {
  left: 36.450cqw; top: 34.991cqw; width: 76.806cqw;
  transform: translate3d(0, var(--phone-y, 0px), 0) rotate(12.81deg);
}

/* Dropped 250px (at the 1440 canvas) from the Figma's 79.548 — deliberately
   runs into the halftone band down by the divider. */
.ph--3 {
  left: -2.049cqw; top: 96.909cqw; width: 72.835cqw;
  transform: translate3d(0, var(--phone-y, 0px), 0) rotate(-17.01deg);
}


/* --- Dithered halftone video bands ---------------------------------------
   Bands with real footage carry a looping <video>. Empty slots fall back to a
   CSS halftone so the composition still reads:
     <div class="dither dither--d"><video autoplay muted loop playsinline
        src="assets/dithered-video-4.mp4"></video></div>
-------------------------------------------------------------------------- */

.dither {
  pointer-events: none;
  background-color: var(--black);
  background-image:
    repeating-radial-gradient(circle at 0 0, #fff 0 1.1px, transparent 1.1px 4px),
    repeating-radial-gradient(circle at 2px 2px, #fff 0 .7px, transparent .7px 4px);
  background-size: 4px 4px;
  opacity: .5;
  overflow: hidden;
}

/* Real footage replaces the stand-in entirely — drop the pattern and the dim */
.dither:has(video) {
  background-image: none;
  opacity: 1;
}

.dither > video,
.dither > img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Don't crop the salad or the burger — show the whole frame and let the band's
   own black fill any remainder, which is invisible against the page.
   It only bites on the salad: that clip is a square 960x960 frame in a 1.742
   box, so `cover` was throwing away 43% of its height. The burger already
   matches its box (864x496 = 1.7419 vs 1.7419), so this is a no-op for it today
   and just stops a crop appearing if the footage is ever swapped.
   The two wide bands match their boxes too, so they keep `cover`. */
.dither--a > video,
.dither--b > video { object-fit: contain; }

/* Raised 260px so its rotated bbox clears the sub-text (which starts at y1043) */
.dither--a { left: -16.449cqw; top:  35.887cqw; width:  42.376cqw; height: 24.327cqw; transform: rotate(38.02deg); }
.dither--b { left:  63.623cqw; top:  92.070cqw; width:  50.890cqw; height: 29.215cqw; transform: rotate(-7.8deg); }
/* Pulled up 250px to close the black gap under the burger. The hero's
   aspect-ratio is shortened to match so the whole page moves up with it. */
/* Bottom edge flush with the stage bottom, which is where the TOP PICKS rule
   sits (the first section head is pulled up 21px and centres its 1px rule on
   the boundary). Was 107.222 — that put its bottom on 2174, the stage height
   before it was extended, which left a band of dead black underneath. */
.dither--c { left:  -1.042cqw; top: 129.861cqw; width: 102.083cqw; height: 43.750cqw; }
.dither--d { left:  -1.042cqw; top:  31.389cqw; width: 102.083cqw; height: 43.750cqw; }


/* --- Hero type ------------------------------------------------------------ */

/* Four real lines — STOREKIT / SUMMER / RELEASE / 2026 (node 40:795). STOREKIT
   occupies the 144px line the Figma left blank above SUMMER, so SUMMER still
   starts at y235 and the block runs y91–y667; centred, that puts it at y379. */
.hero-title {
  /* Fixed px, not cqw, so it lines up with the section headers and the sub-text
     at every viewport rather than drifting as the stage scales. */
  right: var(--page-margin);
  top: 26.319cqw;              /* 379 — four lines of 144, so SUMMER still starts at y235 */
  transform: translateY(-50%);
  width: 62.223cqw;            /* 896.01 */
  margin: 0;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 11.111cqw;        /* 160 */
  /* Leading is measured off the visible gap, not the line box: the caps stand
     102/128 of the .9 box, so the air between rows was 26px. Half of that is
     13px, i.e. a 115px box — .81. */
  line-height: .81;
  text-transform: uppercase;
  text-align: right;
  color: var(--white);
  word-break: break-word;
}

/* STOREKIT sets the measure — it is exactly 896px at 160px, which is where the
   Figma's 896.01 column width comes from. That leaves no slack, so nowrap keeps
   a fallback font overflowing leftward into empty space rather than tripping
   word-break and splitting the word mid-glyph. */
.hero-title span { display: block; white-space: nowrap; }

/* Moved to the left column, below the wordmark. */
.hero-lede {
  right: var(--page-margin);   /* right column, ranged with the wordmark above */
  /* Centred in the black gap under the burger. Measured off the footage rather
     than the band boxes, because both bands are halftone-on-black and their
     rects run well past their imagery: sampling every frame of both loops, the
     burger's lowest pixel in this column is y1762, and the wide band below is
     pure black for its first 279px so its imagery only starts at y2149. That
     leaves a 387px void; the copy sits centred in it, 85px clear either side.
     (The band rect does start at 1870, i.e. behind the lower half of this text
     — that is black video, and .hero-lede outranks .dither on z-index.) */
  top: 128.239cqw;             /* 1847 — top-anchored, not centred */
  width: 34.514cqw;            /* 497 */
  margin: 0;
  font-weight: 500;
  font-size: 1.204cqw;         /* 17.34 — matches the grid tile titles */
  line-height: 1.3;
  text-align: right;
  /* Full white, no opacity. It was .8, which over the halftone footage behind
     it read as grey rather than as white-dimmed — the band is already dark and
     busy, so the copy needs all the contrast it can get. */
  color: var(--white);
}

.hero-lede p { margin: 0 0 1.15em; }
.hero-lede p:last-child { margin-bottom: 0; }


/* ==========================================================================
   FEATURES — two labelled sections, six rows of three
   ========================================================================== */

/* Full-bleed so the section headers run the width of the page on the same
   --page-margin as the hero type. The grid inside keeps its own width. */
.features {
  margin: 0 auto;
  padding: 0 var(--page-margin);
}

/* --- Section header: label — rule — StoreKit mark ------------------------- */

.section-head {
  display: flex;
  align-items: center;
  height: 41px;
  margin: var(--gutter) 0;
  /* Sits above the hero stage — the first header overlaps the halftone band,
     and the stage's positioned children go up to z-index 2. */
  position: relative;
  z-index: 3;
}

/* The first header overlaps the halftone band so its rule sits right on the
   band's bottom edge — Figma: header y2403, rule y2423, band ends y2424.
   Both the offset and the header are fixed px, so the rule stays 1px above the
   stage bottom at every viewport width even as the stage scales. */
.features > .section-head:first-child { margin-top: -21px; }

.section-head__label {
  margin: 0 16px 0 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 18px;
  letter-spacing: 1.05px;      /* tracking scaled with the size */
  line-height: 1;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--white);
}

.section-head__rule {
  flex: 1 1 auto;
  height: 1px;
  margin-right: 12px;
  background: var(--white);
}

.section-head__mark {
  flex: 0 0 auto;
  width: 23.002px;
  height: 20px;
}


/* --- Tile grid ------------------------------------------------------------ */

/* Unchanged size, still centred — the section around it just got wider. */
.tile-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gutter);
  max-width: var(--grid-width);
  margin-inline: auto;
}

.tile {
  container-type: inline-size;
  position: relative;
  display: flex;
  flex-direction: column;
  aspect-ratio: 420 / 640;
  border: var(--hairline);
  border-radius: 12px;
  overflow: hidden;
  background: var(--black);
  text-decoration: none;
  color: inherit;
}

.tile__visual {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}

.tile__visual img,
.tile__visual video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.tile__heading {
  flex: 0 0 auto;
  /* Grown from 120/420 to fit the larger subtext plus the gap above it. At the
     old height a two-line tagline filled the box to within a pixel, so anything
     bigger clipped.

     Sized against the RENDERED tile, not the 420 reference. Tiles are ~306px
     wide at 1440 and ~300px on a phone carousel, so every cqw here lands at
     about 73% of its nominal value — which is why the subtext read as tiny at a
     nominal "16px". This is ~113px in practice, leaving ~5px of slack on the
     longest two-line tagline.

     The tile's aspect-ratio is fixed and .tile__visual is flex: 1 1 auto, so
     this comes out of the video rather than changing the grid. */
  height: 37cqw;
  /* Divider only. A full box border would double up on the tile's own border
     down the sides, and its square bottom corners would be sliced off by the
     tile's 12px radius + overflow clip. */
  border-top: var(--hairline);
  padding: 6.190cqw 7.143cqw;           /* 26 / 30 */
}

.tile__heading h3 {
  margin: 0;
  font-weight: 700;
  font-size: 5.714cqw;                  /* 24 / 420 */
  line-height: 1.2;
  color: var(--white);
}

.tile__heading p {
  /* Air between the title and the tagline. They were flush, which is what made
     the tagline read as an afterthought rather than a line worth reading. */
  margin: 2.6cqw 0 0;                   /* ~8px on a 306px tile */
  font-weight: 500;
  font-size: 5cqw;                      /* ~15px rendered, was ~11.6px */
  /* 1.2 is fine for a single line and cramped for two, and most of these wrap. */
  line-height: 1.35;
  color: var(--white);
}

.tile--link:focus-visible { outline: 3px solid var(--white); outline-offset: -6px; }

/* Hovering a tile softens every other one so the one you're on comes forward.
   Scoped to .features so tiles either side of the section divider behave the
   same. Guarded by (hover: hover) — on touch there is no hover to leave. */
@media (hover: hover) {
  .tile {
    transition: filter .25s ease, opacity .25s ease;
  }

  .features:has(.tile:hover) .tile:not(:hover) {
    filter: blur(4px);
    opacity: .55;
  }
}

@media (prefers-reduced-motion: reduce) {
  .tile { transition: none; }
}
/* ==========================================================================
   FEATURE OVERVIEW — carousel. Figma node 11:2057 for the card itself.

   Three identical full-size cards on one rail. Uniform widths mean a single
   translate of (card + gap) moves exactly one slide, so the strip shuffles
   continuously rather than one card leaving and another arriving.
   ========================================================================== */

.modal {
  width: 100vw;
  max-width: 100vw;
  height: 100vh;
  max-height: 100vh;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  overflow: hidden;
}

/* Explicit, because setting display here would override dialog:not([open]). */
.modal[open] { display: block; }
.modal:not([open]) { display: none; }

.modal::backdrop {
  background: rgba(0, 0, 0, .5);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* Wider than the viewport on purpose — the neighbours hang off both edges. */
.modal__rail {
  position: absolute;
  top: 50%;
  left: 50%;
  display: flex;
  align-items: center;
  gap: var(--card-gap);
  transform: translate(-50%, -50%);
  will-change: transform;
}

/* Close sits on the shell, not the card — one control, not three. */
.modal__close {
  position: absolute;
  top: 28px;
  right: 28px;
  z-index: 2;
  display: grid;
  place-items: center;
  width: 38px;
  height: 38px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  color: var(--white);
  cursor: pointer;
  transition: background-color .2s ease;
}

.modal__close:hover { background: rgba(255, 255, 255, .24); }
.modal__close:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }


/* --- The card ------------------------------------------------------------- */

/* Dimmed by default; .is-focus is the centre card. The focus class moves at the
   same moment the rail starts travelling, so the incoming card brightens *as*
   it arrives rather than after it has landed. */
.card {
  flex: 0 0 auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  width: var(--card-w);
  height: var(--card-h);
  background: var(--black);
  border-radius: 12px;
  overflow: hidden;
  opacity: .3;
  filter: blur(2px);
  cursor: pointer;
  transition: opacity .42s ease-out, filter .42s ease-out;
}

.card.is-focus {
  opacity: 1;
  filter: none;
  cursor: default;
}

.card:not(.is-focus) * { pointer-events: none; }
.card:not(.is-focus):hover { opacity: .5; filter: blur(1px); }

.card__content {
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
  padding: 32px;
}

/* TOP PICKS · 03 / 17 */
.card__meta {
  display: flex;
  gap: 10px;
  margin: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: .9px;
  text-transform: uppercase;
  color: var(--white);
  opacity: .55;
}

.card__title {
  margin: 26px 0 0;
  font-weight: 700;
  font-size: 18px;
  line-height: 1.2;
  color: var(--white);
}

.card__desc {
  margin: 24px 0;
  max-width: 327.89px;
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  font-weight: 500;
  font-size: 18px;
  line-height: 1.35;
  color: #f3f3f0;
  opacity: .8;
}

.card__cta {
  flex: 0 0 auto;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 44px;
  padding: 0 18px;
  border-radius: 6px;
  /* Glass: translucent white with a bright hairline edge. */
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .28);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
  color: var(--white);
  font-weight: 700;
  font-size: 16px;
  line-height: 1;
  letter-spacing: .4px;
  text-transform: uppercase;
  text-decoration: none;
  transition: background-color .2s ease, border-color .2s ease, transform .2s ease;
}

/* `display: inline-flex` above outranks the UA sheet's `[hidden] { display: none }`,
   so hiding the CTA on a tile with no destination has to be spelled out. */
.card__cta[hidden] { display: none; }

/* The waiting-list CTA is a <button>, which unlike <a href> does not get a
   pointer cursor for free. */
button.card__cta { cursor: pointer; }

.card__cta::after {
  content: "→";
  font-size: 15px;
  line-height: 1;
  transform: translateX(-2px);
  transition: transform .2s ease;
}

.card__cta:hover {
  background: rgba(255, 255, 255, .2);
  border-color: rgba(255, 255, 255, .45);
  transform: translateY(-1px);
}

.card__cta:hover::after { transform: translateX(2px); }
.card__cta:active { transform: translateY(0); }
.card__cta:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }

/* Named prev/next — a bare arrow doesn't say what's coming. */
.card__nav {
  display: flex;
  justify-content: space-between;
  gap: 16px;
  margin-top: 24px;
  padding-top: 18px;
  border-top: 1px solid rgba(255, 255, 255, .15);
}

.card__navbtn {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 13px;
  line-height: 1.2;
  text-align: left;
  opacity: .6;
  cursor: pointer;
  transition: opacity .2s ease;
}

.card__navbtn:hover { opacity: 1; }
.card__navbtn:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }
.card__navbtn--next { text-align: right; }

.card__navname { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.card__navarrow { flex: 0 0 auto; opacity: .7; }

.card__visual { overflow: hidden; }

.card__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Tiles that open the overview */
.tile[data-modal-title] { cursor: pointer; }
.tile[data-modal-title]:focus-visible { outline: 3px solid var(--white); outline-offset: -6px; }

/* Set by restoreFocus() when a dialog is closed after a pointer interaction:
   focus still returns to the trigger, but without the ring a mouse user has no
   use for. Removed on the first keypress or on blur. */
.is-focus-quiet:focus-visible { outline: none; }

/* "…more" toggle, Reels-style. Hidden unless the copy actually overflows. */
.card__more {
  display: none;
  align-self: flex-start;
  margin: -6px 0 14px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 14px;
  opacity: .6;
  cursor: pointer;
}

.card__more:hover { opacity: 1; }
.card__more:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }


/* --- Mobile: content-led ---------------------------------------------------
   The card stops being a fixed 640px box and sizes to what's in it. The video
   keeps its own aspect ratio instead of being cover-cropped into a slot, and
   the description clamps to two lines behind a "more" toggle.
--------------------------------------------------------------------------- */
@media (max-width: 760px) {
  :root {
    --card-w: calc(100vw - 32px);
    --card-gap: 16px;
  }

  .card {
    grid-template-columns: 1fr;
    /* max-content, not the default auto. With auto rows and a definite
       max-height the grid distributes the space it has rather than the space
       the rows need, so expanding the description squeezed the media row from
       429px to 390px — while .card__visual's aspect-ratio held the element at
       429px, spilling it 39px over the copy. Sizing rows to their content lets
       the card overflow and scroll, which is what overflow-y below is for. */
    grid-auto-rows: max-content;
    height: auto;                       /* content-led, not a fixed box */
    max-height: calc(100dvh - 32px);
    overflow-y: auto;
    align-content: start;
  }

  /* A fixed height, in viewport units rather than a ratio. It must not come
     from the video: an unloaded <video> reports 300x150, so `auto` made the
     box 2:1 until metadata landed and then snapped — the shrink mid-swipe.
     The sources are not consistent with each other either (720x900, 720x720,
     720x962), so `auto` also changed the card height card to card.

     svh, not a ratio, because the media should scale with the phone rather
     than with the card's width, and this is what lets the card reclaim the
     space the close button used to take above it. The cost is crop: see the
     note on .card__video below. */
  .card__visual {
    order: -1;
    aspect-ratio: auto;
    height: 60svh;
  }

  /* Over the media in the card's top-right, not above the card. Sitting on the
     shell it cost ~72px of vertical space before the card even started, on the
     one axis a phone has least of.

     The button lives on the shell, not in the card, so its offset has to be
     derived rather than guessed. The card is centred and its height is the
     60svh media plus ~220px of copy, so its top edge sits at
     (100svh - 60svh - 220) / 2 = 20svh - 110. Add ~16px to land inside the
     corner.

     Only the copy varies, and only by a line of wrapped title. Forcing the
     shortest and longest content the deck can produce moved the card top
     between 32px and 54px at 812, leaving the button 14-36px inside it, and
     it holds at 667 and 932 too. A fixed px top would have been correct at
     exactly one phone size. */
  #feature-modal .modal__close {
    top: calc(20svh - 94px);
    right: 28px;
    z-index: 3;
  }

  /* cover, not contain. The box has a fixed height so it cannot resize under
     the reader mid-swipe, but the sources are not all its shape — the 1:1
     clips like Toast and Uber Direct sat letterboxed inside it with black bars
     top and bottom. I had assumed those would disappear against the card's
     black; they do not, because the footage carries its own lighter background
     and the bars read as bars.

     cover fills the box instead, at the price of horizontal crop, and the
     60svh box above made that price higher: the box is now ~0.70 wide-to-tall,
     so a 4:5 source (most of the deck) loses ~12% of its width and a 1:1 source
     (Toast, Uber Direct) loses ~30% — 15% off each side. These compositions
     absorb it, because the logo is centred and generously margined in every
     one, but that is the number to lower 60svh against if any future clip
     crowds its frame.

     height: 100% rather than auto stays essential either way: with auto the
     element takes its height from the video's own ratio, which is 300x150
     until metadata lands, and it resizes mid-swipe. */
  .card__video {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* The copy sits on the card's own black, and a fade carries the image down
     into it rather than ending on a hard cut. It also means that if the copy
     ever does ride up over the media — a long description on a short viewport,
     say — it lands on a darkened area and stays readable rather than sitting
     on the photo. z-index puts the content, and so the fade, above the media
     row it reaches into. */
  .card__content {
    padding: 20px;
    position: relative;
    z-index: 1;
    background: var(--black);
  }

  .card__content::before {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    bottom: 100%;                       /* directly above the copy */
    height: 84px;
    background: linear-gradient(to top, var(--black), rgba(0, 0, 0, 0));
    pointer-events: none;
  }
  .card__title { margin-top: 18px; }

  .card__desc {
    flex: 0 0 auto;
    max-width: none;
    margin: 14px 0;
    overflow: visible;
    font-size: 16px;
  }

  .card.is-clamped .card__desc {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .card__more:not([hidden]) { display: block; }

  .card__nav { margin-top: 18px; }

  .modal__close { top: 16px; right: 16px; }
}

@media (prefers-reduced-motion: reduce) {
  .modal__rail, .card, .card__cta, .card__cta::after { transition: none; }
}
/* ==========================================================================
   ONE MORE THING — sticky beat before the kiosk reveal
   ========================================================================== */

.omt {
  position: relative;
  height: 240vh;               /* scroll runway for the reveal + fade */
  background: var(--black);
}

.omt__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
}

/* One word at a time. All three stack in the same box and cross-fade, so each
   fills the whole frame instead of taking a third of it: page-margin inset
   either side, 2vh top and bottom, and preserveAspectRatio="none" doing the
   vertical stretch. */
.omt__text {
  position: absolute;
  left: var(--page-margin);
  right: var(--page-margin);
  top: 2vh;
  bottom: 2vh;
  z-index: 0;                  /* behind the figure */
}

.omt__line {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transform: translateY(6%);
  transition: opacity .35s ease, transform .35s ease;
}

.omt__line.is-in {
  opacity: 1;
  transform: none;
}

.omt__line text {
  fill: var(--white);
  font-family: var(--font-display);
  font-weight: 900;
  font-size: 200px;
  text-transform: uppercase;
}

/* Sits in front of the type; JS drives the Y rise from scroll progress. */
.omt__figure {
  position: absolute;
  right: 2vw;
  bottom: 0;
  height: 100%;
  width: auto;
  max-width: none;
  z-index: 1;
  transform: translate3d(0, 100%, 0);
  will-change: transform;
  pointer-events: none;
}

@media (max-width: 760px) {
  /* The word is stretched to whatever box it sits in, and on desktop that box
     is the full stage — the figure rises through the type and the overlap is
     the composition. On a phone the figure is 72% of the stage and bottom
     anchored, so it covered everything below the 28% mark and only the tops of
     the letterforms survived. Confining the type to the top half gives it a
     box the figure cannot reach into as far, and it still reads as one collage
     because the head crosses the baseline. */
  .omt__text {
    top: 3vh;
    bottom: auto;
    height: 56vh;                       /* 47vh + 20% */
  }

  .omt__figure { height: 72%; right: -8vw; }
}

/* No scroll animation to sequence the words, so fall back to the stacked
   layout — all three readable at once, a third of the frame each. */
@media (prefers-reduced-motion: reduce) {
  .omt__text { display: flex; flex-direction: column; }
  .omt__line {
    position: static;
    flex: 1 1 0;
    height: auto;
    min-height: 0;
    opacity: 1;
    transform: none;
    transition: none;
  }
}


/* ==========================================================================
   KIOSKS — closing section
   ========================================================================== */

.stage--kiosk { margin-top: 54px; }     /* Figma: last row ends 6591, image at 6645 */

/* Pinned so the build has scroll to play over. The stage rides up as normal
   until its bottom meets the viewport bottom — at which point the whole
   composition is in frame — then sticks there while the runway scrolls past.
   That travel is what the frame scrub reads.
   The offset is (viewport height - stage height), so it is negative whenever the
   stage is taller than the window; JS writes it because only JS knows both at
   once. `bottom: 0` would be the wrong direction — that pins an element on the
   way IN and releases it after, which is the reverse of holding it. */
.kiosk-pin { position: relative; }
.stage--kiosk { position: sticky; top: var(--kiosk-stick, 0px); }
.kiosk-pin__runway { height: var(--kiosk-pin); }

/* --- Snap lock ------------------------------------------------------------
   The build only reads once the stage is framed, so the page catches there and
   hands the scroll to the scrub.

   `html:not(.lenis)` — this is the FALLBACK path. Lenis drives scrollTop from
   its own rAF loop, so the browser never sees the gesture that native snapping
   keys off; leaving `scroll-snap-type` on while Lenis runs gets the two fighting
   over the same scrollTop and the page stutters. Lenis adds `.lenis` to <html>
   as soon as it initialises, so this rule applies only when it did not — script
   blocked, or reduced motion, where Lenis bails on purpose. The Lenis path snaps
   in JS instead; see "Kiosk: snap lock" in index.html.

   `proximity`, never `mandatory`: the runway under the lock IS the scrub, and a
   mandatory container would fight it for every pixel and keep dragging the
   reader back onto the lock. Proximity engages only as a scroll comes to rest.
   `scroll-snap-stop: always` is what makes it a lock rather than a suggestion —
   a fling down the page has to stop here once, and can carry on after. */
html:not(.lenis) { scroll-snap-type: y proximity; }

.kiosk-pin__lock {
  position: absolute;
  left: 0;
  right: 0;
  top: var(--kiosk-lock, 0px);
  height: 1px;
  pointer-events: none;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

/* Scroll-scrubbed frame sequence. Full-bleed across the stage at the frames'
   native 16:9, so the whole composition is visible with no crop. */
.kiosk-frames {
  left: 0;
  top: auto;
  bottom: 0;                   /* sits flush with the end of the page */
  width: 100%;
  height: auto;
  aspect-ratio: 1920 / 1080;
  max-width: none;
  pointer-events: none;
}


/* ==========================================================================
   BOOT — the loading state
   ========================================================================== */

/* Locked while the overlay is up, so nothing scrolls behind it. Removed by JS
   on dismiss, and never applied at all if the script does not run.
   On <html>, deliberately: overflow:hidden on BODY would make body a scroll
   container and break the sticky hero and kiosk pin — the same trap the body
   rule near the top of this file calls out. html is the scrollport already. */
.is-booting { overflow: hidden; }

/* Background scroll lock while a dialog is open. On desktop Lenis is stopped
   and this is belt-and-braces; on mobile Lenis is not running, so this is the
   whole mechanism. On <html> for the same reason as .is-booting above. */
.is-modal-open { overflow: hidden; }

.boot {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: var(--black);
  opacity: 1;
  transition: opacity .65s ease;

  /* Failsafe. If the script is blocked, errored, or never ran, this still
     clears the overlay — a loader that can trap the whole page behind a JS
     failure is not worth having. JS normally wins long before 8s. */
  animation: boot-failsafe .65s ease 8s forwards;
}

@keyframes boot-failsafe { to { opacity: 0; visibility: hidden; } }

.boot.is-done { opacity: 0; pointer-events: none; }
.boot[hidden] { display: none; }

/* 2x the original. Bounded by height as well as width: the aspect is 4:3, so
   height = width x .75, and capping width at 106vh keeps the globe under 80vh.
   Without that third term a short window clips the top of the globe. */
.boot__stage {
  width: min(840px, 82vw, 106vh);
  aspect-ratio: 4 / 3;
  overflow: hidden;
}

.boot__globe {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* No filter: the source is already a white halftone globe on black, so it
     blends straight into the overlay with no visible edge. */
}

/* Reduced motion: the still poster stands in for the spin, and the overlay
   still clears on the same schedule. */
@media (prefers-reduced-motion: reduce) {
  .boot__globe { display: none; }
  .boot__stage { background: url("assets/globe-boot.jpg") center / cover no-repeat; }
}


/* ==========================================================================
   CTA DOCK — fixed, always on screen
   The only conversion point on the page, so it does not scroll away. Sits
   below the dialogs by definition: <dialog> renders in the top layer, which
   no z-index can reach past.
   ========================================================================== */

.cta-dock {
  position: fixed;
  right: var(--page-margin);
  bottom: 24px;
  z-index: 40;
  display: flex;
  gap: 12px;
}

.cta-dock__btn {
  display: inline-flex;
  align-items: center;
  height: 46px;
  padding: 0 20px;
  border-radius: 6px;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 15px;
  line-height: 1;
  letter-spacing: .4px;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color .2s ease, border-color .2s ease, transform .2s ease;
}

/* Secondary: the same glass as .card__cta. */
.cta-dock__btn--ghost {
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .28);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
  color: var(--white);
}

.cta-dock__btn--ghost:hover {
  background: rgba(255, 255, 255, .2);
  border-color: rgba(255, 255, 255, .45);
  transform: translateY(-1px);
}

/* Primary: inverted. The one solid white object on a black page — it should be
   the most findable thing on screen. */
.cta-dock__btn--solid {
  background: var(--white);
  border: 1px solid var(--white);
  color: var(--black);
}

.cta-dock__btn--solid:hover { transform: translateY(-1px); }
.cta-dock__btn:active { transform: translateY(0); }
.cta-dock__btn:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }

/* ==========================================================================
   SCROLL HINT — centre bottom, fixed like the dock
   Sits under the boot overlay (z 100) so it only appears once the page does,
   and under the dock (z 40) so it can never cover a conversion button. JS adds
   .is-hidden on the first real scroll; if the script never runs the hint just
   stays, which is harmless.
   ========================================================================== */

.scroll-hint {
  position: fixed;
  left: 50%;
  bottom: 22px;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  color: rgba(255, 255, 255, .8);
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 11px;
  letter-spacing: 3px;
  text-transform: uppercase;
  pointer-events: none;
  transition: opacity .45s ease, visibility 0s linear .45s;
}

/* Indent compensates the tracking after the last letter, so the word itself
   is what sits centred, not the word plus a trailing 3px space. */
.scroll-hint__label { text-indent: 3px; }

.scroll-hint__chevron { animation: scroll-hint-bob 1.6s ease-in-out infinite; }

@keyframes scroll-hint-bob {
  0%, 100% { transform: translateY(0); opacity: .55; }
  50%      { transform: translateY(5px); opacity: 1; }
}

.scroll-hint.is-hidden { opacity: 0; visibility: hidden; }

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

/* Waiting-list CTA on the kiosk build.

   Absolute, so it contributes no height: the snap lock is derived from the
   stage height, and anything in the flow here would shift the position it is
   timed against.

   visibility, not just opacity. A transparent button is still focusable, and
   this sits at the very end of the document where a stray Tab is most likely to
   reach it. The 0s-delayed visibility transition lets it fade OUT fully before
   being hidden, while appearing instantly on the way in. JS also toggles
   `inert`, which is what removes it from the accessibility tree. */
.kiosk-cta {
  position: absolute;
  left: 50%;
  bottom: 7%;
  z-index: 3;
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transform: translateX(-50%) translateY(10px);
  transition: opacity .45s ease, transform .45s ease, visibility 0s linear .45s;
}

.kiosk-cta.is-in {
  opacity: 1;
  visibility: visible;
  pointer-events: auto;
  transform: translateX(-50%) translateY(0);
  transition: opacity .45s ease, transform .45s ease, visibility 0s;
}

/* The kiosk render is a lit screen, so a white button can land on a bright
   area. The shadow lifts it off whatever is behind rather than relying on a
   scrim that would have to be positioned against the artwork. */
.kiosk-cta .cta-dock__btn {
  filter: drop-shadow(0 6px 24px rgba(0, 0, 0, .55));
  white-space: nowrap;
}

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

/* ==========================================================================
   LEAD MODAL — the form, in the feature card's clothes
   ========================================================================== */

/* No rail here, so the shell centres the card itself.
   Written as .modal--lead[open], not .modal--lead: the base sheet sets
   .modal[open] { display: block } and an attribute selector outranks a plain
   class, so the unqualified version loses and the card sits at the top. */
.modal--lead[open] { display: grid; place-items: center; }
.modal--lead:not([open]) { display: none; }

.card--lead {
  display: block;                   /* not the feature card's 1fr 1fr split */
  width: min(620px, calc(100vw - 32px));
  height: auto;
  max-height: min(760px, calc(100vh - 48px));
  opacity: 1;
  filter: none;
  cursor: default;
  border: var(--hairline);
}

/* The form pane scrolls, not the page behind it. */
.card--lead .card__content {
  padding: 36px;
  max-height: inherit;
  overflow-y: auto;
}

.lead__lede {
  margin: 14px 0 32px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 28px;
  line-height: 1.15;
  letter-spacing: -.4px;
  color: var(--white);
}

/* --- Step 1: segment ----------------------------------------------------- */

.lead-seg { margin: 0 0 26px; padding: 0; border: 0; }

/* Visually hidden, still announced — the visible question is carried by the
   two options themselves. */
.lead-seg__legend {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.lead-seg__opts {
  display: grid;
  grid-template-columns: 1fr;      /* stacked: the card is 620px, not a page */
  gap: 12px;
}

.lead-seg__opt {
  position: relative;
  display: block;
  padding: 18px 20px;
  border: var(--hairline);
  border-radius: 12px;
  background: var(--black);
  cursor: pointer;
  transition: background-color .2s ease, border-color .2s ease, transform .2s ease;
}

/* The radio drives state but is never seen. Kept in the layout (not
   display:none) so it stays focusable and shows up to assistive tech. */
.lead-seg__opt input {
  position: absolute;
  opacity: 0;
  width: 1px; height: 1px;
  margin: 0;
  pointer-events: none;
}

.lead-seg__body { display: block; }

.lead-seg__title {
  display: block;
  margin-bottom: 6px;
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 18px;
  line-height: 1.2;
  color: var(--white);
}

.lead-seg__note {
  display: block;
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 15px;
  line-height: 1.35;
  color: rgba(255, 255, 255, .62);
}

.lead-seg__opt:hover {
  background: rgba(255, 255, 255, .06);
  transform: translateY(-1px);
}

/* Selected: the same glass as .card__cta, so the chosen branch reads as the
   live control on the page. */
.lead-seg__opt:has(input:checked) {
  background: rgba(255, 255, 255, .12);
  border-color: rgba(255, 255, 255, .45);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
}

/* Focus lands on the hidden radio, so the ring has to be drawn on the label. */
.lead-seg__opt:has(input:focus-visible) {
  outline: 2px solid var(--white);
  outline-offset: 3px;
}

/* The chosen path, once the two option cards collapse. */
.lead-chosen {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 0 0 22px;
}

.lead-chosen[hidden] { display: none; }

.lead-chosen__label {
  font-family: var(--font-ui);
  font-weight: 700;
  font-size: 17px;
  line-height: 1.2;
  color: var(--white);
}

.lead-chosen__change {
  padding: 0;
  border: 0;
  background: none;
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 14px;
  line-height: 1.2;
  color: rgba(255, 255, 255, .55);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color .2s ease;
}

.lead-chosen__change:hover { color: var(--white); }
.lead-chosen__change:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }

/* --- Step 2: fields ------------------------------------------------------ */

.lead-fields {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 18px 20px;
}

/* display:grid on the parent outranks the UA sheet's [hidden] { display: none },
   the same trap as .card__cta[hidden]. */
.lead-fields[hidden] { display: none; }

.field {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0;
  grid-column: 1 / -1;          /* full width unless told otherwise */
}

.field--half { grid-column: span 1; }

.field[hidden] { display: none; }

.field__label {
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 14px;
  letter-spacing: .2px;
  text-transform: uppercase;
  color: rgba(255, 255, 255, .72);
}

.field__opt {
  text-transform: none;
  letter-spacing: 0;
  color: rgba(255, 255, 255, .45);
}

/* Same glass as .card__cta — translucent fill, bright hairline edge. */
.field__input {
  width: 100%;
  height: 52px;
  padding: 0 16px;
  border-radius: 6px;
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .28);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 16px;
  line-height: 1;
  transition: background-color .2s ease, border-color .2s ease;
}

.field__input::placeholder { color: rgba(255, 255, 255, .38); }
.field__input:hover { border-color: rgba(255, 255, 255, .45); }

.field__input:focus {
  outline: none;
  background: rgba(255, 255, 255, .18);
  border-color: var(--white);
}

.field__input:focus-visible {
  outline: 2px solid var(--white);
  outline-offset: 3px;
}

/* Error state. Colour is not the only signal — the message below carries it,
   which is what actually matters for anyone who cannot separate the two. */
.field__input[aria-invalid="true"] {
  border-color: #ff6b5a;
  background: rgba(255, 107, 90, .1);
}

.field__err {
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 14px;
  line-height: 1.3;
  color: #ff8f82;
}

.field__err[hidden] { display: none; }

/* Dial code + number as one control. The select is sized to its content so a
   long name like "United Kingdom (+44)" is readable, and the number takes the
   rest. */
.field__combo {
  display: flex;
  gap: 10px;
}

.field__combo .field__input { flex: 1 1 auto; min-width: 0; }

.field__prefix {
  flex: 0 0 auto;
  /* Just "🇬🇧 +44" now, so it needs a fraction of the width the country names did.
     Sized for the longest, "+852". */
  width: 116px;
  height: 52px;
  padding: 0 28px 0 12px;
  border-radius: 6px;
  background: rgba(255, 255, 255, .12);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, .28);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .18);
  color: var(--white);
  font-family: var(--font-ui);
  font-weight: 500;
  font-size: 16px;
  line-height: 1;

  /* The native arrow is drawn in the platform's own colour and disappears on
     black, so it is replaced with one that follows currentColor. */
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23fff' stroke-width='1.6' stroke-linecap='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  cursor: pointer;
  transition: background-color .2s ease, border-color .2s ease;
}

.field__prefix:hover { border-color: rgba(255, 255, 255, .45); }

.field__prefix:focus {
  outline: none;
  background-color: rgba(255, 255, 255, .18);
  border-color: var(--white);
}

.field__prefix:focus-visible { outline: 2px solid var(--white); outline-offset: 3px; }

/* The dropdown itself is drawn by the OS, which on a dark page would otherwise
   render white-on-white in some browsers. */
.field__prefix option { background: #111; color: var(--white); }

/* Honeypot: off-screen rather than display:none, which some bots skip. */
.lead-hp {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

/* --- Foot ---------------------------------------------------------------- */

.lead-foot {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  margin-top: 10px;
}

/* Solid white on black, matching the dock's "Get demo". Both are the primary
   action of their surface and should read as the same kind of object; the
   glass .card__cta it inherits is the secondary treatment used by the feature
   tiles, which left the form's only action looking optional. No arrow either,
   for the same reason — "Get demo" does not have one. */
.lead-submit {
  height: 52px;
  padding: 0 26px;
  background: var(--white);
  border-color: var(--white);
  color: var(--black);
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  box-shadow: none;
}

.lead-submit::after { content: none; }

.lead-submit:hover {
  background: var(--white);
  border-color: var(--white);
  transform: translateY(-1px);
}

/* Not filled in yet. Grey rather than white so the button reads as not-ready
   at a glance, which is the whole point — but it stays focusable and
   clickable, and is aria-disabled rather than disabled. A truly disabled
   submit is a dead end: it drops out of the tab order and tells someone
   nothing about which field it is waiting on. Pressing this one runs the
   normal validation path, so it answers that question instead. */
.lead-submit[aria-disabled="true"] {
  background: rgba(255, 255, 255, .14);
  border-color: rgba(255, 255, 255, .22);
  color: rgba(255, 255, 255, .45);
  cursor: not-allowed;
}

.lead-submit[aria-disabled="true"]:hover {
  background: rgba(255, 255, 255, .14);
  border-color: rgba(255, 255, 255, .22);
  transform: none;
}

/* In flight. This one IS disabled — there is nothing useful a second press
   could do while the request is open. */
.lead-submit[disabled] {
  opacity: .5;
  cursor: not-allowed;
  transform: none;
}

/* Names the feature when the form was opened from a waiting-list CTA. Sits
   between the lede and the segment choice, so the reader knows what they are
   joining before they are asked who they are. */
.lead__feature {
  /* .lead-chosen below has no top margin, so without a bottom one here the
     feature line sits flush against "I'd like a demo / Change". */
  margin: 10px 0 24px;
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 15px;
  line-height: 1.4;
  color: rgba(255, 255, 255, .62);
}

.lead__feature[hidden] { display: none; }

.lead-note,
.lead-status {
  margin: 0;
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 14px;
  line-height: 1.4;
  color: rgba(255, 255, 255, .5);
}

.lead-status { margin-top: 20px; }
.lead-status:empty { display: none; }
.lead-status[data-tone="error"] { color: #ff8f82; }

/* --- Success ------------------------------------------------------------- */

.lead-done[hidden] { display: none; }

.lead-done__title {
  margin: 0 0 10px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 34px;
  line-height: 1.15;
  letter-spacing: -.4px;
  color: var(--white);
}

.lead-done__note {
  margin: 0;
  max-width: 46ch;
  font-family: var(--font-ui);
  font-weight: 400;
  font-size: 17px;
  line-height: 1.45;
  color: rgba(255, 255, 255, .62);
}


/* ==========================================================================
   RESPONSIVE — the grid reflows; stages scale on their own
   ========================================================================== */

@media (max-width: 900px) {
  :root { --page-margin: 24px; --gutter: 24px; }
  .tile-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 560px) {
  .tile-grid { grid-template-columns: 1fr; }
}

/* ==========================================================================
   KIOSK ON MOBILE — full-bleed finale
   The desktop stage is a 1440x1082 landscape frame with the 16:9 build sitting
   along its bottom edge. Scaled to a portrait phone that leaves the kiosk about
   210px tall in an 812px viewport — a stamp at the bottom of a black screen.

   Here the stage takes the whole viewport and the frame sequence is sized to
   COVER it: height drives, width overflows, and the stage clips the sides.
   Losing the left and right of a 16:9 frame is the right trade — the kiosk is
   centred in the composition, so what gets cut is background.
   ========================================================================== */

@media (max-width: 760px) {
  .stage--kiosk {
    aspect-ratio: auto;
    /* svh, not vh: with vh the stage is sized for the browser chrome retracted,
       so the build sits partly under the address bar until the reader scrolls. */
    height: 100svh;
    margin-top: 0;               /* the 54px Figma offset is a desktop measure */
  }

  /* Sized against the KIOSK, not the frame. The kiosk occupies 53% of the
     frame's width and 85% of its height, so simply covering the viewport with
     the frame over-scales it — the kiosk ends up ~890px wide on a 375px screen
     and you see only its middle. 280vw puts the kiosk itself at about 1.5x the
     viewport width: imposing, clipped at the sides as intended, and roughly 62%
     of the viewport tall.

     bottom: 0, flush with the page end, same as the desktop rule. An earlier
     10svh lift kept the kiosk's base off the dock, but the stage, the pin and
     the halftone band all end at 0, so lifting only the scrub left a band of
     dead black under it. The dock floats over the base instead. */
  .kiosk-frames {
    left: 50%;
    right: auto;
    bottom: 0;
    width: 280vw;
    height: auto;
    transform: translateX(-50%);
  }

  /* Above the render on mobile, not below it. At the foot of the screen this
     button collided with the fixed CTA dock — the dock occupies 16-62px from
     the bottom and the button sat at 57-103px, a real 5px overlap, and visually
     they touched.

     The position is derived, not picked. The canvas is bottom-anchored at
     280vw wide with a 1920/1080 ratio, so it is 157.5vw tall and the empty band
     above it is (100svh - 157.5vw). Centring a 46px button in that band is the
     expression below, which holds as the band changes with BOTH axes — its
     height comes from the viewport height, its top edge from the viewport
     width. A percentage would only be right at one screen size: the band is
     221px at 375x812 but just 76px at 375x667. */
  .kiosk-cta {
    top: calc((100svh - 157.5vw - 46px) / 2);
    bottom: auto;
    /* Descends into place now that it enters from above. */
    transform: translateX(-50%) translateY(-10px);
  }

  .kiosk-cta.is-in { transform: translateX(-50%) translateY(0); }

  /* The halftone band is placed in cqw against the landscape stage, so at this
     height it lands as a thin strip across the middle. Pinned to the foot of
     the stage instead, behind the kiosk, which is where it reads. */
  .stage--kiosk .dither--d {
    left: 0;
    top: auto;
    bottom: 0;
    width: 100%;
    height: 46svh;
  }
}


/* ==========================================================================
   TILE GRIDS ON MOBILE — horizontal carousels
   Stacked one-per-row, 18 tiles is roughly 9,000px of vertical scroll on a
   phone. Each section becomes a snap carousel instead: the section keeps its
   own scroll axis, and the page itself gets ~2 screens shorter.

   Full-bleed via negative margins so tiles run off both edges rather than
   stopping at the page margin, with matching padding to keep the first tile
   aligned to the grid. Tiles are 80vw so the next one always peeks — that peek
   IS the affordance, which is why there are no arrows or dots.
   ========================================================================== */

@media (max-width: 760px) {
  .tile-grid {
    display: flex;
    gap: 14px;
    max-width: none;
    margin-inline: calc(var(--page-margin) * -1);
    padding-inline: var(--page-margin);
    overflow-x: auto;
    overflow-y: visible;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: var(--page-margin);
    /* Keeps a horizontal fling from chaining out to the page (or, on iOS, to
       browser back). */
    overscroll-behavior-x: contain;
    scrollbar-width: none;
  }

  .tile-grid::-webkit-scrollbar { display: none; }

  .tile {
    flex: 0 0 80vw;
    scroll-snap-align: start;
  }

  /* The blur-the-others hover trick is desktop-only, and on a touch device a
     sticky :hover would leave tiles blurred after a tap. */
  .features:has(.tile:hover) .tile:not(:hover) { filter: none; opacity: 1; }
}


/* ==========================================================================
   HERO ON MOBILE
   The desktop hero is an absolutely-positioned collage measured in cqw, so it
   scales with the viewport rather than reflowing. That works down to tablet and
   then falls apart: at 375px every cqw value is 26% of its designed size, which
   puts the lede at ~4.5px, and the phones sit on top of the type because their
   overlap was composed against a 1440px canvas.

   Below 760px the collage is therefore abandoned rather than shrunk. The stage
   becomes an ordinary grid — wordmark, a row of the three phones, then the
   copy — with type set in real units instead of container units.
   ========================================================================== */

@media (max-width: 760px) {
  /* Back to a collage, but composed for portrait rather than scaled down from
     the 1440 canvas. The renders are absolute again and placed in cqw (% of the
     stage width) so they hold their composition at any phone size, while the
     TYPE is set in px/clamp so it never shrinks with them — that split is the
     whole point, and it is what the old scaled-collage got wrong.

     Title and copy stay in flow, so the stage takes its height from the text
     instead of a fixed aspect-ratio that would clip or gap as the copy rewraps. */
  .stage--hero {
    aspect-ratio: auto;
    display: block;
    padding: 64px var(--page-margin) 40px;
  }

  .hero-title,
  .hero-lede {
    position: relative;                 /* in flow, but still stackable */
    left: auto;
    top: auto;
    right: auto;
    width: auto;
    transform: none;                    /* also drops the JS parallax offset */
    z-index: 1;                         /* under the renders, as on desktop */
  }

  /* The halftone bands are composed against the 1440 canvas, so on a phone they
     land across the type and over the TOP PICKS header below. Hidden here — the
     collage they belong to no longer exists at this width. */
  .stage--hero .dither { display: none; }

  .hero-title {
    font-size: clamp(38px, 15.4vw, 88px);
    line-height: .84;
    text-align: right;
  }

  /* Renders: left-heavy, bleeding well past the page margin, and riding over the
     last lines of the wordmark the way the desktop collage does.

     These widths are the VIDEO BOX, not the phone, and the gap between the two
     is why they look wrong. Each .webm carries a lot of transparent padding —
     the device fills only 49% / 44% / 52% of its frame width — and .ph--3/--4
     are rotated, which grows the on-screen bounding box again. A 124cqw box
     therefore renders a device about 198px wide on a 375px screen. Most of what
     hangs off the page edge is empty pixels.

     The three sets below are solved rather than eyeballed: the visible device
     bbox (rotation included) was measured against each box, giving the linear
     coefficients, and these values are the inverse for the target composition.
     Adjust them by re-deriving, not by nudging — the relationship between box
     and device is not 1:1 in either axis.

     .ph is z-index 2, so no extra stacking is needed to get them over the type. */
  .stage--hero .ph--2 { left: -29.6cqw; top: 34.6cqw;  width: 123.8cqw; transform: none; }
  .stage--hero .ph--4 { left: 49.1cqw;  top: 86.8cqw;  width: 115.7cqw; transform: rotate(10deg); }
  .stage--hero .ph--3 { left: -44.6cqw; top: 182.6cqw; width: 118.1cqw; transform: rotate(-9deg); }

  .hero-lede {
    /* Pushed clear of the two upper renders, then indented to sit alongside the
       map. Margins rather than absolute coords so the stage grows with the copy.
       The large top margin IS the extra spacing under the wordmark — it is what
       buys the renders the room to be this size. */
    margin-top: 127cqw;
    margin-left: 46cqw;
    z-index: 3;                         /* legible even if a render creeps over */
    /* Was 1.204cqw — about 4.5px on a 375px screen. */
    font-size: 15px;
    line-height: 1.4;
    /* Ranged right, matching desktop and the wordmark above it. */
    text-align: right;
    /* Was .85. Removed with the desktop one — mobile sets its own, so dropping
       only the desktop declaration would have left this reading grey. */
  }

  .hero-lede p + p { margin-top: .85em; }
}

@media (max-width: 760px) {
  /* The two-column field grid is KEPT here rather than collapsed to one.
     First/last name and the other pairs are short enough to share a row at this
     width, and stacking all six is what pushed the send button off screen. */
  .lead-fields { gap: 14px 12px; }
  .lead__lede, .lead-done__title { font-size: 24px; }
  .card--lead .card__content { padding: 26px 22px; }

  /* Dial code and number on one line. The select shrinks to its content —
     "🇬🇧 +44" needs far less than the 116px it is given on desktop. */
  .field__combo { flex-wrap: nowrap; gap: 8px; }
  .field__prefix {
    width: 104px;
    padding: 0 24px 0 10px;
    background-position: right 8px center;
  }

  /* Full-width dock so neither label truncates on a phone. */
  .cta-dock {
    left: 16px;
    right: 16px;
    bottom: 16px;
    gap: 10px;
  }
  .cta-dock__btn { flex: 1 1 0; justify-content: center; padding: 0 12px; }

  /* No hint on a phone. The dock owns the whole bottom edge, the lede text
     flows right through the centre where the hint would sit, and the mobile
     layout already cuts content at the fold — the desktop collage is the
     full-screen poster that needed the nudge. */
  .scroll-hint { display: none; }

}

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