/* ══════════════════════════════════════════════════════════════════════════
   app-v2.css — heala.fit, third edition.

   THE PRODUCT IS THE ARGUMENT. Everything on this page exists to get someone
   to a working Heala screen and then to a signup box. So the page is quiet
   and the phone is loud: one hue, one weight of shadow, generous air, and the
   only saturated colour outside the handset is the accent on a control you
   are meant to press.

   LIGHT BY DEFAULT as of 2026-08-28. The page was redrawn against a bright,
   airy reference: a tinted wash at the top of the hero falling to near-white,
   near-black ink, near-black pill buttons, and the violet reserved for state
   INSIDE the product rather than spent on every control. Dark is still a real
   second theme — every colour is a token, `:root` carries the day values and
   `[data-theme="dark"]` the night ones — and it keeps the silver handset.

   The phone replicas are NOT themed by this file. They ship the app's own
   light theme (app-ui.css, copied out of lib/theme.ts) and stay light in both
   site themes, because that is what the app actually looks like when you open
   it.
   ══════════════════════════════════════════════════════════════════════════ */

/* ── The display face ──────────────────────────────────────────────────────
   Nunito, variable 700–900, SIL Open Font License, self-hosted because the
   site CSP is `font-src 'self'` — and because a marketing page should not
   hand a third party a log line for every visitor. ONE file covers the whole
   weight range, so 800 and 900 cost nothing beyond the 700 already loaded.

   ⚠️ latin-ext IS NOT OPTIONAL. We ship en/tr/es, and Turkish lives there:
   ğ Ğ ı İ ş Ş are all outside the latin block. Drop that file and every
   Turkish headline silently falls back to the system face mid-word. */
@font-face {
  font-family: "Heala Display";
  src: url("assets/nunito-latin.woff2") format("woff2");
  font-weight: 700 900; font-style: normal; font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
                 U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
                 U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Heala Display";
  src: url("assets/nunito-latin-ext.woff2") format("woff2");
  font-weight: 700 900; font-style: normal; font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
                 U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF,
                 U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  --navh: 72px;
  color-scheme: light;

  --f-display: "Heala Display", ui-rounded, "SF Pro Rounded", system-ui,
               -apple-system, "Segoe UI", Roboto, sans-serif;
  --f-text: -apple-system, "SF Pro Text", system-ui, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;

  /* ── Day. The hero wash runs from a pale violet down to a near-neutral
     paper; everything below it is white or a hair off it. Cards are white on
     paper rather than light-grey on white, which is what keeps the page
     feeling open instead of quilted. ──────────────────────────────────── */
  --bg:      #FFFFFF;
  --bg-2:    #F4F2F9;
  --card:    #FFFFFF;
  --card-2:  #F7F5FC;
  --well:    #F1EFF6;

  --wash-1:  #E7DDFD;      /* top of the hero */
  --wash-2:  #F1EFF5;      /* where it lands */

  --text:    #141021;
  --sub:     rgba(20, 16, 33, .68);
  --mut:     rgba(20, 16, 33, .46);
  --line:    rgba(38, 20, 80, .10);
  --line-2:  rgba(38, 20, 80, .18);

  /* ── The accent is the app's own #6D28D9 and it is spent SPARINGLY: on
     state, on a ring, on a number that matters. The thing you press is
     near-black, because a page where everything is violet has no way left
     to say "this one". ────────────────────────────────────────────────── */
  --acc:      #6D28D9;
  --acc-2:    #8B5CF6;
  --acc-deep: #5B21B6;
  --acc-ink:  #FFFFFF;
  --acc-soft: rgba(109, 40, 217, .08);
  --acc-line: rgba(109, 40, 217, .26);
  --acc-glow: rgba(109, 40, 217, .20);

  /* The pressable near-black, and what sits on it. */
  --ink:      #17141F;
  --ink-2:    #2A2536;
  --on-ink:   #FFFFFF;

  --good:  #16A34A;
  --warn:  #D97706;

  --r:    22px;
  --r-lg: 30px;
  --w:    1200px;
  --nav:  68px;

  --shadow-1: 0 2px 10px rgba(38, 20, 80, .06);
  --shadow-2: 0 20px 46px -22px rgba(38, 20, 80, .20);
  --shadow-3: 0 46px 96px -44px rgba(38, 20, 80, .30);
}

[data-theme="dark"] {
  color-scheme: dark;

  --bg:      #17142B;
  --bg-2:    #1D1938;
  --card:    #262146;
  --card-2:  #2E2853;
  --well:    #100D20;

  --wash-1:  #241E44;
  --wash-2:  #17142B;

  --text:    #EFEAFF;
  --sub:     rgba(226, 218, 252, .72);
  --mut:     rgba(190, 178, 226, .58);
  --line:    rgba(178, 160, 240, .17);
  --line-2:  rgba(178, 160, 240, .30);

  /* On night the app's violet has nowhere near the contrast it has on paper,
     so the accent lifts — and the pressable near-black inverts to near-white,
     because a black pill on a dark page is a hole. */
  --acc:      #B49CFF;
  --acc-2:    #D3C4FF;
  --acc-deep: #7C5CE0;
  --acc-ink:  #100A1F;
  --acc-soft: rgba(180, 156, 255, .14);
  --acc-line: rgba(180, 156, 255, .34);
  --acc-glow: rgba(180, 156, 255, .40);

  --ink:      #F3EFFF;
  --ink-2:    #DCD3F5;
  --on-ink:   #17142B;

  --good:  #4ADE80;
  --warn:  #FBBF24;

  --shadow-1: 0 2px 8px rgba(0, 0, 0, .28);
  --shadow-2: 0 18px 44px -20px rgba(0, 0, 0, .65);
  --shadow-3: 0 44px 90px -40px rgba(0, 0, 0, .8);
}

/* ── Reset ───────────────────────────────────────────────────────────── */
* { box-sizing: border-box }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto } }
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--f-text);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: clip;
}
img, svg, video { max-width: 100%; display: block }
a { color: inherit; text-decoration: none }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer }
/* `hidden` is only a UA `display:none`, so ANY author rule that sets display
   silently outranks it and the element stays on screen. It has to win. */
[hidden] { display: none !important }
:focus-visible { outline: 2.5px solid var(--acc); outline-offset: 3px; border-radius: 8px }

.wrap { width: min(var(--w), calc(100% - 48px)); margin-inline: auto }
@media (max-width: 700px) { .wrap { width: calc(100% - 32px) } }
.sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden;
      clip: rect(0 0 0 0); white-space: nowrap; border: 0 }

/* ── Type ────────────────────────────────────────────────────────────── */
/* ⚠️ THE DISPLAY FACE IS ROUNDED AND ALREADY WIDE, so it wants LESS negative
   tracking than the system sans did, not more. The old -.042em was tuned for
   SF Pro; carried over unchanged it closed Nunito's counters and the headline
   turned into a dark bar. */
h1, h2, h3, h4 { margin: 0; font-family: var(--f-display); font-weight: 800;
                 letter-spacing: -.021em; text-wrap: balance }
h1 { font-size: clamp(38px, 5.4vw, 76px); line-height: .98; letter-spacing: -.026em; font-weight: 900 }
h2 { font-size: clamp(29px, 3.6vw, 50px); line-height: 1.03; letter-spacing: -.024em; font-weight: 900 }
h3 { font-size: clamp(20px, 1.8vw, 27px); line-height: 1.16 }
p  { margin: 0 }

.lede { color: var(--sub); font-size: clamp(15px, 1.05vw, 17.5px); line-height: 1.5; font-weight: 450 }
.kicker {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 11.5px; font-weight: 800; letter-spacing: .18em;
  text-transform: uppercase; color: var(--acc);
}
.kicker::before {
  content: ""; width: 18px; height: 1.5px; border-radius: 1px;
  background: currentColor; opacity: .5;
}
.tiny { font-size: 12.5px; line-height: 1.5; color: var(--mut); font-weight: 500 }

/* ══ BUTTONS ═════════════════════════════════════════════════════════════
   One primary. It is the loudest object on the page that is not a phone,
   and it moves when you touch it — a CTA that behaves like a surface people
   are used to pressing gets pressed. */
.btn {
  position: relative;
  /* CLIPPED. The shine below is parked outside the button until you hover it,
     and without this it simply sits on the page as a grey parallelogram next
     to every CTA. (It also cannot use a negative z-index to hide behind the
     button's own background — that hides the half that is *inside* and leaves
     the half that is outside.) box-shadow paints outside the border box and
     is not clipped by this, so the glow and the pulse still work. */
  overflow: hidden;
  display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  min-height: 54px; padding: 0 30px; border-radius: 999px;
  font-family: var(--f-display);
  font-size: 16.5px; font-weight: 800; letter-spacing: -.012em;
  /* ⚠️ NEAR-BLACK, NOT THE ACCENT. When every control was violet the page had
     no way left to say "this one" — and the violet then had to compete with
     the violet INSIDE the handsets, which is where it actually means
     something. The pill is the darkest thing on a pale page; that is the
     whole trick, and it is why the reference reads as calm. */
  background: var(--ink);
  color: var(--on-ink);
  box-shadow: 0 10px 26px -12px rgba(23, 20, 31, .55), inset 0 1px 0 rgba(255, 255, 255, .10);
  transition: transform .18s cubic-bezier(.2, .8, .3, 1), box-shadow .18s ease;
  cursor: pointer; white-space: nowrap;
}
.btn:hover { transform: translateY(-2px);
              background: var(--ink-2);
              box-shadow: 0 20px 40px -14px rgba(23, 20, 31, .55), inset 0 1px 0 rgba(255,255,255,.12) }
.btn:active { transform: translateY(0) scale(.985) }
.btn svg { width: 17px; height: 17px }
/* The shine — a light that crosses the button on hover, over the top of the
   label the way a real sheen does. Never on its own layer behind anything. */
.btn::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(105deg, transparent 34%, rgba(255,255,255,.20) 50%, transparent 66%);
  transform: translateX(-130%); transition: transform .7s cubic-bezier(.2,.8,.3,1);
}
.btn:hover::after { transform: translateX(130%) }
@media (prefers-reduced-motion: reduce) {
  .btn, .btn::after { transition: none }
  .btn:hover { transform: none }
  .btn:hover::after { transform: translateX(-130%) }
}
.btn--ghost {
  background: transparent; color: var(--text);
  box-shadow: inset 0 0 0 1px var(--line-2);
}
.btn--ghost:hover { background: var(--card); box-shadow: inset 0 0 0 1px var(--line-2) }
.btn--sm { min-height: 44px; padding: 0 22px; font-size: 15px }
.btn--wide { width: 100% }

/* A pulsing ring around the primary CTA. A SHADOW, not an element: the button
   clips its own children now, and a ring that lives outside the border box
   would be the first casualty. */
.btn--pulse { animation: cta-ring 3.4s cubic-bezier(.2, .7, .3, 1) infinite }
.btn--pulse:hover { animation: none }
@keyframes cta-ring {
  0%   { box-shadow: 0 10px 26px -12px rgba(23,20,31,.55), inset 0 1px 0 rgba(255,255,255,.10), 0 0 0 0 var(--acc-glow) }
  55%  { box-shadow: 0 10px 26px -12px rgba(23,20,31,.55), inset 0 1px 0 rgba(255,255,255,.10), 0 0 0 15px rgba(109, 40, 217, 0) }
  100% { box-shadow: 0 10px 26px -12px rgba(23,20,31,.55), inset 0 1px 0 rgba(255,255,255,.10), 0 0 0 0 rgba(109, 40, 217, 0) }
}
@media (prefers-reduced-motion: reduce) { .btn--pulse { animation: none } }

/* ══ NAV ═════════════════════════════════════════════════════════════════ */
/* ⚠️ NO GROUND OF ITS OWN AT THE TOP OF THE PAGE. A white bar sitting on the
   hero's violet wash is a seam, not a nav — the eye reads the join before it
   reads the headline. At scroll 0 the wash runs under it and the nav is only
   its contents; the plate and the hairline fade in together the moment the
   page moves, which is the only time a nav needs a ground. */
.nav {
  position: sticky; top: 0; z-index: 90;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: border-color .25s ease, background .25s ease;
}
.nav.is-solid {
  background: color-mix(in srgb, var(--bg) 78%, transparent);
  -webkit-backdrop-filter: blur(20px) saturate(150%);
  backdrop-filter: blur(20px) saturate(150%);
  border-bottom-color: var(--line);
}
/* Three tracks, and the middle one is centred on the PAGE rather than on
   whatever space the other two leave over — which is why it is a grid with
   two equal outer columns and not a flex row with margin-auto. */
.nav-in { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
          gap: 24px; min-height: var(--nav) }
.nav-right { display: flex; align-items: center; gap: 12px; justify-self: end }
/* min-height, not height: the wordmark is 32px of art in a 44px target,
   which is what an <a> in a nav has to be on a phone. */
.brand {
  display: flex; align-items: center; gap: 10px; flex: none;
  min-height: 44px; justify-self: start;
}
.brand img { width: 32px; height: 32px; border-radius: 9px }
.brand b { font-family: var(--f-display); font-size: 22px; font-weight: 900; letter-spacing: -.03em }
.nav-links { display: flex; gap: 30px; font-size: 15px; font-weight: 650 }
.nav-links a { color: var(--text); opacity: .74; transition: opacity .15s ease }
.nav-links a:hover { opacity: 1 }
@media (max-width: 940px) { .nav-links { display: none } .nav-in { grid-template-columns: 1fr auto } }

/* The language switch. Three letters, not a globe with a hidden menu: the
   whole point is that someone who lands in the wrong language can see the
   way out without opening anything. */
.lang-tog {
  display: inline-flex; padding: 3px; border-radius: 999px; flex: none;
  background: var(--card); border: 1px solid var(--line);
}
.lang-tog button {
  min-width: 38px; height: 40px; padding: 0 9px; border-radius: 999px;
  font-size: 12.5px; font-weight: 800; letter-spacing: .04em;
  color: var(--mut); transition: background .16s ease, color .16s ease;
}
.lang-tog button[aria-pressed="true"] { background: var(--acc-soft); color: var(--acc) }
@media (max-width: 1040px) { .lang-tog button { min-width: 32px; padding: 0 6px; font-size: 11.5px } }

/* The theme switch — a two-slot track, not a mystery icon. */
.theme-tog {
  display: inline-flex; padding: 3px; border-radius: 999px; flex: none;
  background: var(--card); border: 1px solid var(--line);
}
.theme-tog button {
  width: 44px; height: 40px; border-radius: 999px;
  display: grid; place-items: center; color: var(--mut);
  transition: background .16s ease, color .16s ease;
}
/* ⚠️ THE BUTTON ITSELF HAS TO BE THE TARGET. An earlier pass kept the pill at
   34 × 30 and grew a 44px ::after around it — which is invisible to
   getBoundingClientRect, to every accessibility audit, and to anyone reading
   the CSS later. The control is the size it claims to be. */
.theme-tog button svg { width: 15px; height: 15px }
.theme-tog button[aria-pressed="true"] { background: var(--acc-soft); color: var(--acc) }
@media (max-width: 560px) { .nav .btn--sm { display: none } }

/* ══ HERO ════════════════════════════════════════════════════════════════
   Centred, and the product is the stage. The argument is four short blocks
   down the middle — kicker, headline, one line of prose, the signup — and
   then five handsets fan out beneath it, deliberately running past the fold.
   A row of phones cut off by the bottom of the screen is an invitation to
   scroll; a row that fits is a picture of an app. */
.hero {
  position: relative;
  /* Pulled up under the transparent nav so the wash starts at the very top of
     the page, then padded back down by the nav's height plus real air above
     the headline. Without the negative margin the wash would begin at the
     nav's bottom edge and the seam would just move. */
  margin-top: calc(var(--nav) * -1);
  padding: calc(var(--nav) + clamp(26px, 4.4vh, 62px)) 0 0;
  overflow: clip;
  text-align: center;
  /* THE WASH IS THE WHOLE GROUND. It used to be two blurred accent clouds on
     a flat page, which reads as glow ON something; the reference is a single
     tint falling the height of the hero and landing on paper, which reads as
     air. It has to end ON --bg or the seam into the next section is visible
     as a band. */
  background: linear-gradient(180deg, var(--wash-1) 0%, var(--wash-2) 62%, var(--bg) 100%);
}
.hero-in {
  position: relative; z-index: 3;
  display: flex; flex-direction: column; align-items: center;
  /* Tight on purpose: every pixel spent here is a pixel of handset below the
     fold, and the handsets are the argument. */
  gap: clamp(11px, 1.1vw, 16px);
}
.hero-copy { display: contents }
/* ── The pre-launch overline ──────────────────────────────────────────
   ⚠️ NOT A PILL. The first cut was a small tinted lozenge with a pulsing
   dot, and it read as a cute badge — the kind of thing a product wears to
   look friendly, which is the opposite of a statement. This has to be the
   loudest quiet thing on the page: display type, heavy, wide tracking, rules
   running out to either side. A poster overline, not a sticker.

   It stays ONE line and sits above the headline, because the hero is
   deliberately three blocks and every pixel here is a pixel of handset
   below the fold. */
.hero-soon {
  display: flex; align-items: center; justify-content: center;
  gap: clamp(12px, 1.6vw, 20px);
  margin: 0; width: 100%;
  font-family: var(--f-display);
  font-size: clamp(15px, 1.9vw, 23px);
  font-weight: 900;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--acc);
  /* The tracking adds a space after the last letter that the rules would
     otherwise centre around, pulling the words visibly left. */
  text-indent: .3em;
}
/* Rules that fade OUT from the words, so the line reads as something
   stretching across the page rather than a boxed label. */
.hero-soon::before, .hero-soon::after {
  content: ''; height: 2px; border-radius: 2px; flex: 0 1 clamp(30px, 9vw, 104px);
  background: linear-gradient(90deg, transparent, var(--acc));
}
.hero-soon::after { background: linear-gradient(90deg, var(--acc), transparent) }
@media (max-width: 560px) {
  .hero-soon { font-size: 13px; letter-spacing: .26em; text-indent: .26em; gap: 10px }
}

/* ── The creature inside the replicas ──────────────────────────────────
   The dock button, the coach's header mark and its message avatars were
   `assets/pulse-fab.png` — the retired mascot — so the five handsets on the
   homepage were still advertising a character the app no longer has.

   ⚠️ --hp-size, NEVER A BARE WIDTH. `.hp` and a host slot are both one class
   and heala-pet.css loads last, so a plain width here loses at equal
   specificity and every one of them would render at the 96px default. */
.fab-pet  { --hp-size: 44px; display: block }
.mark-pet { --hp-size: 39px; display: block }
.av-pet   { --hp-size: 28px; display: block }

/* ── The launch clock ─────────────────────────────────────────────────
   Four cells, no colons. A colon between every pair is three more marks
   competing with the digits, and the labels already say what each number is.

   ⚠️ TABULAR FIGURES ARE NOT DECORATION HERE. Proportional digits change
   width as they tick, so the whole row jitters left and right once a second
   — the one animation on the page nobody asked for. */
.hero-clock {
  display: flex; align-items: flex-start; justify-content: center;
  gap: clamp(10px, 3vw, 22px);
  margin: 0; padding: 2px 0 0;
}
.hero-clock[hidden] { display: none }
.hero-clock span { display: grid; justify-items: center; gap: 2px; min-width: 3.1ch }
.hero-clock b {
  font-family: var(--f-display);
  font-size: clamp(30px, 8.2vw, 52px);
  font-weight: 900; line-height: .95; letter-spacing: -.03em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.hero-clock em {
  font-style: normal; font-size: 10px; font-weight: 800;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--mut);
}
/* The seconds are the only cell that moves every tick, so they carry the
   accent — it reads as a live thing rather than four static numbers. */
.hero-clock span:last-child b { color: var(--acc) }

/* The date, quiet. It is the answer to a question the clock cannot answer,
   not a second headline — so it sits at label size and label weight. */
.hero-when {
  margin: 6px 0 0; text-align: center;
  font-size: 11.5px; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; color: var(--mut);
}
.hero-when[hidden] { display: none }

.hero h1 { max-width: 17ch }
.hero h1 span { color: var(--acc) }
.hero .lede { max-width: 52ch }
/* One quiet line under the field, the way the reference carries one under its
   button. Anything more competes with the headline for the same glance. */
.join-anchor { display: block; height: 0 }
.hero-say { font-size: 14.5px; font-weight: 550; color: var(--mut) }

/* ── The signup, on the centre line ───────────────────────────────────── */
.hero .join-form { width: 100%; max-width: 560px; margin-top: 6px }
.hero .join-note { text-align: center }

/* ── The signup row ───────────────────────────────────────────────────────
   One control that happens to contain two: the input and the button share a
   pill, so it reads as a single thing to fill in rather than a form. */
.join-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 8px 8px 12px; border-radius: 999px;
  background: var(--card); border: 1px solid var(--line-2);
  box-shadow: var(--shadow-1);
  transition: border-color .18s ease, box-shadow .18s ease;
}
.join-row:focus-within {
  border-color: var(--acc-line);
  box-shadow: 0 0 0 4px var(--acc-soft);
}
.join-input {
  flex: 1; min-width: 0; padding: 14px 8px 14px 12px;
  background: none; border: 0; outline: none;
  font: inherit; font-size: 17px; font-weight: 500; color: var(--text);
}
.join-input::placeholder { color: var(--mut) }
.join-row .btn { flex: none; padding: 15px 26px; font-size: 17px }

/* ⚠️ OFF SCREEN, NEVER `display: none`. A hidden input is not filled in by
   the bots this exists to catch, and without any rule at all it is simply a
   second visible text box in the signup form. */
.join-trap { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0 }

/* ── The count and the two tiers, small ───────────────────────────────────
   Under the field, above the deck: what is on offer, what it costs to wait,
   and how far gone it is — in one line and a hairline bar. It used to be two
   tall cards, then a line below the handsets; up here it is read, and it is
   still the smallest type in the hero. */
/* ── Already on the list ──────────────────────────────────────────────────
   The field's replacement, same width and same weight so the hero does not
   jump when it swaps in. */
/* ⚠️ NOT A PILL. It borrowed the signup field's 999px radius, and at three
   lines of text that is not a pill any more — it is a stadium with corners so
   round the copy has to sit in the middle of a lot of nothing. The field is
   one line and can be a pill; this is a receipt. */
.joined {
  width: 100%; max-width: 470px;
  padding: 16px 20px 14px; border-radius: var(--r-lg);
  background: var(--card); border: 1px solid var(--line);
  box-shadow: var(--shadow-1);
  text-align: left;
  display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 2px 12px;
}
/* The tick gets its own column and spans the rows, so the text block sets its
   own left edge instead of being centred around an icon. */
.joined-hd { grid-column: 2 }
.joined > svg, .joined-tick {
  grid-row: 1 / span 2; align-self: start; margin-top: 2px;
  width: 26px; height: 26px; border-radius: 999px;
  display: grid; place-items: center;
  background: var(--acc-soft); color: var(--acc);
}
.joined-tick svg { width: 14px; height: 14px }
.joined-tx { grid-column: 2 }
.joined-code { grid-column: 2; justify-self: start }
.joined-undo { grid-column: 2; justify-self: start; margin-left: -8px }
.joined-hd {
  font-family: var(--f-display); font-size: 16.5px; font-weight: 900;
  letter-spacing: -.02em; color: var(--text);
}
.joined-tx { margin-top: 3px; font-size: 13px; line-height: 1.5; color: var(--sub) }
.joined-tx b { color: var(--text); font-weight: 750 }
.joined-code {
  margin-top: 10px; display: inline-block;
  padding: 5px 13px; border-radius: 999px;
  background: var(--acc-soft); border: 1px solid var(--acc-line);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 13.5px; font-weight: 700; color: var(--acc); letter-spacing: .02em;
}
.joined-undo {
  display: block; margin-top: 8px; padding: 4px 8px;
  font-size: 12.5px; font-weight: 600; color: var(--mut);
  text-decoration: underline; text-underline-offset: 2px;
}
.joined-undo:hover { color: var(--text) }
/* A pill this tall needs to become a card once the text wraps. */
@media (max-width: 560px) { .joined { padding: 14px 16px 12px } }

/* ── The offer, on a plate the pet holds up ───────────────────────────────
   ⚠️ IT HAS TO BE AN OBJECT. Loose centred text cannot be carried — the pet's
   paws would meet nothing and it would read as reaching, not holding. A card
   with an edge is a thing with a weight, and the two paws meet its underside.
   Its width is capped so those paws land inside it rather than past its
   corners, which is the difference between carrying and balancing. */
.tiers {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; align-items: center; gap: 13px;
  width: 100%; max-width: 380px; margin-top: 12px;
  padding: 18px 22px 20px; border-radius: var(--r-lg);
  background: var(--card); border: 1px solid var(--line);
  box-shadow: var(--shadow-2);
}
.tiers-live {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 15px; font-weight: 600; color: var(--sub);
}
.tiers-live b { font-weight: 850; color: var(--text); font-variant-numeric: tabular-nums }
.tiers-live i {
  width: 8px; height: 8px; border-radius: 99px; background: var(--acc);
  box-shadow: 0 0 0 0 var(--acc-glow); animation: live-dot 2.4s ease-out infinite;
}
@keyframes live-dot {
  0%   { box-shadow: 0 0 0 0 var(--acc-glow) }
  70%  { box-shadow: 0 0 0 8px transparent }
  100% { box-shadow: 0 0 0 0 transparent }
}
@media (prefers-reduced-motion: reduce) { .tiers-live i { animation: none } }

.tiers-set { display: inline-flex; gap: 10px; flex-wrap: wrap; justify-content: center }
.tier-chip {
  display: inline-flex; align-items: baseline; gap: 8px;
  padding: 9px 16px; border-radius: 999px; white-space: nowrap;
  border: 1px solid var(--line-2); background: var(--card-2);
  font-size: 14.5px; font-weight: 600; color: var(--mut);
  transition: opacity .2s ease, border-color .2s ease;
}
.tier-chip b {
  font-family: var(--f-display);
  font-size: 20px; font-weight: 900; letter-spacing: -.03em; color: var(--sub);
}
/* The one you can still have. */
.tier-chip.is-live {
  border-color: var(--acc-line); background: var(--acc-soft); color: var(--sub);
}
.tier-chip.is-live b { color: var(--acc) }
/* The one you cannot. Struck, not just dimmed — a faded chip reads as
   "coming later", which is the opposite of what it means. */
.tier-chip.is-gone { opacity: .5 }
.tier-chip.is-gone b { text-decoration: line-through }

/* Can only ever be corrected DOWNWARD by the truth. */
.tier-bar {
  display: block; height: 5px; border-radius: 3px; overflow: hidden;
  background: var(--line-2); width: 100%; max-width: 300px;
}
.tier-bar i { display: block; height: 100%; border-radius: 3px; background: var(--acc) }

.hero-tail {
  display: flex; flex-direction: column; align-items: center; gap: 13px;
  margin-top: 4px;
}

/* ⚠️ A SHORT WINDOW IS THE COMMON CASE, NOT THE EDGE ONE. The --s-row ladder
   below shrinks the deck as the viewport shortens, but the copy above it kept
   its full size — so on a 1280 x 720 window the argument filled the screen and
   the deck started below the fold entirely. The whole point of this hero is
   that you can see the product without scrolling, so the TEXT gives way
   first: type steps down, gaps close, and the parts that are nice to have go
   before the parts that are not. The signup field never goes. */
@media (max-height: 960px) {
  .hero h1 { font-size: clamp(34px, 4.3vw, 62px) }
  .tiers { gap: 11px; padding: 15px 20px 17px }
}
@media (max-height: 860px) {
  .hero { padding-top: calc(var(--nav) + 20px) }
  .hero-in { gap: 10px }
  .hero h1 { font-size: clamp(31px, 3.6vw, 50px); max-width: 26ch }
}
@media (max-height: 770px) {
  .hero h1 { font-size: clamp(28px, 3vw, 40px) }
  .hero-say { display: none }
  .hero-in { gap: 8px }
  .tiers { gap: 7px }
}
@media (max-height: 680px) {
  .hero { padding-top: calc(var(--nav) + 12px) }
  .hero h1 { font-size: 30px }
  /* The chips are the offer; the running total is the nice-to-have. */
  .tiers-live { display: none }
}

/* ══ THE DECK ═════════════════════════════════════════════════════════════
   Five handsets fanned like a hand of cards, and the pet standing behind the
   middle one.

   It was an arc first — outer pair turning toward a shared vanishing point
   under a 2200px perspective AND scaling down, which reads as five objects at
   different distances. Then a flat even row, which reads as a shelf. A deck
   is neither: same-size cards, overlapping, tilted about the edge they pivot
   on. No perspective, no scale, no opacity fade.

   ⚠️ EVERY SLOT IS A REAL REPLICA, and every replica costs WebGL contexts.
   See the note on the markup before changing which screens are here. */
.fan {
  position: relative; z-index: 1;
  /* ⚠️ THE PAWS HAVE TO CLEAR THE PLATE'S BOTTOM EDGE. Pulled up any further
     they end up INSIDE it — the plate is opaque, so the grip disappears and
     the pet is just standing under a card. Here the paw tops tuck about 10px
     under the lip and the rest of the paw shows below it, which is what
     holding something looks like. A share of --pet-w, not a pixel count,
     because the pet changes size at every breakpoint. */
  margin-top: calc(var(--pet-w) * .048);
  /* ⚠️ THE ROW RESERVES THE PET'S BAND. Without this the pet is absolutely
     positioned into space the layout does not know about, so it climbs over
     the signup field — and it does it differently at every viewport height,
     because the amount it overhangs scales with --s-row. */
  padding-top: calc(var(--pet-w) * .55);

  /* ⚠️ TWO CONSTRAINTS, AND THE SMALLER ONE WINS. Height decides how big the
     handsets CAN be (--s-fit, from the ladder below); width decides how big
     they may be before five of them and four gaps run off the page
     (--s-cap). As a fanned deck they overlapped and width never bound; flat,
     a row is 5.4x one handset, and under about 1320px wide that is the
     limit. Both are plain numbers, so min() picks between them. */
  --s-fit: .849;
  --s-cap: .790;
  --s-row: min(var(--s-fit), var(--s-cap));
  /* The space between two handsets, as a share of one's width. */
  --ph-gap: .17;
  /* ⚠️ THE BOX IS NOT THE CREATURE. The drawing fills 0.72 of its own 200pt
     box across and 0.75 down — horns to feet with air on every side — so a
     box set to the phone's width draws a pet two-thirds of it, which reads as
     a keyring rather than something standing there. 1.45 of the phone width
     puts the FUR at about 1.06 of it. */
/* ⚠️ AND IT STOPS. The band this reserves is 0.6 of the pet's width, so a pet
     tied to the handsets forever means every pixel the handsets gain, the
     band takes back more than half of. Past 300px the pet is big enough for
     anyone; the cap is what lets the deck keep growing. */
  --pet-w: min(calc(var(--ph-w) * 1.45), 300px);
  /* How far of itself the pet stands below the top of the phone. The overlap
     IS the illusion of standing behind: with none it floats like a sticker. */
  --pet-sink: .33;
}
.fan .ph { --s: var(--s-row) }
.fan { --ph-w: calc(402px * var(--s-row)); --ph-h: calc(874px * var(--s-row)) }

/* The pet is DECORATION BEHIND THE PRODUCT, so it sits between the wash and
   the handsets and never takes a pointer. */
.fan-pet {
  position: absolute; left: 50%; z-index: 1;
  --hp-size: var(--pet-w);
  bottom: calc(var(--ph-h) - var(--pet-w) * var(--pet-sink));
  transform: translateX(-50%);
  pointer-events: none;
}

/* ── The floating cards ───────────────────────────────────────────────────
   Two real cards out of the Today screen, parked in the band the pet stands
   in. They are what the handsets below are full of, at a size you can read
   without leaning in.

   ⚠️ zoom, NOT transform: scale. The replicas are authored at 402pt and
   styled by app-ui.css in absolute pixels, so they have to be shrunk whole.
   `transform: scale()` leaves the LAYOUT box at full size — the wrapper would
   still be 402px wide and `right: 6%` would anchor a box four times wider
   than the thing you can see. zoom scales the box too, so these position
   like any other element.

   ⚠️ NO backdrop-filter ON THEM. They float forever, and a backdrop blur on a
   permanently-moving element makes the compositor re-read what is behind it
   every frame; Chrome drops that snapshot on some of them and the card
   strobes. It cost us the watch cards in the wearables section once already. */
/* ⚠️ IT REACHES ABOVE .fan, ON PURPOSE. Confined to the pet's band the cards
   sat level with its head, which is as low as the hero goes before the deck
   starts. The empty space worth using is higher — either side of the offer
   plate, which is only 380px wide in a 1440 page. A negative top lifts the
   box into the copy; `.hero { overflow: clip }` keeps it from ever adding a
   scrollbar, and the bottom still stops at the row so nothing lands on a
   handset. */
.hstats {
  position: absolute; left: 0; right: 0;
  top: calc(var(--pet-w) * -.72);
  height: calc(var(--pet-w) * 1.27);
  pointer-events: none; z-index: 1;
}
.hcard {
  position: absolute;
  border-radius: 20px;
  /* ⚠️ A TILTED CARD WITH AN UNTILTED SHADOW READS AS A STICKER. This was one
     symmetric drop shadow straight down (--shadow-3) under a card leaning on
     two axes, which is the single biggest reason the pair looked pasted on
     rather than lying in the scene.

     Three shadows doing three jobs: a tight CONTACT shadow with almost no
     blur, which is what tells the eye how far off the surface something is;
     a mid shadow pushed sideways by --hc-sx so it falls away from the lean;
     and a wide ambient one. Real objects cast all three. */
  box-shadow:
    0 2px 5px rgba(10, 4, 30, .30),
    calc(var(--hc-sx, 0px) * .6) 22px 34px -18px rgba(10, 4, 30, .42),
    var(--hc-sx, 0px) 56px 96px -44px rgba(10, 4, 30, .55);
  animation: hcard-float 10s ease-in-out infinite;
  will-change: transform; backface-visibility: hidden;
}
/* Light raking across the face, and an edge.
   A flat white plane at an angle is the other half of the sticker problem:
   a real card catches light unevenly and has a lit rim on the edge nearest
   the viewer. The gradient angle is per-card so it agrees with the lean. */
.hcard::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit;
  pointer-events: none; z-index: 3;
  background: linear-gradient(var(--hc-lit, 105deg),
    rgba(255, 255, 255, .30) 0%,
    rgba(255, 255, 255, .06) 26%,
    rgba(10, 4, 30, .00) 58%,
    rgba(10, 4, 30, .13) 100%);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .65),
    inset 0 0 0 1px rgba(255, 255, 255, .16),
    inset 0 -1px 0 rgba(10, 4, 30, .16);
}
.hcard-in { zoom: var(--hc-z, .46); width: 402px; border-radius: 42px; overflow: hidden }
/* The card inside brings its own plate; this one only has to hold the
   shadow and clip the corners. */
.hcard .ap-card { margin: 0 }

/* ⚠️ WELL ABOVE THE HANDSET'S OWN SCALE. At --s the card is exactly as small
   as it already is inside the phone, which makes lifting it out pointless.
   These run at roughly 1.3x the size the deck renders them, which is the
   whole reason they are out here.

   ⚠️ AND THEY HAVE TO FIT THE BAND. Recovery is the tallest card on the Today
   screen — a 118pt orb plus its header — so the zoom is what keeps it inside
   the pet's band instead of hanging over the handsets. */
/* ⚠️ THE TILT IS PER-CARD AND IT LIVES IN A VARIABLE, because the float
   animation owns `transform` outright — a base rotation written as its own
   transform on .hcard would simply be overwritten the moment the animation
   starts. The keyframes below read --hc-rot, so each card can lean its own
   way and still share one animation.

   They lean AWAY from each other and turn slightly toward the middle: the
   pair reads as two cards lying on a surface angled toward the reader rather
   than two rectangles pinned flat to the page. */
/* --hc-sx pushes the shadow AWAY from the lean and --hc-lit points the
   highlight at the raised edge, so both agree with the rotation above. They
   are mirrored because the two cards lean opposite ways. */
.hcard--rec { left: 3%;  top: 4%;  --hc-z: .56; --hc-rot: -7deg; --hc-ry:  9deg;
              --hc-sx: 26px;  --hc-lit: 108deg; animation-delay: 0s }
.hcard--str { right: 3.5%; top: 20%; --hc-z: .62; --hc-rot:  6deg; --hc-ry: -9deg;
              --hc-sx: -26px; --hc-lit: 252deg; animation-delay: -5s }

@keyframes hcard-float {
  0%, 100% {
    transform: perspective(760px) rotateY(var(--hc-ry, 0deg))
               translateY(0) rotate(calc(var(--hc-rot, 0deg) - 1.2deg));
  }
  50% {
    transform: perspective(760px) rotateY(calc(var(--hc-ry, 0deg) * .72))
               translateY(-15px) rotate(calc(var(--hc-rot, 0deg) + 1.2deg));
  }
}
@media (prefers-reduced-motion: reduce) {
  /* Killing the animation would also kill the lean, since the lean is IN the
     keyframes. Hold the resting pose instead of falling back to square. */
  .hcard {
    animation: none;
    transform: perspective(760px) rotateY(var(--hc-ry, 0deg)) rotate(var(--hc-rot, 0deg));
  }
}
/* ⚠️ THE BAND IS THE PET'S FIRST. Below this the row is nearly the full width
   and the cards would have to sit ON the handsets to fit anywhere. */
@media (max-width: 1240px) { .hstats { display: none } }

.fan-glow {
  position: absolute; left: 50%; bottom: 0; width: min(940px, 94vw); height: 62%;
  transform: translateX(-50%); border-radius: 50% 50% 0 0 / 30% 30% 0 0;
  background: radial-gradient(ellipse at 50% 100%, var(--acc-glow), transparent 70%);
  filter: blur(70px); opacity: .5; pointer-events: none; z-index: 0;
}

/* ── A flat row ───────────────────────────────────────────────────────────
   ⚠️ NO ROTATION, NO OVERLAP, ONE BASELINE. It was a fanned deck for a while
   — same-size cards tilted about their bottom edge — and a deck is a nice
   object but a worse SHOWCASE: every card past the middle is turned away
   from you, and the outer two give most of their screen to the card in
   front. Five upright phones in a line show five whole screens, which is
   what a product page is for, and it is what the reference does. */
.fan-row {
  position: relative; z-index: 2;
  display: flex; align-items: flex-end; justify-content: center;
  gap: calc(var(--ph-w) * var(--ph-gap));
}
.fan-slot { position: relative; flex: none }
.fan-slot--far-l, .fan-slot--l, .fan-slot--mid, .fan-slot--r, .fan-slot--far-r {
  transform: none; opacity: 1; z-index: 2;
}
.fan-slot--mid { z-index: 3 }
.fan-slot .ph { box-shadow: var(--shadow-2) }
.fan-slot--mid .ph { box-shadow: var(--shadow-3) }

/* ⚠️ ALL FIVE ARE LIVE. app-ui.js has always wired every `.ph[data-screen]`
   on the page — its dock switches tabs and its screen scrolls — and the ONLY
   thing that made four of them pictures was this rule taking their pointer
   events away. Deleting it is the whole feature.

   ⚠️ AND IT RAISES THE CONTEXT CEILING. Each phone showing `today` holds
   three WebGL contexts, so five of them switched there is fifteen, plus two
   for the floating cards — against a desktop cap around sixteen. go() hands
   the old screen's contexts back before it drops it, which is what keeps
   this bounded, and app-shaders.js listens for webglcontextlost and rebuilds.
   Anything added here that mounts a shader is not free. */

@keyframes bob { 0%, 100% { translate: 0 0 } 50% { translate: 0 -10px } }
@media (prefers-reduced-motion: reduce) { .float { animation: none } }

/* ⚠️ A CAPTION, NOT A PILL. As a bordered card with a shadow it read as a
   BUTTON — floating in the gap between the deck and the store badges, the
   loudest thing in an area that is meant to be quiet, and the first thing
   your eye went to after the headline. It is a note about the thing above
   it, so it is set like one: no plate, no border, no shadow. */
.stage-hint {
  position: absolute; left: 50%; bottom: -22px; transform: translateX(-50%);
  display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
  font-size: 11.5px; font-weight: 600; color: var(--mut);
  transition: opacity .3s ease;
}
.stage-hint svg { width: 12px; height: 12px; color: var(--mut); opacity: .8 }
.fan-slot--mid.is-touched .stage-hint { opacity: 0; pointer-events: none }

/* ── How big the handsets get ──────────────────────────────────────────────
   ⚠️ THE DECK IS SIZED TO RUN ABOUT 15% PAST THE FOLD, ON PURPOSE. Sized to
   FIT above it, the handsets came out 122px wide on a 900-tall laptop —
   narrower than the reference's — because everything over them is fixed:
   nav 69, headline 121, field 89, offer plate 126, the pet's band 104. That
   left 262px of 900 for the thing people came to see. A row of phones cut
   off by the bottom of the screen is an invitation to scroll; a row that
   fits is a picture of an app. The bleed buys about 35% more handset and
   costs the bottom of the outer cards, which carry nothing.

   ⚠️ MEASURED, AND NOT MONOTONIC. The deck below the offer plate is about
   1230px x --s-row — a constant the fit script MEASURES off the live page
   rather than restating, so it survives the next change to the pet's band or
   the pull-up. The copy above steps DOWN at 960, 860, 770 and 680, and each
   of those hands the deck a chunk of room back, so --s-row jumps UP again
   right after. A ladder that only ever shrank would waste that room.

   ⚠️ EACH BAND IS SET FROM ITS FLOOR, NOT ITS CEILING. `max-height: 950`
   covers 911..950 and is tightest at 911; setting it from 950 puts the
   bottom of the band under the fold. That was wrong in the first pass. */
@media (min-width: 1101px) and (max-height: 1060px) { .fan { --s-fit: .849 } }
@media (min-width: 1101px) and (max-height:  990px) { .fan { --s-fit: .832 } }
@media (min-width: 1101px) and (max-height:  950px) { .fan { --s-fit: .766 } }
@media (min-width: 1101px) and (max-height:  910px) { .fan { --s-fit: .700 } }
@media (min-width: 1101px) and (max-height:  870px) { .fan { --s-fit: .716 } }
@media (min-width: 1101px) and (max-height:  840px) { .fan { --s-fit: .665 } }
@media (min-width: 1101px) and (max-height:  810px) { .fan { --s-fit: .614 } }
@media (min-width: 1101px) and (max-height:  780px) { .fan { --s-fit: .621 } }
@media (min-width: 1101px) and (max-height:  750px) { .fan { --s-fit: .553 } }
@media (min-width: 1101px) and (max-height:  710px) { .fan { --s-fit: .505 } }
@media (min-width: 1101px) and (max-height:  680px) { .fan { --s-fit: .511 } }
@media (min-width: 1101px) and (max-height:  650px) { .fan { --s-fit: .481 } }
@media (min-width: 1101px) and (max-height:  625px) { .fan { --s-fit: .400 } }
/* --s-cap = (128vw / 5.68) / 402px. ⚠️ 128vw, AND THE ROW IS MEANT TO RUN OFF
   BOTH EDGES. Holding all five inside the page caps the middle three at
   whatever a fifth of the viewport allows; letting the outer pair be trimmed
   by the edges is what buys the other three their size, and a row that
   continues past the screen reads as a row that continues. */
@media (max-width: 1560px) { .fan { --s-cap: .700 } }
@media (max-width: 1400px) { .fan { --s-cap: .628 } }
@media (max-width: 1320px) { .fan { --s-cap: .592 } }
@media (max-width: 1240px) { .fan { --s-cap: .556 } }
@media (max-width: 1160px) { .fan { --s-cap: .520 } }
@media (max-width: 1100px) { .fan { --s-row: .40 } }
@media (max-width: 900px)  { .fan { --s-row: .36 } }

.hero-foot {
  position: relative; z-index: 2;
  display: flex; flex-direction: column; align-items: center;
  gap: 18px; padding: clamp(34px, 3.4vw, 52px) 0 clamp(10px, 2vw, 26px);
  text-align: center;
}

/* ══ THE MOBILE CTA BAR ═══════════════════════════════════════════════════
   Under 560px the nav's "Get early access" is hidden — it does not fit beside
   the wordmark and the theme switch. That left a twelve-thousand-pixel page
   with no way to reach the signup except scrolling back to the top. The bar
   below appears once the hero's own form has gone past, sits above the home
   indicator, and hides itself again while the form is on screen so it never
   competes with the thing it points at. */
.mcta {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 80;
  display: none; align-items: center; gap: 12px;
  padding: 10px 16px calc(10px + env(safe-area-inset-bottom));
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  -webkit-backdrop-filter: blur(18px) saturate(150%);
  backdrop-filter: blur(18px) saturate(150%);
  border-top: 1px solid var(--line);
  transform: translateY(110%);
  transition: transform .28s cubic-bezier(.2, .8, .3, 1);
  /* The blur EARNS its keep here — the page scrolls behind a fixed bar — but
     it still slides on a transform, so it gets a stable layer to slide on. */
  will-change: transform;
}
.mcta.is-up { transform: none }
.mcta-tx { flex: 1; min-width: 0 }
.mcta-tx b { display: block; font-size: 13.5px; font-weight: 750; letter-spacing: -.01em }
.mcta-tx span { display: block; font-size: 11.5px; color: var(--mut); margin-top: 1px }
.mcta .btn { flex: none; padding: 11px 18px; font-size: 14px }
@media (max-width: 560px) { .mcta { display: flex } }
@media (prefers-reduced-motion: reduce) { .mcta { transition: none } }

/* The world map is a shape, not a chart — at 343px in a padded column the
   continents stop being legible. It takes the full width of the phone. */
@media (max-width: 700px) {
  .geo-canvas { margin: 0 -16px; width: calc(100% + 32px) }
}

/* ══ THE HERO, ON A PHONE ═════════════════════════════════════════════════
   The desktop promise is "the product without scrolling". A 390 × 844 screen
   cannot hold five handsets at a readable size — five at any scale that fits
   is five unreadable slivers — so it holds THREE, and the outer two are
   allowed to bleed off both edges. A row that runs past the screen reads as a
   row that continues; a row squeezed to fit reads as a row of stamps. */
/* ⚠️ THE FIELD HAS TO HOLD ITS OWN PLACEHOLDER. The desktop sizes put
   "you@email.com" and a 17px button in the same pill, and at 390px the
   placeholder was cut mid-word — which is the one string on the page that
   tells someone what to type. The row steps down before that happens. */
@media (max-width: 620px) {
  .join-row { padding: 6px 6px 6px 10px }
  .join-input { font-size: 15.5px; padding: 12px 6px 12px 8px }
  .join-row .btn { padding: 13px 18px; font-size: 15px; min-height: 46px }
}
@media (max-width: 430px) {
  .join-input { font-size: 14.5px }
  .join-row .btn { padding: 12px 14px; font-size: 14px }
}
/* An iPhone SE. The button's label is what has to give at this width — the
   arrow after it says "go" on its own. */
@media (max-width: 340px) {
  .join-input { font-size: 13.5px; padding: 11px 4px 11px 6px }
  .join-row { padding: 5px 5px 5px 8px }
  .join-row .btn { padding: 11px 12px; font-size: 13px; gap: 6px }
}

@media (max-width: 700px) {
  /* ⚠️ THE NAV'S HEIGHT IS NOT OPTIONAL HERE. The hero is pulled up under a
     transparent nav so the wash starts at the top of the page, and the
     padding is what puts the content back below it. A bare `12px` here — a
     leftover from when the nav had its own opaque plate — slid the headline
     straight under the wordmark. */
  .hero { padding-top: calc(var(--nav) + 12px) }
  .hero-in { gap: 18px }
  h1 { font-size: clamp(32px, 9.4vw, 42px); line-height: 1.02 }
  .hero-say { font-size: 12.5px }

  /* ⚠️ ALL FIVE, BY SWIPING. They used to be three — the outer pair removed,
     because five at any scale that fits 390px is five unreadable slivers. But
     removing them also removed two screens from the argument on the device
     most people read it from. A scroll-snap row gives all five at a readable
     size and costs nothing: no library, no JS, and the browser's own
     momentum. The middle one is centred on load, so the page still opens on
     the same handset it always did.

     ⚠️ scroll-padding, NOT padding, DOES THE CENTRING. Padding on a snap
     container shifts the snap positions with it; scroll-padding tells the
     browser where "centred" is without moving anything. */
  .fan-row {
    display: flex;
    /* ⚠️ overflow-y HAD TO STOP SAYING `visible`, AND NOT BECAUSE OF CLIPPING.
       When either axis is not `visible`, CSS computes a `visible` on the other
       axis to `auto` — so `overflow-x: auto; overflow-y: visible` is really
       auto/auto, and the row of handsets was a VERTICAL scroller too. You
       could drag the phones up and down inside their own strip, which is what
       made the carousel feel broken. `hidden` clips exactly as the computed
       `auto` already did; it just refuses to scroll. */
    overflow-x: auto; overflow-y: hidden;
    /* And the browser is told the gesture is horizontal, so a vertical swipe
       that starts on a phone scrolls the PAGE instead of being eaten by
       axis-lock heuristics. `contain` stops a swipe past the last handset
       turning into the iOS back-navigation. */
    touch-action: pan-x;
    overscroll-behavior-x: contain;
    scroll-snap-type: x mandatory;
    scroll-padding-inline: 50%;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none;
    /* Half a screen either side so the first and last can reach the middle. */
    padding-inline: calc(50vw - var(--ph-w) / 2);
    gap: calc(var(--ph-w) * var(--ph-gap));
  }
  .fan-row::-webkit-scrollbar { display: none }
  .fan-slot { scroll-snap-align: center }
  .fan { --s-row: .58; --ph-gap: .12 }
}
/* ⚠️ THESE ONLY SHRANK BECAUSE FIVE HAD TO FIT AT ONCE. They swipe now, so
   the handset can be the size it deserves — the constraint moved from "five
   across 390px" to "one across 390px". */
@media (max-width: 480px) { .fan { --s-row: .54; --ph-gap: .12 } }
@media (max-width: 380px) { .fan { --s-row: .48; --ph-gap: .12 } }

/* ⚠️ THE HINT COMES OFF ON A PHONE. Below the handset it lands in the row's
   own bleed; above it, it lands across the pet's face. There is nowhere left
   for it — and it is telling a touch device it can touch things, which is the
   one platform that did not need to be told. */
@media (max-width: 700px) { .stage-hint { display: none } }

/* The reward line is three phrases on one row at 1200px and three lines at
   390px. Letting it wrap is the whole fix; forcing it to one line is what
   pushed it off the screen. */
@media (max-width: 700px) {
  .hero-foot { gap: 13px; padding-top: 26px }
  .tiers { gap: 8px }
  .tiers-live { font-size: 12px }
  .tier-chip { font-size: 11.5px; padding: 4px 10px }
  .tier-bar { max-width: 220px }
  .hero-tail { gap: 12px }
}

/* The badges and the socials shared one row that wrapped into three lines on
   a phone. Two badges of equal width, then the socials on their own line. */
@media (max-width: 520px) {
  /* Side by side, shorter. Two badges stacked is 110px of a screen whose
     whole budget above the fold is about 700 — and the badges are not the
     thing anyone is here to press yet. A grid would stretch them: the two
     pieces of artwork have different aspect ratios and neither may be
     distorted, so they keep their own widths and just get smaller. */
  .stores { display: flex; justify-content: center; gap: 10px; flex-wrap: nowrap }
  .store img { height: 42px }
  .store-soon { font-size: 8px; padding: 1px 6px; top: -12px }
}
@media (max-width: 360px) { .store img { height: 38px } }

/* ── Store buttons ────────────────────────────────────────────────────── */
.stores { display: flex; gap: 12px; flex-wrap: wrap }
/* ⚠️ THE BADGE IS AN IMAGE, NOT A BUTTON WE DREW. Apple and Google both
   publish this artwork and both require it used as supplied — no re-drawn
   logo, no re-set type, no altered proportions. The <a> is a frame around it
   and nothing else, which is why there is no padding, no background and no
   border here: anything of ours touching the badge is a guideline broken.
   Swapping in the official downloads is a file replacement, not a rewrite. */
.store {
  position: relative; display: block; flex: none;
  border-radius: 9px;
  transition: transform .18s ease, filter .18s ease;
}
.store img { display: block; height: 50px; width: auto }
.store:hover { transform: translateY(-2px); filter: brightness(1.08) }
@media (prefers-reduced-motion: reduce) { .store:hover { transform: none } }
/* The one thing we do add: a small flag ABOVE the badge, clear of the
   artwork, because a store badge that does not open a store has to say so. */
.store-soon {
  position: absolute; top: -14px; left: 50%; transform: translateX(-50%);
  padding: 1px 7px; border-radius: 999px; white-space: nowrap;
  background: var(--bg-2); border: 1px solid var(--line-2);
  font-size: 8px; font-weight: 800; letter-spacing: .09em;
  text-transform: uppercase; color: var(--mut);
}
/* The flag sits ABOVE the badge, outside it, so the row has to make room or
   it collides with whatever is over it. */
.stores { margin-top: 10px }

/* ── Socials ──────────────────────────────────────────────────────────── */
.socials { display: flex; align-items: center; gap: 10px }
.socials a {
  width: 42px; height: 42px; border-radius: 13px;
  display: grid; place-items: center;
  background: var(--card); border: 1px solid var(--line-2); color: var(--text);
  transition: transform .18s ease, background .18s ease, border-color .18s ease, color .18s ease;
}
.socials a svg { width: 21px; height: 21px }
.socials a:hover { transform: translateY(-2px); border-color: var(--acc-line); color: var(--acc) }
.socials .lbl { font-size: 12.5px; font-weight: 600; color: var(--mut); margin-right: 2px }

/* ══ SECTIONS ════════════════════════════════════════════════════════════ */
.band { padding: clamp(64px, 8vw, 128px) 0; position: relative }
/* ⚠️ THE NAV IS STICKY, SO EVERY ANCHOR LANDS UNDER IT. Clicking "Wearables"
   scrolled #bands to y=0 and the nav sat on top of its heading — the section
   opened mid-sentence. Nothing in this file offset it; `scroll-margin-top` on
   the target is the whole fix, and it has to be on the SECTION rather than
   html{scroll-padding} so a jump inside a scrollable child is unaffected. */
section[id], .join-anchor { scroll-margin-top: calc(var(--nav) + 18px) }
.band--alt { background: var(--bg-2) }
.band--tight { padding: clamp(44px, 5vw, 76px) 0 }
.band-head { max-width: 720px; display: flex; flex-direction: column; gap: 16px }
/* A centred heading gets more room than a column one: 720px puts three words
   on a third line and the balance reads as an accident. */
.band-head--mid { max-width: 880px }
.band-head--mid { margin-inline: auto; text-align: center; align-items: center }

/* Anything that fades in on scroll. */
.fade { opacity: 0; transform: translateY(20px); transition: opacity .7s ease, transform .7s cubic-bezier(.2,.8,.3,1) }
.fade.in { opacity: 1; transform: none }
@media (prefers-reduced-motion: reduce) { .fade { opacity: 1; transform: none; transition: none } }

/* ══ THE SIGNUP MAP ══════════════════════════════════════════════════════ */
.geo {
  border-radius: var(--r-lg); overflow: hidden;
  background: var(--card); border: 1px solid var(--line);
  padding: clamp(18px, 2.4vw, 28px);
}
.geo-head { display: flex; align-items: flex-end; gap: 16px; flex-wrap: wrap; margin-bottom: 10px }
.geo-head .tx { flex: 1; min-width: 220px }
.geo-head h3 { font-size: clamp(19px, 1.8vw, 24px) }
.geo-head p { margin-top: 6px; font-size: 13.5px; color: var(--mut); font-weight: 500 }
.geo-stat { text-align: right }
.geo-stat b { display: block; font-size: clamp(28px, 3vw, 40px); font-weight: 850; letter-spacing: -.04em; line-height: 1; color: var(--acc) }
.geo-stat span { font-size: 11.5px; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; color: var(--mut) }
/* geo-map.js paints a canvas land mask and an SVG bubble layer, and takes
   every colour from here — it is shared with the admin console, which draws
   the same world on white. */
/* ⚠️ THE LAND IS NOT VIOLET. It was, and that was the whole problem: the
   renderer ramps every cell from --geo-dot toward --geo-hot, so when both
   ends of that ramp were the same hue the map had no ends — a country with
   three members and a country with none were the same colour a little
   lighter, and the whole panel read as one violet haze.

   Land is a neutral slate. Violet then means exactly one thing: people. */
.geo-canvas { position: relative; --geo-dot: rgba(26, 20, 48, .30); --geo-hot: var(--acc) }
[data-theme="dark"]  .geo-canvas { --geo-dot: rgba(214, 206, 245, .22) }
/* The world is drawn at whatever width it is given, so capping the width is
   how the section stops being 500px of ocean. */
.geo-canvas { max-width: 880px; margin-inline: auto }
.geo-canvas canvas { width: 100%; height: auto; display: block }
.geo-canvas svg { position: absolute; inset: 0; width: 100%; height: 100% }
.geo-core { fill: var(--acc); opacity: .92 }
.geo-halo { fill: var(--acc); opacity: .14 }
.geo-canvas g { cursor: default; transition: opacity .15s ease }
.geo-canvas g:hover .geo-core { opacity: 1 }
.geo-canvas g:hover .geo-halo { opacity: .26 }
/* The named places, in words, under the heat. */
.geo-top { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 12px }
.geo-top span {
  display: inline-flex; align-items: center; gap: 7px;
  padding: 7px 12px; border-radius: 999px;
  background: var(--card-2); border: 1px solid var(--line);
  font-size: 13px; font-weight: 600; color: var(--sub);
}
.geo-top i { font-style: normal; font-size: 14px; line-height: 1 }
.geo-top b { font-size: 13px; font-weight: 800; color: var(--acc); font-variant-numeric: tabular-nums }

.geo-tip {
  position: absolute; z-index: 3; pointer-events: none;
  transform: translateY(-100%);
  padding: 8px 11px; border-radius: 12px; white-space: nowrap;
  background: var(--card-2); border: 1px solid var(--line-2);
  box-shadow: var(--shadow-2);
}
.geo-tip b { display: block; font-size: 13px; font-weight: 750; letter-spacing: -.01em }
.geo-tip span { display: block; margin-top: 2px; font-size: 11.5px; font-weight: 600; color: var(--mut) }
/* Before anyone has joined there is nothing to draw, and a 240pt empty box
   is a worse advert than one honest line. */
.geo-empty {
  display: grid; place-items: center; min-height: 84px;
  font-size: 13.5px; color: var(--mut); font-weight: 550; text-align: center;
}
.geo-stat[hidden] { display: none }

/* ══ THE BANDS BAND ══════════════════════════════════════════════════════
   The one section that is allowed to be a spectacle: three watches orbiting
   a phone that is showing what they feed. */
.wear { position: relative; overflow: clip; padding: clamp(48px, 5.4vw, 82px) 0 }
.wear-in {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(22px, 2.6vw, 34px);
}
.wear-logos { display: flex; align-items: center; justify-content: center; gap: 14px; flex-wrap: wrap }
.wear-logo {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 15px 9px 9px; border-radius: 999px;
  background: var(--card); border: 1px solid var(--line-2);
  font-size: 14.5px; font-weight: 700; letter-spacing: -.01em;
}
.wear-logo img { width: 26px; height: 26px; border-radius: 7px }
/* ── The two health stores ────────────────────────────────────────────────
   Half a step behind the four bands, because the phone in your pocket is the
   fallback rather than the headline — but carrying the real marks, at the
   same 26px the real logos are. */
.wear-logos--os { margin-top: -4px }
.wear-logos--os .wear-logo { font-size: 13.5px; color: var(--sub) }
/* Google's mark ships as a bare heart with no plate of its own, and every
   other logo in this row sits on one. This is that plate — white, because
   that is the ground the mark is drawn for. */
.wl-plate {
  width: 26px; height: 26px; border-radius: 7px; flex: none;
  display: grid; place-items: center;
  background: #fff; box-shadow: inset 0 0 0 1px rgba(38, 20, 80, .10);
}
.wl-plate img { width: 18px; height: 18px }
/* Four across on a desktop, and they wrap rather than squeezing: a 4-up grid
   of 15px prose at 300px a column is already at the edge of readable. */
.wear-list {
  display: grid; gap: 14px; margin: 0; padding: 0; list-style: none; width: 100%;
  grid-template-columns: repeat(auto-fit, minmax(258px, 1fr));
}
.wear-list li {
  display: flex; flex-direction: column; gap: 11px;
  padding: 20px; border-radius: var(--r);
  background: var(--card); border: 1px solid var(--line);
  box-shadow: var(--shadow-1);
}
.wear-list .ic {
  width: 34px; height: 34px; border-radius: 11px; flex: none;
  display: grid; place-items: center;
  background: var(--acc-soft); color: var(--acc);
}
.wear-list .ic svg { width: 17px; height: 17px }
.wear-list b { display: block; font-size: 15.5px; font-weight: 750; letter-spacing: -.015em }
.wear-list span { display: block; margin-top: 3px; font-size: 13.5px; line-height: 1.5; color: var(--sub) }

/* The orbit. Three watch faces on soft rings around the phone. */
/* ⚠️ THE ORBIT MUST BE WIDER THAN THE PHONE IN IT. The watches are positioned
   in percentages of THIS box, so when the section became a centred column and
   the box shrank to the handset, `left: -6%` stopped meaning "outside the
   phone" and started meaning "over its screen". The rings went with it. */
.orbit {
  position: relative; display: grid; place-items: center; perspective: 1400px;
  /* Wider than the handset in it — the watches are positioned in percentages
     of THIS box, so if it shrinks to the phone, `left: -6%` stops meaning
     "beside it" and starts meaning "over its screen". */
  width: min(500px, 100%);
}
.orbit-ring {
  position: absolute; border-radius: 50%; border: 1px dashed var(--line-2);
  pointer-events: none; opacity: .8;
}
/* ⚠️ ELLIPSES, NOT CIRCLES. A circle on a 500px-wide box is 500px TALL, and
   the handset inside it is 367 — so a perfect ring added about 170px of empty
   dashed air to the section for nothing. They orbit a phone, and a phone is
   taller than it is wide. */
.orbit-ring--1 { width: 104%; aspect-ratio: 1.30 }
.orbit-ring--2 { width: 80%;  aspect-ratio: 1.22 }
/* ⚠️ NO backdrop-filter HERE. These three cards translate and rotate forever,
   inside a container that establishes a 3D context — and a backdrop blur on a
   moving element makes the compositor re-read what is behind it every frame.
   Chrome drops that snapshot on some of them, which is the flashing: the card
   goes momentarily flat and comes back. It bought nothing either way, because
   what is behind them is a flat band colour and a 1px dashed ring, and
   blurring a flat colour returns the same flat colour. Opaque, and each card
   gets its own stable layer. */
.watch {
  position: absolute; z-index: 3;
  width: 86px; padding: 11px; border-radius: 22px;
  background: var(--card);
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-2);
  display: flex; flex-direction: column; align-items: center; gap: 7px;
  animation: watch-float 8s ease-in-out infinite;
  will-change: transform;
  backface-visibility: hidden;
}
.watch img { width: 34px; height: 34px; border-radius: 10px }
.watch b { font-size: 15px; font-weight: 800; letter-spacing: -.02em; line-height: 1 }
.watch span { font-size: 9.5px; font-weight: 800; letter-spacing: .1em; text-transform: uppercase; color: var(--mut) }
.watch--1 { top: 2%;  left: -4%;  animation-delay: 0s }
.watch--2 { top: 40%; right: -8%; animation-delay: -2.6s }
.watch--3 { bottom: 4%; left: 0;   animation-delay: -5.2s }
@keyframes watch-float {
  0%, 100% { transform: translateY(0)    rotate(-3deg) }
  50%      { transform: translateY(-18px) rotate(3deg) }
}
@media (prefers-reduced-motion: reduce) { .watch { animation: none } }
@media (max-width: 620px) {
  .watch { width: 78px; padding: 9px; border-radius: 20px }
  .watch img { width: 26px; height: 26px }
  .watch--1 { left: -2% } .watch--3 { left: 0 }
}
/* ⚠️ .58 MADE THIS SECTION ONE AND A HALF SCREENS TALL. 874pt of handset at
   .58 is 507px before the rings and the watch cards, so the heading had
   scrolled away by the time the phone was on screen — you never saw the
   claim and its evidence at the same time, which is the only reason the
   spectacle is here. */
.orbit .ph { --s: .38; transform: rotateY(6deg) rotateX(2deg) }
.wear-cta { align-self: center }
@media (max-width: 620px) { .orbit .ph { --s: .40 } }
@media (max-height: 850px) { .orbit .ph { --s: .38 } }

/* ══ SCROLL FREEZE ═══════════════════════════════════════════════════════
   One phone pinned to the middle of the viewport while the copy walks past
   it. Pure CSS sticky for the pin; app-v2.js only decides which screen the
   handset shows. On a narrow screen it degrades to a plain stack — a pinned
   element in a 380px-tall viewport is a scroll trap. */
.freeze { position: relative }
.freeze-in {
  display: grid; grid-template-columns: 58px minmax(0, 1fr) minmax(0, 420px);
  gap: clamp(20px, 3.4vw, 46px); align-items: start;
}
.freeze-steps { display: flex; flex-direction: column; min-width: 0 }
/* Each step owns a full viewport of scroll, and its content is centred in it —
   so the sentence you are reading and the phone it describes sit on the same
   line of sight. */
.freeze-step {
  min-height: calc(100vh - var(--navh)); display: flex; flex-direction: column;
  justify-content: center; gap: 16px; padding: 24px 0;
}
.freeze-step .n {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 12px;
  background: var(--acc-soft); border: 1px solid var(--acc-line);
  color: var(--acc); font-size: 13px; font-weight: 850;
}
.freeze-step h3 { font-size: clamp(23px, 2.3vw, 32px) }
.freeze-step .lede { max-width: 44ch }
.freeze-step .pills { display: flex; flex-wrap: wrap; gap: 8px }
.freeze-step .pills span {
  padding: 7px 13px; border-radius: 999px;
  background: var(--card); border: 1px solid var(--line);
  font-size: 12.5px; font-weight: 650; color: var(--sub);
}
/* Everything but the live step steps back — one idea is in focus at a time. */
.freeze-step { opacity: .28; transition: opacity .45s ease, transform .45s cubic-bezier(.2,.8,.3,1) }
.freeze-step:not(.is-live) { transform: translateY(6px) }
.freeze-step.is-live { opacity: 1; transform: none }
@media (prefers-reduced-motion: reduce) {
  .freeze-step { opacity: 1; transition: none; transform: none }
}

/* ── The rail ──────────────────────────────────────────────────────────
   Twelve steps with one on screen at a time is a tour you can get lost in.
   The rail is the table of contents: where you are, how much is left, and
   a way to jump. It is built from the steps themselves in app-v2.js, so it
   cannot fall out of sync with them. */
.freeze-rail {
  position: sticky; top: var(--navh); height: calc(100vh - var(--navh));
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
}
.freeze-rail::before {
  content: ""; position: absolute; top: 50%; left: 50%; width: 1px;
  height: calc(100% - 120px); transform: translate(-50%, -50%);
  background: linear-gradient(180deg, transparent, var(--line) 12%, var(--line) 88%, transparent);
}
.freeze-rail button {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0; border: 0; border-radius: 50%;
  background: none; cursor: pointer; color: var(--mut);
  font-size: 11px; font-weight: 800; font-variant-numeric: tabular-nums;
  transition: color .3s ease;
}
.freeze-rail button span {
  position: absolute; inset: 9px; border-radius: 50%;
  background: var(--line-2); transition: all .35s cubic-bezier(.2,.8,.3,1);
}
.freeze-rail button i { position: relative; opacity: 0; transition: opacity .3s ease; font-style: normal }
.freeze-rail button:hover span { background: var(--sub) }
.freeze-rail button[aria-current="true"] { color: var(--acc-ink) }
.freeze-rail button[aria-current="true"] span {
  inset: 0; background: var(--acc);
  box-shadow: 0 6px 18px -6px color-mix(in srgb, var(--acc) 75%, transparent);
}
.freeze-rail button[aria-current="true"] i { opacity: 1 }

.freeze-stage {
  position: sticky; top: var(--navh); height: calc(100vh - var(--navh));
  display: grid; place-items: center; justify-self: center;
  /* ⚠️ WITHOUT PERSPECTIVE, rotateY IS NOT A ROTATION. It is an affine
     horizontal squash: both vertical edges stay exactly the same height, so a
     -6deg "tilt" just made the handset narrower. With a vanishing point the
     near edge grows and the far edge shrinks, the side wall the frame draws
     finally has a reason to be visible, and the thing reads as an object
     standing in the page rather than a picture of one. */
  perspective: 1500px;
  perspective-origin: 52% 46%;
}
/* The handset is sized to the height it is centred in, so the dock — the part
   that proves the replica is real — never falls off the bottom. */
.freeze-stage .ph {
  --s: .74;
  --tx: 0deg; --ty: 0deg;
  transform: rotateX(calc(2.5deg + var(--tx))) rotateY(calc(-11deg + var(--ty))) rotateZ(.8deg);
  transition: transform .4s cubic-bezier(.2, .8, .3, 1);
  /* A deeper cast than the frame's own, because at this angle the handset is
     leaning away from a light that is now clearly above and to the left. */
  filter: drop-shadow(-26px 40px 44px rgba(4, 6, 14, .55));
}
@media (prefers-reduced-motion: reduce) {
  .freeze-stage .ph { transition: none }
}
@media (max-height: 980px) { .freeze-stage .ph { --s: .68 } }
@media (max-height: 900px) { .freeze-stage .ph { --s: .62 } }
@media (max-height: 820px) { .freeze-stage .ph { --s: .56 } }
@media (max-height: 740px) { .freeze-stage .ph { --s: .50 } }
@media (max-height: 660px) { .freeze-stage .ph { --s: .45 } }
.freeze-glow {
  position: absolute; width: 92%; aspect-ratio: .8; border-radius: 50%;
  background: radial-gradient(circle, var(--acc-glow), transparent 64%);
  filter: blur(70px); opacity: .55; pointer-events: none;
}
@media (max-width: 1000px) {
  /* ONE COLUMN, AND THE HANDSET STAYS PINNED — just to the top instead of the
     side. The previous mobile answer un-pinned it: a static phone showing
     screen one, followed by thirteen paragraphs about screens you never saw.
     That deletes the entire claim of the section on the device most people
     read it from. Here the phone holds a band at the top, the copy scrolls
     underneath it, and the screen changes as it goes — the same idea as the
     desktop freeze, rotated ninety degrees. */
  .freeze-in { grid-template-columns: minmax(0, 1fr); gap: 0 }
  .freeze-rail { display: none }
  .freeze-stage {
    /* ⚠️ 4px ABOVE THE NAV, NOT LEVEL WITH IT. --navh rounds, and the nav's own
       box came out 3px short of it — so there was a hairline of bare page
       between the two sticky layers, and copy scrolling past flashed through
       it. Overlapping by 4 and giving the padding back keeps the handset where
       it was and leaves nothing to see through. */
    position: sticky; top: calc(var(--navh) - 4px); z-index: 3;
    height: auto; justify-self: stretch; order: -1;
    /* ⚠️ THE FADE HAS TO SIT BELOW THE HANDSET, NOT ACROSS IT. The copy runs
       UNDER this band, so the band needs a floor. The first cut faded from 72%
       to transparent over the stage's whole height — but the phone occupies
       almost all of that height, so the fade ran across the phone's lower
       half and the paragraph scrolling behind it showed THROUGH the handset.
       That double exposure is the "text moving up looks weird": two things
       legible in the same pixels.

       Solid the whole way down to the padding, then fade inside the 30px gap
       under the phone, where there is nothing to see through. */
    padding: 10px 0 30px;
    background: linear-gradient(180deg,
      var(--bg) 0, var(--bg) calc(100% - 30px),
      color-mix(in srgb, var(--bg) 55%, transparent) calc(100% - 10px), transparent 100%);
  }
  .freeze-stage .ph {
    /* ⚠️ THE BAND WAS EATING THE COPY. At .46 the handset held 442px of an
       844px screen, leaving 330px for a step whose content is 322px — eight
       pixels of slack, so a heading was under the glass almost as soon as it
       arrived. That shortfall is what the sticky heading was invented to paper
       over, and the sticky heading is what broke the reading order. Fix the
       cause: a smaller handset leaves ~100px of genuine slack, and the copy
       stays put long enough to read. */
    --s: .385;
    /* No tilt on a small screen: the handset is already only 190pt wide, and
       a rotation costs width the screen does not have. */
    transform: none; filter: none;
  }
  .freeze-glow { display: none }
  /* ⚠️ ONE STEP OWNS THE COPY AREA, and the number that sets that is the space
     the band leaves behind — not a guessed 54vh. Below the band there is
     roughly one screenful of room, so sizing each step to exactly that means
     the heading enters at the bottom, rises to sit just under the handset, and
     only leaves once the next one is arriving. */
  .freeze-step {
    min-height: calc(100svh - var(--navh) - var(--freeze-band, 300px) - 8px);
    padding: 14px 0 24px;
    justify-content: flex-start;
    /* A lighter step-back than the desktop .28. There the dimmed steps are
       off to the side of a reading line; here they are the paragraph you are
       about to scroll into, directly under your thumb, and .28 renders that
       as an unreadable grey. */
    opacity: .5; transform: none;
  }
  .freeze-step.is-live { opacity: 1 }
  .freeze-steps { gap: 0 }

  /* ⚠️ THE HEADING IS NOT STICKY, AND IT MUST NOT BE. It was — pinned to the
     underside of the band so the title and the screen it describes stayed on
     screen together. But `position: sticky` only holds the H3; the rest of the
     step keeps scrolling up past it, and the lede sits AFTER the heading in
     the document, so it ended up ABOVE it on screen. Measured mid-scroll the
     lede rendered 239px higher than its own title: you read the paragraph,
     then the heading, then the pills. That is the "scroll looks horrible" —
     not a fade or a z-index, the reading order coming apart.

     The step being exactly one copy-area tall is what solves the original
     problem instead, and it solves it without reordering anything. */
  .freeze-step h3 { padding: 0 0 6px; margin: 0 }
  .freeze-step h3 { font-size: 23px }
  .freeze-step .lede { max-width: none }
}
/* The handset is the section's whole argument, so it keeps as much height as
   the viewport can spare — but never so much that the copy under it is a
   letterbox. */
@media (max-width: 1000px) and (max-height: 780px) { .freeze-stage .ph { --s: .40 } }
@media (max-width: 1000px) and (max-height: 680px) { .freeze-stage .ph { --s: .35 } }
@media (max-width: 380px) { .freeze-stage .ph { --s: .42 } }

/* ══ FEATURE GRID ════════════════════════════════════════════════════════ */
.grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px }
.cardlet {
  padding: 24px; border-radius: var(--r);
  background: var(--card); border: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 10px;
  transition: transform .2s ease, border-color .2s ease;
}
.cardlet:hover { transform: translateY(-3px); border-color: var(--acc-line) }
.cardlet .ic {
  width: 40px; height: 40px; border-radius: 13px;
  display: grid; place-items: center; background: var(--acc-soft); color: var(--acc);
}
.cardlet .ic svg { width: 19px; height: 19px }
.cardlet b { font-size: 17px; font-weight: 750; letter-spacing: -.02em }
.cardlet p { font-size: 14px; line-height: 1.55; color: var(--sub) }

/* ══ PRICING ═════════════════════════════════════════════════════════════ */
.plans { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; max-width: 880px; margin-inline: auto }
.plan {
  padding: 28px; border-radius: var(--r-lg);
  background: var(--card); border: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 14px;
}
.plan--pro { border-color: var(--acc-line); box-shadow: 0 0 0 1px var(--acc-line), var(--shadow-2) }
.plan .top { display: flex; align-items: center; gap: 10px }
.plan .top b { font-size: 17px; font-weight: 800; letter-spacing: -.02em }
.plan .flag {
  margin-left: auto; padding: 4px 10px; border-radius: 999px;
  background: var(--acc); color: var(--acc-ink);
  font-size: 10.5px; font-weight: 850; letter-spacing: .08em; text-transform: uppercase;
}
.plan .price { display: flex; align-items: baseline; gap: 6px }
.plan .price b { font-size: 40px; font-weight: 850; letter-spacing: -.045em; line-height: 1 }
.plan .price span { font-size: 14px; font-weight: 600; color: var(--mut) }
.plan ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px }
.plan li { display: flex; gap: 9px; align-items: flex-start; font-size: 14.5px; line-height: 1.45; color: var(--sub) }
.plan li svg { width: 15px; height: 15px; flex: none; margin-top: 2px; color: var(--acc) }

/* ══ THE CLOSER ══════════════════════════════════════════════════════════ */
.closer { overflow: clip }
/* The second place the pet appears, and the only one where it is the first
   thing you meet rather than something standing behind the product. */
.closer-pet { --hp-size: clamp(96px, 11vw, 150px); margin-bottom: 2px }

/* ══ FOOTER ══════════════════════════════════════════════════════════════ */
.foot { border-top: 1px solid var(--line); padding: 48px 0 40px; background: var(--bg-2) }
.foot-in { display: grid; grid-template-columns: minmax(0, 1.4fr) repeat(3, minmax(0, 1fr)); gap: 32px }
@media (max-width: 820px) { .foot-in { grid-template-columns: 1fr 1fr } }
.foot h4 { font-size: 12px; font-weight: 800; letter-spacing: .14em; text-transform: uppercase; color: var(--mut); margin-bottom: 14px }
.foot ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 10px }
.foot a { font-size: 14.5px; font-weight: 550; color: var(--sub) }
.foot a:hover { color: var(--text) }
.foot-brand { display: flex; flex-direction: column; gap: 16px }
.foot-legal {
  margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--line);
  display: flex; gap: 16px; flex-wrap: wrap; align-items: center;
  font-size: 13px; color: var(--mut); font-weight: 500;
}
.foot-legal .sp { margin-left: auto }

/* ══ THE POST-SIGNUP POPUP ═══════════════════════════════════════════════ */
.modal {
  position: fixed; inset: 0; z-index: 200;
  display: grid; place-items: center; padding: 20px;
  background: rgba(4, 9, 18, .72);
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
  opacity: 0; pointer-events: none; transition: opacity .28s ease;
}
.modal.is-open { opacity: 1; pointer-events: auto }
.modal-card {
  position: relative; width: min(460px, 100%);
  border-radius: var(--r-lg); padding: 34px 30px 28px;
  background: var(--card); border: 1px solid var(--line-2);
  box-shadow: var(--shadow-3);
  text-align: center; display: flex; flex-direction: column; gap: 16px; align-items: center;
  transform: translateY(18px) scale(.96); transition: transform .34s cubic-bezier(.2,.8,.3,1);
}
.modal.is-open .modal-card { transform: none }
@media (prefers-reduced-motion: reduce) {
  .modal, .modal-card { transition: none }
}
.modal-x {
  position: absolute; top: 12px; right: 12px;
  width: 36px; height: 36px; border-radius: 12px;
  display: grid; place-items: center; color: var(--mut);
}
.modal-x:hover { background: var(--bg-2); color: var(--text) }
.modal-x svg { width: 17px; height: 17px }
.modal-mark {
  width: 84px; height: 84px; border-radius: 26px;
  display: grid; place-items: center;
  background: var(--acc-soft); border: 1px solid var(--acc-line);
}
.modal-mark .hp { width: 58px; height: 58px }
.modal-mine {
  display: block; margin-top: 8px;
  font-size: 12px; font-weight: 600; color: var(--acc);
}
.modal-card h3 { font-size: 26px }
.modal-card p { font-size: 15px; line-height: 1.55; color: var(--sub) }
/* The code, the reason they gave an address. */
.modal-code {
  width: 100%; padding: 16px; border-radius: 18px;
  background: var(--bg-2); border: 1px dashed var(--acc-line);
  display: flex; flex-direction: column; gap: 4px;
}
.modal-code b { font-size: 30px; font-weight: 850; letter-spacing: .06em; color: var(--acc) }
.modal-code span { font-size: 12.5px; font-weight: 600; color: var(--mut) }
.modal-actions { display: flex; gap: 10px; flex-wrap: wrap; justify-content: center; width: 100% }
.modal-actions .btn { flex: 1 1 180px }

/* A short burst of confetti — it fires once, on success, and cleans itself up.
   Nothing else on the page is allowed to celebrate. */
.confetti { position: fixed; inset: 0; z-index: 210; pointer-events: none; overflow: hidden }
.confetti i {
  position: absolute; top: -12px; width: 8px; height: 14px; border-radius: 2px;
  animation: fall linear forwards;
}
@keyframes fall {
  to { transform: translateY(105vh) rotate(720deg); opacity: .1 }
}

/* ══ THE MATHS ════════════════════════════════════════════════════════════
   One year of subscriptions, drawn to scale. Both tracks share the same
   100% = $355.96 axis, which is the whole argument: the Heala bar is short
   because the number is small, not because a designer drew it small. */
.vs { max-width: 940px; margin: 0 auto }
/* ⚠️ A PLATE, NOT TEXT ON THE BAND. Every figure here sat directly on the
   section background, so the eye had nothing to tell it where the comparison
   started and stopped and the whole thing read as loose data. One card, and
   it is a chart. */
.vs-card {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-lg); box-shadow: var(--shadow-1);
  padding: clamp(20px, 3vw, 38px) clamp(18px, 3vw, 36px);
}

.vs-row {
  display: grid; grid-template-columns: 150px 1fr 168px;
  align-items: center; gap: 22px;
}
.vs-row + .vs-row { margin-top: 18px }

.vs-label b { display: block; font-size: 16px; font-weight: 800; letter-spacing: -.01em }
.vs-label span { display: block; margin-top: 3px; font-size: 13px; color: var(--mut) }

.vs-track { display: flex; align-items: center; gap: 4px; height: 62px }

.vs-seg {
  position: relative; display: flex; flex-direction: column; justify-content: center;
  height: 100%; padding: 0 12px; overflow: hidden;
  border-radius: 10px; background: var(--card-2); border: 1px solid var(--line);
  /* Grows into place the first time the section is seen. */
  width: 0; transition: width .9s cubic-bezier(.2,.8,.25,1);
}
.fade.in .vs-seg { width: var(--p) }
.vs-seg:nth-child(2) { transition-delay: .08s }
.vs-seg:nth-child(3) { transition-delay: .16s }
.vs-seg:nth-child(4) { transition-delay: .24s }

.vs-seg i {
  font-style: normal; font-size: 12.5px; font-weight: 700; color: var(--sub);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.vs-seg em {
  font-style: normal; font-size: 13px; font-weight: 800; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
/* A tinted family, so the four read as one stack of other people's apps. */
.vs-row--them .vs-seg:nth-child(1) { background: color-mix(in srgb, var(--acc) 7%, var(--card-2)) }
.vs-row--them .vs-seg:nth-child(2) { background: color-mix(in srgb, var(--acc) 11%, var(--card-2)) }
.vs-row--them .vs-seg:nth-child(3) { background: color-mix(in srgb, var(--acc) 15%, var(--card-2)) }
.vs-row--them .vs-seg:nth-child(4) { background: color-mix(in srgb, var(--acc) 19%, var(--card-2)) }

.vs-seg--mine {
  background: var(--acc) !important; border-color: transparent;
  box-shadow: 0 10px 26px -12px color-mix(in srgb, var(--acc) 70%, transparent);
}
.vs-seg--mine i { color: color-mix(in srgb, var(--acc-ink) 72%, transparent) }
.vs-seg--mine em { color: var(--acc-ink) }

/* The saving, shown as the space the other three used to take. */
.vs-gap {
  flex: 1; display: flex; align-items: center; justify-content: center; height: 100%;
  border-radius: 10px; border: 1px dashed var(--line-2);
  font-size: 13px; color: var(--mut); text-align: center; padding: 0 10px;
}
.vs-gap b { color: var(--ink); font-weight: 800; margin-right: 5px; font-variant-numeric: tabular-nums }

.vs-total { text-align: right; font-size: 22px; font-weight: 850; letter-spacing: -.02em; font-variant-numeric: tabular-nums }
.vs-total span { display: block; margin-top: 2px; font-size: 12.5px; font-weight: 600; color: var(--mut); letter-spacing: 0 }
.vs-total--mine { color: var(--acc) }

.vs-key {
  display: flex; flex-wrap: wrap; gap: 10px 20px;
  margin: 26px 0 0; padding: 22px 0 0; border-top: 1px solid var(--line); list-style: none;
}
.vs-key li { display: flex; align-items: center; gap: 9px; font-size: 13.5px; color: var(--sub) }
.vs-key i {
  width: 10px; height: 10px; border-radius: 3px; flex: none;
  background: color-mix(in srgb, var(--acc) calc(7% + var(--i, 0) * 4%), var(--card-2));
  border: 1px solid var(--line);
}
.vs-key .mine { color: var(--ink); font-weight: 650 }
.vs-key .mine i { background: var(--acc); border-color: transparent }

.vs-note {
  margin: 22px 0 0; padding-top: 18px; border-top: 1px solid var(--line);
  font-size: 13px; line-height: 1.55; color: var(--mut); text-align: center;
}
.vs-note a { color: var(--sub); text-decoration: underline; text-underline-offset: 2px }

/* The rest of what's in there, as a quiet line rather than a card grid. */
.alsos { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 44px }
.alsos span {
  padding: 9px 15px; border-radius: 999px; font-size: 13.5px; font-weight: 600; color: var(--sub);
  background: var(--card); border: 1px solid var(--line);
}

@media (max-width: 820px) {
  .vs-row { grid-template-columns: 1fr; gap: 10px }
  .vs-label { display: flex; align-items: baseline; gap: 8px }
  .vs-label span { margin-top: 0 }
  .vs-total { text-align: left; font-size: 20px }
  .vs-total span { display: inline; margin-left: 8px }
  .vs-track { height: 54px }
  .vs-seg { padding: 0 8px }
  .vs-seg i { font-size: 11px }
  .vs-gap { font-size: 12px }
}
@media (max-width: 560px) {
  .vs-seg i { display: none }
  .vs-seg em { font-size: 12px }
}

/* ══ THE HANDSET, ON NIGHT ════════════════════════════════════════════════
   app-ui.css paints the frame graphite, which is right on paper and wrong
   here: a black phone on a near-black page is a silhouette with a screen
   floating inside it, and the thickness the frame works hard to draw goes
   with it. Under the dark theme it becomes brushed silver — six variables,
   no gradient rewritten — and every facet the frame already draws starts
   catching light instead of swallowing it. */
[data-theme="dark"] .ph {
  /* NATURAL TITANIUM, not chrome. The first pass ran the rail from #FDFDFE to
     #E4E7ED with a white side wall, and every corner where two of those stops
     met blew out into a white glare — a mirror, not a machined case. These are
     the same metal read at roughly half the luminance: a warm-grey body that
     is clearly lighter than the page without ever reaching white. */
  --rail-lit:  #A9AFB9;
  --rail-hi:   #8D939D;
  --rail-mid:  #585E68;
  --rail-low:  #3C414A;
  --rail-back: #757B85;
  --wall-lit:  #9BA1AB;
  --wall-dark: #32363D;
  /* A silver rail needs a DARK inner chamfer, not a light one — a white line
     against a white screen is nothing, and the bezel stops existing. */
  --bezel-ink: rgba(12, 14, 20, .44);
  --bezel-top: rgba(255, 255, 255, .30);
  /* How hard the plate's top facet catches the light. On graphite this can be
     generous; on a pale metal the same value is the corner glare. */
  --facet: .13;
}
[data-theme="dark"] .ph::after {
  background: radial-gradient(ellipse at center, rgba(2, 3, 8, .78), transparent 72%);
}

