/* ==========================================================================
   CELES GLOBAL — v2 styles
   --------------------------------------------------------------------------
   Aesthetic held from v1: cream / lavender / sans-serif, generous whitespace.
   What changed in this pass: a full-bleed hero, a two-column "connected
   system", a much larger app mockup, real sections for early access and
   clinics, a
   cinematic video break, an editorial bottom CTA — and one shared vertical
   rhythm so the lower page flows instead of floating in cream voids.

   CONTENTS
     0  Tokens
     1  Reset + primitives
     2  Buttons
     3  Nav + logo
     4  Hero (full bleed)
     5  Statement
    6b  The 3D marquee — shared component (clinics.html uses it)
     6  One connected system
     7  The Celes app
     8  Key features switcher
     9  Media pair used by the early-access split
    10  Celes Early Access (waitlist)
   10b  Waitlist form (.wl — mini + full)
    11  Clinic section
   11b  Founders
    12  Bottom CTA
    13  Footer
    14  Modals + forms
    15  Device frames + placeholders
    16  Motion utilities
    17  Responsive
   ========================================================================== */

/* ------------------------------------------------------------ 0. Tokens */
:root {
  /* Surfaces.
     --paper and --cream are the ONLY two section surfaces on the page, and
     they are deliberately within ~1% of each other: scenes alternate between
     them so no two neighbours share an exact edge colour, but the step is far
     below the threshold at which it reads as a change of paint. The hairline
     on .scene + .scene is what marks a boundary now, not a change of surface.
     Anything more separated than this made the scroll read as a stack of
     different pages instead of one continuous one.
     --white and --cream-2 are COMPONENT surfaces (cards, frames, insets), not
     section backgrounds — do not promote either to a scene. */
  --white:      #ffffff;
  --paper:      #fbf9f5;   /* section surface A  */
  --cream:      #faf9f6;   /* section surface B  */
  --cream-2:    #f4efe6;   /* card/frame insets  */

  /* Ink */
  --ink:        #1b1822;   /* display + primary  */
  --ink-2:      #3b3546;
  --ink-soft:   #6f6879;   /* body secondary     */
  --ink-mute:   #a8a1b3;   /* de-emphasised      */

  /* Lavender accent */
  --lav:        #c9b8f0;
  --lav-600:    #9179d2;
  --lav-700:    #7a61bd;
  --lav-300:    #e2d8f8;
  --lav-100:    #efeafb;
  --lav-50:     #f7f4fd;

  /* Lines */
  --line:       #e8e1d7;
  --line-2:     #ece7f5;

  /* Type */
  --font-head:  "Plus Jakarta Sans", system-ui, -apple-system, sans-serif;
  --font-body:  "Inter", system-ui, -apple-system, sans-serif;

  /* Shape + depth */
  --r-xs:  10px;
  --r-sm:  16px;
  --r:     24px;
  --r-lg:  34px;
  --shadow-xs: 0 2px 10px rgba(27, 24, 34, .05);
  --shadow-sm: 0 6px 26px rgba(27, 24, 34, .07);
  --shadow-md: 0 20px 60px rgba(27, 24, 34, .11);
  --shadow-lg: 0 40px 110px rgba(27, 24, 34, .16);

  /* Layout */
  --maxw:   1240px;
  --pad:    clamp(20px, 4vw, 40px);
  /* The bar itself, notch excluded. §20 re-derives --nav-h from this so that
     everything measuring against the nav clears the Dynamic Island too. */
  --nav-h-base: 78px;
  --nav-h:  var(--nav-h-base);

  /* ONE vertical rhythm for the whole page. Every scene pads from this scale,
     so the scroll cadence is even — no scene tiny, none marooned in dead
     space. --sec-y-sm is only for text-only scenes that need less room. */
  --sec-y:       clamp(96px, 10vw, 168px);
  --sec-y-sm:    clamp(72px, 7.5vw, 124px);
  --sec-y-tight: clamp(56px, 6vw, 88px);

  /* Motion — one ease for everything so the site feels like one object */
  --ease: cubic-bezier(.22, 1, .36, 1);
  --ease-soft: cubic-bezier(.4, 0, .2, 1);
}

/* ------------------------------------------------- 1. Reset + primitives */
*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--nav-h);
  -webkit-text-size-adjust: 100%;
}

/* Lenis (smooth scrolling). It sets these classes on <html> itself; native
   smooth-behaviour has to be off or the two fight over the same position. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-smooth [data-lenis-prevent] { overscroll-behavior: contain; }
.lenis.lenis-stopped { overflow: hidden; }

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.62;
  color: var(--ink);
  background: var(--cream);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

/* Set by JS while a modal is open, so the page behind can't scroll. */
body.is-locked { overflow: hidden; }

img, video, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* [hidden] must beat any component that sets its own `display` — several here
   do (.form, .modal, .panel), and without this they stay visible when hidden. */
[hidden] { display: none !important; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
}

:focus-visible {
  outline: 2px solid var(--lav-600);
  outline-offset: 3px;
  border-radius: 6px;
}

/* Display voice — lighter weight + tight tracking reads far more premium
   than the heavy 800 the first version used. */
h1, h2, h3, h4 {
  font-family: var(--font-head);
  font-weight: 600;
  margin: 0;
  line-height: 1.02;
  letter-spacing: -0.035em;
}

.container {
  width: 100%;
  max-width: var(--maxw);
  margin: 0 auto;
  padding: 0 var(--pad);
}

/* ---------------------------------------- 1c. One screen, content centred
   THE FIX FOR "STUCK HALFWAY BETWEEN TWO SECTIONS". A section shorter than
   the viewport is the whole cause of it: stop anywhere near its edge and the
   screen is showing the bottom of one thing and the top of the next, with
   neither composed. No amount of scroll tuning fixes that, and the
   programmatic snap that used to paper over it (script.js §6e) was worse than
   the problem. A section that is AT LEAST one screen tall cannot produce the
   state at all.

   THE PADDING IS INSIDE THE HEIGHT, NOT ADDED TO IT. `box-sizing: border-box`
   is global (§1), so min-height counts the padding: a section with short
   content is exactly 100svh and a pinned scene locked at `top top` fits the
   window it is locked into. Add the padding on top instead and every pinned
   section is taller than the screen pinning it, which is how step 03 came to
   be cut off the bottom of the step stream.

   THE TOP PAD CLEARS THE NAV. It is fixed and translucent over every scene,
   so the floor on the vertical padding is its height plus a margin — content
   centred in the box is then never underneath it, at any window height.

   svh, NOT vh. On mobile Safari a 100vh box is taller than the visible
   viewport while the URL bar is showing; svh is the small (bar-visible)
   viewport, so nothing is pushed under the fold on first paint. §20 takes
   the same measurement to dvh where the address bar actually moves. */
.section {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: max(var(--sec-y), calc(var(--nav-h) + clamp(24px, 4vh, 56px)));
  padding-inline: 0;
}
/* Flex children stretch by default, so the container keeps its own width and
   its own max-width — this only stops a stray inline-level child collapsing. */
.section > * { width: 100%; }

/* --------------------------------------------------------------- Scenes
   Every top-level block is a scene: a self-contained scroll stop that fully
   clips its own contents. `overflow: hidden` is the rule that ends the
   bleed-through — decorative blooms, the hero footage and any oversized media
   are now cut at their scene's edge instead of spilling into the next one.
   The hairline + a change of surface make each boundary legible, so scrolling
   reads as arriving somewhere new rather than one continuous wash. Surfaces
   alternate paper/cream down the page (set per scene below), so no two
   neighbours ever share an edge colour. */
.scene {
  position: relative;
  overflow: hidden;
  isolation: isolate;   /* keeps a scene's stacking context to itself */

  /* One scene per screen. Short scenes (the statement especially) would
     otherwise share a viewport with their neighbours, which is the crammed
     feeling we are fixing. Taller scenes simply grow past this and are
     unaffected — the content is centred either way, so the extra room reads
     as deliberate breathing space rather than a dead gap. */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.scene + .scene,
main + .scene { border-top: 1px solid var(--line); }

.eyebrow {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .72rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 20px;
}
.eyebrow--accent { color: var(--lav-600); }

.section__head {
  max-width: 720px;
  margin: 0 auto clamp(46px, 5.5vw, 76px);
  text-align: center;
}
.section__title { font-size: clamp(2rem, 4.4vw, 3.4rem); }
.section__lead {
  margin-top: 22px;
  font-size: 1.1rem;
  color: var(--ink-soft);
}

/* Small status tag — used for "Coming soon" */
.tag {
  display: inline-block;
  margin-left: 8px;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--lav-100);
  color: var(--lav-700);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .6rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  vertical-align: middle;
  white-space: nowrap;
}
.tag--inline { margin-left: 10px; }

/* ----------------------------------------------------------- 2. Buttons */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .92rem;
  letter-spacing: -.01em;
  padding: 13px 26px;
  border-radius: 999px;
  border: 1.5px solid transparent;
  cursor: pointer;
  white-space: nowrap;
  transition:
    transform .35s var(--ease),
    background .3s var(--ease-soft),
    color .3s var(--ease-soft),
    border-color .3s var(--ease-soft),
    box-shadow .35s var(--ease);
}
.btn--lg    { padding: 16px 34px; font-size: 1rem; }
.btn--block { width: 100%; }

.btn--primary {
  background: var(--lav);
  color: #241f33;
  box-shadow: 0 10px 26px rgba(145, 121, 210, .28);
}
.btn--primary:hover {
  background: var(--lav-600);
  color: #fff;
  transform: translateY(-2px);
  box-shadow: 0 16px 36px rgba(145, 121, 210, .38);
}

.btn--outline {
  background: transparent;
  color: var(--ink);
  border-color: rgba(27, 24, 34, .22);
}
.btn--outline:hover {
  background: var(--ink);
  color: var(--cream);
  border-color: var(--ink);
  transform: translateY(-2px);
}
.btn--outline-light { color: #fff; border-color: rgba(255,255,255,.42); }
.btn--outline-light:hover { background: #fff; color: var(--ink); border-color: #fff; }

/* Solid white — for use on the dark CTA image block */
.btn--light {
  background: #fff;
  color: var(--ink);
  box-shadow: 0 12px 34px rgba(0, 0, 0, .28);
}
.btn--light:hover {
  background: var(--lav);
  color: #241f33;
  transform: translateY(-2px);
}

.btn--dark {
  background: var(--ink);
  color: var(--cream);
}
.btn--dark:hover { background: #000; transform: translateY(-2px); }

.btn--ghost {
  background: transparent;
  color: var(--ink);
  border-color: rgba(27, 24, 34, .16);
}
.btn--ghost:hover { border-color: var(--ink); background: rgba(27,24,34,.04); }

/* -------------------------------------------------------- 3. Nav + logo */
.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 90;
  height: var(--nav-h);
  display: flex;
  align-items: center;
  transition: background .4s var(--ease-soft), box-shadow .4s var(--ease-soft);
}
/* The hero video runs edge to edge underneath the bar, and the subject's head
   sits directly behind the links. This is a light scrim — it lifts the nav off
   the footage without dimming it, and cross-fades out once .nav--solid
   takes over on scroll. */
.nav::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(255, 255, 255, .92) 0%,
    rgba(255, 255, 255, .66) 58%,
    rgba(255, 255, 255, 0) 100%);
  transition: opacity .4s var(--ease-soft);
}
.nav--solid::before { opacity: 0; }
/* Logo hard-left, links right-of-centre, buttons hard-right.
   max-width matches .hero__inner (1680px) rather than the narrower page
   container, which is what puts the logo on exactly the same left edge as the
   "Built with dermatologists" pill below it — they share one vertical line.
   margin-right:auto on the logo pushes the other two groups over as a pair,
   and the tight gap keeps the links hard against the buttons on the right. */
.nav__inner {
  width: 100%;
  max-width: 1680px;
  margin: 0 auto;
  padding: 0 var(--pad);
  display: flex;
  align-items: center;
  gap: clamp(18px, 2.4vw, 34px);
}
.nav__inner > .logo { margin-right: auto; }

/* --- the Celes mark ------------------------------------------------------
   Petal/drop inside an open "C" arc. Drawn once as a <symbol> in index.html
   and pulled in with <use>, so nav and footer share one source of truth. */
.logo {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  flex: none;
}
.logo__mark {
  width: 27px;
  height: 27px;
  flex: none;
  color: var(--lav-600);
  transition: transform .5s var(--ease);
}
.logo:hover .logo__mark { transform: rotate(-8deg) scale(1.06); }
.logo__word {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1.4rem;
  letter-spacing: -.04em;
}
/* Slightly larger lockup where the logo stands alone rather than in a bar */
.logo--footer .logo__mark { width: 30px; height: 30px; }

.nav__links { display: flex; gap: clamp(20px, 2.2vw, 32px); flex: none; }
.nav__links a {
  position: relative;
  font-weight: 500;
  font-size: .93rem;
  color: var(--ink-2);
  transition: color .25s var(--ease-soft);
}
/* Hairline that grows from the middle on hover */
.nav__links a::after {
  content: "";
  position: absolute;
  left: 50%; bottom: -6px;
  width: 0; height: 1.5px;
  background: var(--lav-600);
  transform: translateX(-50%);
  transition: width .35s var(--ease);
}
.nav__links a:hover { color: var(--ink); }
.nav__links a:hover::after { width: 100%; }

.nav__actions { display: flex; gap: 10px; flex: none; }
.nav__actions .btn { padding: 10px 20px; font-size: .86rem; }

.nav--solid {
  background: rgba(253, 252, 250, .82);
  backdrop-filter: saturate(180%) blur(14px);
  -webkit-backdrop-filter: saturate(180%) blur(14px);
  box-shadow: 0 1px 0 rgba(27,24,34,.07), 0 10px 30px rgba(27,24,34,.05);
}

/* Mobile menu */
.nav__toggle {
  display: none;
  flex-direction: column;
  gap: 5px;
  padding: 8px;
  flex: none;
}
.nav__toggle span {
  width: 23px; height: 2px;
  background: var(--ink);
  border-radius: 2px;
  transition: transform .35s var(--ease), opacity .2s;
}
.nav__toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav__toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav__toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

.nav__mobile {
  position: fixed;
  top: var(--nav-h); left: 0; right: 0;
  z-index: 89;
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 18px var(--pad) 28px;
  background: var(--paper);
  border-bottom: 1px solid var(--line);
  box-shadow: var(--shadow-md);
  transform: translateY(-120%);
  transition: transform .45s var(--ease);
}
.nav__mobile.open { transform: translateY(0); }
.nav__mobile a:not(.btn) {
  padding: 13px 4px;
  font-weight: 600;
  border-bottom: 1px solid var(--line);
}
.nav__mobile .btn { margin-top: 10px; }

/* --------------------------------------------------- 4. Hero (full bleed)
   The footage is the stage. 100vw, edge to edge, no max-width and no side
   padding boxing it in — the standard full-bleed idiom, so it stays correct
   even if a container is ever wrapped around the section. */
.hero {
  position: relative;
  width: 100vw;
  max-width: 100vw;
  margin-left: calc(50% - 50vw);
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;   /* content sits higher, vertically balanced */
  overflow: hidden;
  /* Sampled from the footage's own studio backdrop, which falls off left to
     right (#fafafa → #f5f5f5). Matching that gradient makes the letterboxing
     below seamless — you cannot see where the video frame ends. */
  background: linear-gradient(to right, #fafafa 0%, #f7f7f7 50%, #f5f5f5 100%);
}

/* The footage: no overlay, no scrim, no mask fade — shown exactly as shot.
   `contain` rather than `cover` is what zooms the model out: cover always
   fills the height, which pinned the top of her head to the very top of the
   viewport and pushed her hair under the nav bar. `contain` shows the whole
   frame and parks it against the bottom, so the empty margin lands above her
   head — clean space between the nav and her hair at any window shape. */
.hero__stage {
  position: absolute;
  /* nav height plus a deliberate breathing gap, so the top of her head never
     runs up against the bar even when `contain` is fitting by height. */
  top: calc(var(--nav-h) + clamp(18px, 4vh, 52px));
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 0;
  pointer-events: none;
}
.hero__video {
  width: 100%;
  height: 100%;
  object-fit: contain;
  object-position: center bottom;
}

/* --- analysis reticles ---------------------------------------------------
   Anchored to the centre line rather than to page percentages, so they stay
   clustered on the subject at any viewport width. */
.scan {
  position: absolute;
  left: 50%;
  width: clamp(70px, 6.6vw, 100px);
  aspect-ratio: 5 / 4;
  border: 1px solid rgba(255, 255, 255, .92);
  border-radius: 2px;
  animation: drift 9s var(--ease-soft) infinite;
  opacity: 0;
}
.scan--a { top: 27%; margin-left: -8vmin;  animation-delay: 0s; }
.scan--b { top: 43%; margin-left: 2.5vmin; animation-delay: 1.1s; width: clamp(78px, 7.4vw, 112px); }
.scan--c { top: 59%; margin-left: -12vmin; animation-delay: 2.2s; }
.is-ready .scan { opacity: 1; transition: opacity .9s var(--ease) .5s; }

.scan__dot {
  position: absolute;
  top: 4px; left: 4px;
  width: 5px; height: 5px;
  background: #fff;
  box-shadow: 0 0 6px rgba(0,0,0,.35);
}
.scan__label {
  position: absolute;
  left: 6px; bottom: 5px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .6rem;
  letter-spacing: .01em;
  color: #fff;
  text-shadow: 0 1px 7px rgba(0, 0, 0, .55);
  white-space: nowrap;
}

/* Uses the standalone `translate` property, not `transform`: that leaves
   `transform` free for GSAP's draw-in scale, and the browser composes the
   two rather than one clobbering the other. */
@keyframes drift {
  0%, 100% { translate: 0 0; }
  33%      { translate: 5px -7px; }
  66%      { translate: -4px 5px; }
}

/* --- trust pill --------------------------------------------------------- */
.trust {
  position: absolute;
  top: calc(var(--nav-h) + 14px);
  left: max(var(--pad), calc(50vw - 840px + var(--pad)));
  z-index: 3;
  display: inline-flex;
  align-items: stretch;
  border-radius: 999px;
  background: rgba(255, 255, 255, .74);
  border: 1px solid var(--line);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--shadow-xs);
}
/* Avatar stack — PLACEHOLDER: drop real <img> in place of the <i> dots */
.trust__faces {
  display: inline-flex;
  align-items: center;
  padding: 7px 4px 7px 10px;
}
.trust__faces i {
  width: 21px; height: 21px;
  border-radius: 50%;
  border: 2px solid #fff;
  margin-right: -7px;
  background: linear-gradient(140deg, var(--lav-300), var(--lav-600));
}
.trust__faces i:nth-child(2) { background: linear-gradient(140deg, #f4d9c9, #d9a184); }
.trust__faces i:nth-child(3) { background: linear-gradient(140deg, #d5e3f5, #8fa8c9); }
.trust__text {
  display: inline-flex;
  align-items: center;
  padding: 8px 17px 8px 15px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: .79rem;
  color: var(--ink-2);
  white-space: nowrap;
}

/* --- foreground: copy bottom-left, rail bottom-right -------------------- */
/* Copy and rail both ride high in the frame rather than sitting on the floor
   of it — the video's empty upper margin is exactly where they belong. */
.hero__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 1680px;
  margin: 0 auto;
  padding: clamp(96px, 15vh, 150px) var(--pad) 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: clamp(24px, 6vw, 90px);
}

.hero__copy { max-width: 34rem; }
.hero__copy .eyebrow { color: var(--lav-600); }

.hero__title {
  font-size: clamp(2.4rem, 4.6vw, 4.1rem);
  font-weight: 600;
  line-height: .96;
  letter-spacing: -.045em;
}
/* Each line wipes up from behind a mask on load */
.hero__title > span {
  display: block;
  overflow: hidden;
  padding-bottom: .06em;   /* room for descenders inside the clip */
}
.hero__title > span > i {
  display: block;
  font-style: normal;      /* <i> is only a hook, not italics */
}

.hero__sub {
  margin-top: 24px;
  max-width: 30ch;
  font-size: 1.06rem;
  color: var(--ink-soft);
}
/* Was a row of two buttons. It now holds the mini waitlist with the clinics
   link beneath it, so it stacks: side by side, a 400px form and a large
   button crowd the hero copy column and wrap unpredictably between the
   breakpoints. The waitlist is the primary action and reads first. */
/* The hero's only action, since "For clinics" moved out to the nav. It is a
   single child now — the column direction and the gap are kept because the
   entrance in §17 animates this element and a later second action would slot
   in without re-deriving any of it. */
.hero__actions {
  margin-top: 32px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 18px;
}

/* --- pillar rail -------------------------------------------------------- */
.hero__rail {
  display: grid;
  gap: clamp(26px, 3.2vw, 42px);
  justify-items: end;
  text-align: right;
  flex: none;
}
.rail__item {
  display: grid;
  grid-template-columns: auto auto;
  grid-template-rows: auto auto;
  align-items: center;
  gap: 0 20px;
}
.rail__icon {
  grid-row: 1 / 3;
  width: 58px; height: 58px;
  display: grid; place-items: center;
  border-radius: var(--r-sm);
  background: rgba(247, 244, 253, .82);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: var(--lav-600);
}
.rail__icon svg { width: 30px; height: 30px; }

.rail__lead {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.85rem, 2.9vw, 2.6rem);
  line-height: 1;
  letter-spacing: -.04em;
}
.rail__note {
  margin-top: 7px;
  max-width: 22ch;   /* wraps to two lines without hard-coded <br> */
  font-size: .92rem;
  line-height: 1.42;
  color: var(--ink-soft);
}

/* --- rail, desktop only: one line per note -------------------------------
   SCOPED TO min-width 1121px ON PURPOSE. 1120 is the exact edge where §17
   re-lays the rail as a horizontal strip with the icon ABOVE the text, so
   this band is precisely the icon-left / text-right layout defined above and
   nothing here can reach tablet. Phones never paint this rail at all — the
   hero is replaced by .mhero below 768px — so mobile is untouched by
   construction, not by a second override.

   WHAT WAS WRONG. The 22ch cap forced every note to two lines, which is what
   made the three rows read as uneven. It was buying space the layout does not
   need: at 1121px there are ~400px of empty gutter between .hero__copy and
   the rail, and the widest note needs ~250px of it. So the cap comes off, the
   note goes nowrap, and the type takes one step up.

   WHY justify-items/1fr AND NOT JUST nowrap. Unwrapped, the three notes are
   different lengths. The rail end-justifies its items, so each would size to
   its own content and the icons would stagger by ~15px — the section would
   end up less aligned than before. Stretching every item to the rail's width
   and letting a 1fr text column absorb the difference pins all three icons to
   one left edge and the whole text column to one left edge with them.

   TEXT IS LEFT-ALIGNED HERE, and only here. The base rail is right-aligned
   (it is the right-hand rail of the hero), but once the notes run full length
   that leaves each short lead — "Daily" is ~100px against a ~258px column —
   stranded a long way from its own icon. Left-aligning puts each lead
   directly above the start of its note and hard against the icon, which is
   the alignment the section was asked for. The trade is a ragged right edge
   against the page margin, which is accepted. Below 1121px the base
   right-alignment is untouched. */
@media (min-width: 1121px) {
  .hero__rail {
    justify-items: stretch;
    text-align: left;     /* overrides the base right-alignment, desktop only */
  }

  .rail__item {
    grid-template-columns: auto 1fr;
    column-gap: 28px;     /* was 20 — slightly more air between icon and text */
  }

  .rail__note {
    max-width: none;      /* the 22ch cap is what forced the wrap */
    white-space: nowrap;
    font-size: 1rem;      /* 14.72px -> 16px: one step up, still far below the
                             lead's 2.6rem, so the heading stays dominant */
    line-height: 1.5;
    margin-top: 9px;
  }
}

/* --- scroll cue --------------------------------------------------------- */
.scrollcue {
  position: absolute;
  left: 50%; bottom: 22px;
  z-index: 3;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 9px;
  font-family: var(--font-head);
  font-size: .64rem;
  font-weight: 600;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
.scrollcue__rail {
  width: 21px; height: 34px;
  border: 1.5px solid var(--ink-mute);
  border-radius: 12px;
  position: relative;
}
.scrollcue__dot {
  position: absolute;
  top: 6px; left: 50%;
  width: 3px; height: 6px;
  border-radius: 3px;
  background: var(--ink-mute);
  transform: translateX(-50%);
  animation: cue 1.8s var(--ease-soft) infinite;
}
@keyframes cue {
  0%, 100% { opacity: 0; transform: translate(-50%, 0); }
  45%      { opacity: 1; transform: translate(-50%, 9px); }
}

/* --------------------------------------------------------- 5. Statement */
.statement {
  padding: var(--sec-y-sm) 0;
  /* Flat, not a gradient: the old white→cream wash ran straight into the
     scene below and erased the boundary.

     --paper, not --white. This was the last scene on the page still sitting
     on #ffffff, and it was the one place the surface rule in §0 was broken —
     pure white against the cream either side of it is a step of ~2.5%, which
     is small in numbers and clearly visible in practice, because it lands
     between the hero and "how it works" as a bright band you scroll through.
     Every other scene alternates --paper/--cream; this one now does too, and
     the boundary is carried by the .scene + .scene hairline like everywhere
     else rather than by a change of paint. */
  background: var(--paper);
}
/* ======================================================================
   6b. THE 3D MARQUEE — a shared component
   ----------------------------------------------------------------------
   PAGE-AGNOSTIC ON PURPOSE. Nothing below knows where it is mounted: the
   component is `.marquee` and its tiles, it sizes itself from its own frame,
   and the only thing a host page supplies is a box to put it in. It was built
   for the homepage, moved to a showcase band mid-page on the CLINICS page, and
   is now that page's full-viewport HERO backdrop (clinics.html §2). Both moves
   cost nothing here because the geometry never depended on the section around
   it — the second one is carried entirely by `.marquee--bleed` in clinics.css
   §2b, which redeclares the frame, the deck size, the column count and the
   drift rate without touching a line below. Keep it that way: anything
   specific to a host page belongs in that page's stylesheet.

   A port of Aceternity's ThreeDMarquee. The original is React + Tailwind +
   framer-motion; none of those exist on this page, so what is reproduced is
   the geometry, not the code:

     a fixed-size deck, tipped by rotateX + rotateZ into an isometric plane,
     holding gridded columns that drift vertically in opposite directions at
     different speeds, all clipped by a much smaller frame.

   Three deliberate departures.

   1. INSIDE THE DECK, LENGTHS ARE FIXED px — not clamps, not vw. The deck is
      always 2560 x 2160 and is fitted to the viewport by scaling the whole
      plane (--mq-s). That is the only way a tipped 3D surface keeps its
      proportions: scale one number and every tile, gap, radius and type size
      travels with it. Responsive rules at the bottom only ever touch --mq-s.

   2. SIX COLUMNS OF NINE, against the reference's four of eight. Not a
      styling preference — a tipped plane projects to a RHOMBUS, and a rhombus
      only covers the rectangle framing it while it stays large against that
      frame. With the reference's proportions the scale cannot go below ~0.9
      before the frame's own corners show through, which is why it reads as
      three big plates rather than a grid. A wider deck buys the headroom to
      scale down to 0.66 and show five or six tiles across instead.

   3. IT IS LIGHT. The component ships dark — neutral-800 ground, dark ring,
      photographic tiles. Every surface, ring, glow and globe here is cream /
      lavender, and the tiles are DOM rather than 31 remote screenshots, which
      is what lets them carry Celes copy and cost nothing to load.

   The drift is CSS keyframes on `translate` alone, so it composites off the
   main thread and stands down under prefers-reduced-motion with one rule.
   ====================================================================== */
/* ------------------------------------------------- 6b-0. the word split ---
   Shared by every line that gets the blur-in treatment — the app scene's
   opening statement (script.js §5b) and any headline that opts into the
   standalone version by carrying [data-intro-words] (script.js §5d). JS
   replaces the line's text with one `.iw` per word so each can be transformed
   on its own.

   Words are inline-BLOCK: an inline box cannot be transformed, and a block
   would put every word on its own line.

   The muted start colour is scoped to .motion — set only when GSAP has
   actually taken over (script.js §1c). If the scripts never arrive the line
   stays fully inked and readable rather than a row of grey that nothing is
   ever going to light. The animations scrub colour, blur, offset and opacity
   together. */
.iw { display: inline-block; }
.motion .iw {
  color: var(--ink-mute);
  will-change: filter, transform, opacity;
}

/* ------------------------------------------- 0c. the character split ------
   The four letter-by-letter headlines (script.js §0b): "One connected system
   for your skin.", "Introducing the Celes app.", "Your daily skin companion."
   and "Built around how skin actually works."

   TWO LEVELS, and both are load-bearing:

     .sw  one per WORD. Inline-block, which is what stops the line breaking
          between two letters of the same word — with a flat list of letters
          every single one becomes a break opportunity and long words split
          across lines at narrow widths.
     .sc  one per CHARACTER. Inline-block for the same reason .iw is: an
          inline box cannot be transformed at all.

   `will-change` is declared here and RETRACTED BY JS once each headline has
   landed (releaseChars in §0b). It is not free — these lines run to ~37
   letters each and four of them held permanently is well over a hundred
   composited layers for animations that are over in half a second.

   overflow-wrap on the word keeps the old behaviour for a word longer than
   the column, which is the one case the break opportunity was doing something
   useful. Under no-JS / reduced motion nothing is ever split and none of this
   applies — the headings stay plain headings. */
.sw {
  display: inline-block;
  overflow-wrap: anywhere;
}
.sc {
  display: inline-block;
  will-change: transform, opacity, filter;
}

/* ---------------------------------------------------- 6b-i. the frame ----
   The window onto the plane. Everything past its edges is cut, which is what
   makes the grid read as a fragment of something much larger. */
.marquee {
  --mq-s: .72;                          /* the one responsive dial */
  position: relative;
  width: min(1240px, 94vw);
  /* The height is part of the coverage budget, not a free choice: every pixel
     added here is rhombus the plane has to reach, and past a point --mq-s has
     to grow to keep up, which costs tiles on screen. 640 is where the two
     stop trading against each other. */
  height: clamp(430px, 47vw, 640px);
  margin: 0 auto;
  border-radius: var(--r-lg);
  overflow: hidden;
  isolation: isolate;
  background:
    radial-gradient(120% 90% at 18% 6%, var(--white), transparent 62%),
    radial-gradient(90% 80% at 88% 96%, var(--lav-50), transparent 66%),
    var(--cream);
  box-shadow:
    inset 0 0 0 1px var(--line-2),
    0 1px 0 rgba(255, 255, 255, .9) inset,
    var(--shadow-md);
  /* A LONG lens, deliberately. The reference sets no perspective at all, so
     its plane is an orthographic shear — every tile identical, no depth.
     Adding some is the improvement; adding a normal amount is the trap. The
     deck's far corners sit ~900px off the plane, so a 1600px perspective
     would magnify the near corner ~2.3x against the far one and the grid
     stops reading as one surface. At 5200px the spread is about 1.2x to
     0.85x: felt as depth, never as a fisheye. */
  perspective: 5200px;
}

/* Soft cream fade at all four edges: the tiles dissolve into the frame
   instead of being guillotined by it. A gradient, never filter: blur() — a
   blurred layer this size is re-rasterised on every drifting frame. */
.marquee::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  border-radius: inherit;
  background:
    linear-gradient(to bottom,
      rgba(250, 247, 242, .96), rgba(250, 247, 242, 0) 11%,
      rgba(250, 247, 242, 0) 89%, rgba(250, 247, 242, .96)),
    linear-gradient(to right,
      rgba(250, 247, 242, .94), rgba(250, 247, 242, 0) 8%,
      rgba(250, 247, 242, 0) 92%, rgba(250, 247, 242, .94));
}

/* ----------------------------------------------------- 6b-ii. the deck ---
   2560 x 2160, centred in the frame, then tipped. The rotate pair is the whole
   illusion: X lays the plane down, Z spins it so the columns run diagonally.
   48°/-42° is a shade shallower than the reference's 55°/-45° — enough tilt to
   read as a surface, little enough that the statements are still legible,
   which matters here because these tiles carry copy and its did not. */
.marquee__deck {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 2560px;
  height: 2160px;
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 34px;
  transform-style: preserve-3d;
  transform:
    translate(-50%, -50%)
    scale(var(--mq-s))
    rotateX(48deg) rotateZ(-42deg);
}

/* align-self: center collapses each column to its content height and centres
   it in the 2160px row. That is what supplies the drift's slack: nine tiles
   plus gaps come to ~2860px, so each column overruns the deck by ~350px at
   both ends, and the travel below can never expose a column's first or last
   tile edge. Any change to the tile count has to be checked against it. */

/* The drift: opposite directions, mismatched periods. Because the four
   durations are prime-ish to each other the grid never resolves into a single
   synchronised sweep — it just keeps breathing. Slow on purpose; this is a
   backdrop to a headline, not a carousel.

   NOT gated on .motion. That class marks what GSAP takes over, and nothing
   here is GSAP's — same contract as the app scene's float (§7e): ambient CSS
   motion runs whether or not the scripts arrive, and answers to
   prefers-reduced-motion alone. */
.marquee__col {
  display: flex;
  flex-direction: column;
  align-self: center;
  gap: 34px;
  will-change: translate;
  animation: mqdrift var(--mq-t) var(--ease-soft) infinite alternate;
}
.marquee__col:nth-child(1) { --mq-t: 27s; --mq-y:  74px; }
.marquee__col:nth-child(2) { --mq-t: 34s; --mq-y: -66px; }
.marquee__col:nth-child(3) { --mq-t: 30s; --mq-y:  62px; }
.marquee__col:nth-child(4) { --mq-t: 38s; --mq-y: -78px; }
.marquee__col:nth-child(5) { --mq-t: 32s; --mq-y:  68px; }
.marquee__col:nth-child(6) { --mq-t: 41s; --mq-y: -72px; }

@keyframes mqdrift {
  from { translate: 0 0; }
  to   { translate: 0 var(--mq-y); }
}

/* --------------------------------------------------- 6b-iii. the tiles ---
   One base, then a family of surfaces. 970/700 is the reference's plate
   ratio, kept so the grid has the same rhythm even though nothing in it is a
   screenshot any more. */
.mtile {
  position: relative;
  flex: none;
  aspect-ratio: 970 / 700;
  border-radius: 18px;
  overflow: hidden;
  background: var(--white);
  /* A hairline ring AND a drop shadow. On a cream ground a white tile with
     only a shadow has no edge where it sits over another white tile, and the
     grid turns to fog — the ring is what keeps fifty-four overlapping plates
     legible as separate objects. */
  box-shadow:
    inset 0 0 0 1px rgba(27, 24, 34, .075),
    0 22px 44px rgba(27, 24, 34, .08);
}

/* --- statements ---------------------------------------------------------
   Set flush to the bottom-left like a caption plate, so a one-word tile and
   a three-line tile share a baseline and the column reads as a set. */
.mtile--text {
  display: flex;
  align-items: flex-end;
  padding: 34px 36px;
  background:
    linear-gradient(160deg,
      var(--white) 0%, var(--paper) 38%,
      var(--lav-50) 72%, var(--lav-100) 100%);
}
/* 38px is sized for the projection, not the page: at --mq-s .66, tipped and
   foreshortened, this lands at roughly the 24px it needs to read as a
   statement rather than a caption. */
.mtile--text p {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 38px;
  line-height: 1.12;
  letter-spacing: -.034em;
  color: var(--ink);
  text-wrap: balance;
}

/* The lavender counterpart. Every third or fourth card, so the grid has a
   pulse of colour without ever becoming a lavender grid. */
.mtile--lav {
  background:
    radial-gradient(120% 120% at 12% 8%, rgba(255, 255, 255, .34), transparent 58%),
    linear-gradient(155deg, var(--lav-600) 0%, #a58ce0 46%, var(--lav) 100%);
  box-shadow:
    inset 0 0 0 1px rgba(145, 121, 210, .34),
    0 18px 44px rgba(122, 97, 189, .2);
}
.mtile--lav p { color: #fff; }

/* --- brand plate --------------------------------------------------------
   The one tile that is pure identity: mark and wordmark, nothing else. */
.mtile--brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 18px;
  background:
    radial-gradient(100% 100% at 50% 0%, var(--white), var(--lav-50) 72%, var(--lav-100));
}
.mtile__mark {
  width: 62px;
  height: 62px;
  flex: none;
  color: var(--lav-600);
}
.mtile--brand span {
  font-family: var(--font-head);
  font-weight: 800;
  font-size: 58px;
  letter-spacing: -.05em;
  color: var(--ink);
}

/* --- glow tiles ---------------------------------------------------------
   The spotlight / glowing-effect card, rebuilt as two offset radials that
   breathe against each other. No blur filter and no canvas: at this count the
   softness has to live in the gradient stops, because a filtered layer is
   re-rasterised on every frame the deck drifts and there are four decks'
   worth of tiles moving at once. */
.mtile--glow {
  background: var(--lav-100);
}
.mtile--glow::before {
  content: "";
  position: absolute;
  inset: -34% -18%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    #b6a1ea 0%, rgba(183, 162, 235, .6) 40%, transparent 76%);
  animation: mqbreathe var(--mq-bt, 12s) var(--ease-soft) var(--mq-bd, 0s) infinite alternate;
}
.mtile--glow-1 { --mq-bt: 11s; --mq-bd: -3s; }
.mtile--glow-2 { --mq-bt: 14s; --mq-bd: -7s; }
/* The floor is .82, not 0. These are surfaces in a grid, not a fade in and
   out — a tile that spends part of its cycle near-white is a hole in the
   composition, and with staggered delays there is always one of them there. */
@keyframes mqbreathe {
  from { transform: scale(.88); opacity: .82; }
  to   { transform: scale(1.1);  opacity: 1; }
}

/* The second radial. On the first tile it is a warm blush in the corner; on
   the second it also carries the sparkle dust — the nod to the sparkles
   component, four static dots on a slow twinkle rather than a particle loop
   with a rAF per tile. Both cases are one element and one background stack,
   which is why they share a pseudo instead of asking for a third. */
.mtile--glow::after {
  content: "";
  position: absolute;
  inset: 0;
}
.mtile--glow-1::after {
  background: radial-gradient(closest-side at 74% 82%,
    rgba(240, 200, 214, .8) 0%, rgba(240, 200, 214, .3) 46%, transparent 78%);
  animation: mqbreathe 16s var(--ease-soft) -6s infinite alternate;
}
.mtile--glow-2::after {
  background:
    radial-gradient(2.6px 2.6px at 22% 30%, rgba(255, 255, 255, .95), transparent 100%),
    radial-gradient(2px   2px   at 68% 22%, var(--lav-600),            transparent 100%),
    radial-gradient(3px   3px   at 79% 71%, rgba(255, 255, 255, .95), transparent 100%),
    radial-gradient(1.8px 1.8px at 38% 78%, var(--lav-600),            transparent 100%),
    radial-gradient(2.2px 2.2px at 54% 54%, rgba(255, 255, 255, .9),  transparent 100%),
    radial-gradient(closest-side at 78% 88%,
      rgba(240, 200, 214, .7) 0%, transparent 76%);
  animation: mqtwinkle 7s var(--ease-soft) -2s infinite alternate;
}
@keyframes mqtwinkle {
  from { opacity: .45; }
  to   { opacity: 1; }
}

/* --- aurora tiles -------------------------------------------------------
   A pastel wash three times the width of its tile, sliding slowly behind it.
   background-position is animated rather than a transform because the wash IS
   the tile's background — there is no extra layer to move, and at this size
   the repaint is cheaper than compositing another element per tile. */
.mtile--aurora {
  background:
    linear-gradient(115deg,
      var(--lav-100) 0%, #dfe8fa 22%, var(--lav-300) 44%,
      #fbe4ec 64%, #e6f2e8 82%, var(--lav-100) 100%);
  background-size: 320% 100%;
  animation: mqaurora var(--mq-at, 26s) var(--ease-soft) var(--mq-ad, 0s) infinite alternate;
}
.mtile--aurora-1 { --mq-at: 24s; --mq-ad: -4s;  background-position: 12% 50%; }
.mtile--aurora-2 { --mq-at: 31s; --mq-ad: -13s; background-position: 58% 50%; }
.mtile--aurora-3 { --mq-at: 28s; --mq-ad: -8s;  background-position: 84% 50%; }
@keyframes mqaurora {
  from { background-position: 0% 50%; }
  to   { background-position: 100% 50%; }
}

/* The meteor: one hairline streak crossing the aurora, then a long dark
   pause before it comes round again. `transform` holds the streak's angle and
   `translate` carries it across — two properties, so the travel never has to
   restate the rotation and lose it. */
.mtile--aurora::after {
  content: "";
  position: absolute;
  top: -26%;
  left: -34%;
  width: 46%;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, rgba(255, 255, 255, .95), transparent);
  transform: rotate(38deg);
  opacity: 0;
  animation: mqmeteor 11s var(--ease-soft) var(--mq-ad, 0s) infinite;
}
@keyframes mqmeteor {
  0%            { translate: 0 0;          opacity: 0; }
  6%            {                          opacity: .9; }
  30%           { translate: 190% 150%;    opacity: 0; }
  30.001%, 100% { translate: 0 0;          opacity: 0; }
}

/* --- globe --------------------------------------------------------------
   The reference's github-globe, reduced to what actually survives at tile
   size: a lavender wireframe on a halo, ringed by an orbit that turns. It
   carries the "global skin health" idea without a WebGL context per tile. */
.mtile--globe {
  display: grid;
  place-items: center;
  color: var(--lav-600);
  background:
    radial-gradient(closest-side, var(--white) 40%, var(--lav-50) 78%, var(--lav-100));
}
.mtile--globe::before {
  content: "";
  position: absolute;
  width: 66%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(201, 184, 240, .55), rgba(201, 184, 240, .16) 58%, transparent 76%);
}
/* The orbit. A dashed ring on a slow spin reads as rotation far more clearly
   than anything that could be done to the meridians themselves. */
.mtile--globe::after {
  content: "";
  position: absolute;
  width: 76%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 1.5px dashed rgba(145, 121, 210, .35);
  animation: mqspin 46s linear infinite;
}
.mtile--globe svg {
  position: relative;
  width: 52%;
  aspect-ratio: 1;
  opacity: .92;
}
@keyframes mqspin {
  from { rotate: 0deg; }
  to   { rotate: 360deg; }
}

/* --- stat cards ---------------------------------------------------------
   The floating stat cards from the app scene, shrunk to tile size. */
.mtile--stat {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Wider than it looks like it needs to be. Tipped 42° the number and its
     caption are no longer stacked squarely — the caption slides in under the
     number's own diagonal, and 6px of gap reads as a collision. */
  gap: 16px;
  /* The bottom padding is the bar's room. Without it the flex centring would
     hang the number over a rule it is supposed to sit above. */
  padding: 0 34px 48px;
  background:
    linear-gradient(150deg, var(--white), var(--paper) 62%, var(--lav-50));
}
.mtile__num {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-family: var(--font-head);
  font-weight: 800;
  font-size: 58px;
  line-height: 1;
  letter-spacing: -.05em;
  color: var(--ink);
  white-space: nowrap;
}
.mtile__num--up { color: var(--lav-700); }
.mtile__up {
  font-style: normal;
  font-size: 30px;
  color: #4f9d76;
}
.mtile__cap {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 17px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-mute);
}
/* The progress bar under the stat — the one moving part on these tiles. */
.mtile--stat::after {
  content: "";
  position: absolute;
  left: 34px;
  right: 34px;
  bottom: 34px;
  height: 8px;
  border-radius: 8px;
  background:
    linear-gradient(90deg, var(--lav-600) 0 var(--mq-fill, 45%),
                            var(--lav-100) var(--mq-fill, 45%) 100%);
}
/* The fill rides on the tile, not on its column — the tiles get re-dealt
   across the grid whenever the mix is re-balanced, and a fill keyed to a
   column position silently follows the wrong number when they do. */
.mtile--fill-16 { --mq-fill: 16%; }
.mtile--fill-30 { --mq-fill: 30%; }
.mtile--fill-45 { --mq-fill: 45%; }
.mtile--fill-75 { --mq-fill: 75%; }

/* NO SCREENSHOTS ON THIS DECK, of the app or of Clinic OS. They were tried and
   cut: at tile size, tipped 42° and drifting, a phone screen is unreadable
   detail — the Travel Mode map in particular read as a map, from some other
   product, rather than as one surface in this grid.

   NOR ANY UI DRAWN AS A SHAPE. The clinics page carried a dashboard tile
   for a while — a chrome bar, a panel and three rows, the dashboard reduced
   to something that survives the projection. It is gone, and the reason is
   editorial rather than visual: that page shows no product UI at all until
   real screenshots exist, and a window drawn in three rectangles is still a
   mockup. The vocabulary is statements and light. Anything that wants to say
   "software" says it in words on a statement tile.

   The app screens have a scene of their own on the homepage (§7) where they
   are large enough to actually be read. */

/* --- gradient-border card -----------------------------------------------
   The hover-border-gradient card, made autonomous: a conic sweep turning
   under an opaque face, so only the border shows it.

   The border is 3px, not the component's 1. On the flat page 1px is a
   jewel-line; laid into this plane and scaled to .72 it lands at well under
   a pixel and the tile just looks like a slightly grubby white rectangle.
   Everything on this deck is drawn for the projection, not for the page. */
.mtile--ring {
  display: grid;
  place-items: center;
  background: var(--white);
}
/* The sweep runs all the way round rather than fading to transparent for most
   of its turn. A single lit arc on a tipped tile does not read as a glowing
   border — it reads as one stray diagonal line drawn across a white card. The
   border is lavender the whole way round and a brighter spot travels along
   it, which is the effect the component is actually after. */
.mtile--ring::before {
  content: "";
  position: absolute;
  inset: -46%;
  background: conic-gradient(from 0deg,
    var(--lav-300) 0deg, var(--lav-600) 72deg, var(--lav) 132deg,
    var(--lav-300) 202deg, var(--lav-100) 280deg, var(--lav-300) 360deg);
  animation: mqspin 16s linear infinite;
}
.mtile--ring::after {
  content: "";
  position: absolute;
  inset: 3px;
  border-radius: 15px;
  background:
    radial-gradient(closest-side at 50% 42%, var(--white), var(--lav-50));
}
.mtile--ring span {
  position: relative;
  z-index: 2;
  width: 46%;
  aspect-ratio: 1;
  border-radius: 50%;
  border: 4px solid var(--lav-600);
  background: radial-gradient(closest-side,
    rgba(183, 162, 235, .5) 0%, rgba(201, 184, 240, .22) 58%, transparent 82%);
  box-shadow: 0 0 0 12px var(--lav-50);
}

.statement__text {
  position: relative;
  z-index: 2;
  width: min(1080px, 92vw);
  max-width: min(1080px, 92vw);
  margin: 0 auto;
  text-align: center;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: clamp(1.5rem, 3.4vw, 2.85rem);
  line-height: 1.18;
  letter-spacing: -.035em;
  color: var(--ink);
}
/* JS wraps each word; opacity is then driven directly by scroll position. */
/* Each word starts a readable light grey and is swept to solid ink by the
   scrubbed ScrollTrigger in script.js §1c. Colour, not opacity, so the
   sentence is legible the whole way through. */
.statement__text .w {
  color: var(--ink-mute);
  will-change: color;
}
.statement__text .w.is-read { color: var(--ink); }

/* --- the ambient cursor light ------------------------------------------
   A wide, very soft radial wash that follows the pointer through this
   scene. Two deliberate choices:

   1. The softness is in the GRADIENT, not a `filter: blur()`. A blurred
      600px layer has to be re-rasterised every frame it moves, which is
      exactly the cost that drops a 60fps section to 40. A multi-stop
      radial reads identically and composites for free.
   2. `multiply`, not screen/soft-light. This scene is flat white — screen
      and soft-light over white are mathematically no-ops, so they would
      render nothing at all. Multiplying a PALE tint into white lays down
      the same gentle wash the effect is after, and because the text and
      pills paint above it in the stack, neither is ever tinted.

   --glow-rgb is lerped by script.js between lavender and the tone of
   whichever pill the cursor is nearest. */
.statement__glow {
  --glow-rgb: 214, 200, 246;      /* lavender, the resting colour */
  position: absolute;
  top: 0;
  left: 0;
  width: 620px;
  height: 620px;
  margin: -310px 0 0 -310px;      /* so transform positions its centre */
  border-radius: 50%;
  background: radial-gradient(closest-side,
              rgba(var(--glow-rgb), .55),
              rgba(var(--glow-rgb), .34) 34%,
              rgba(var(--glow-rgb), .13) 58%,
              rgba(var(--glow-rgb), 0)   78%);
  mix-blend-mode: multiply;
  opacity: 0;                     /* JS fades it in with the pointer */
  z-index: 1;
  pointer-events: none;
  will-change: transform, opacity;
}

.statement__chips {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.chip {
  --s: 1;                          /* depth scale   */
  --rot: 0deg;                     /* resting tilt  */
  --o: 1;                          /* depth opacity */
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 9px 16px;
  border-radius: 12px;
  background: rgba(255, 255, 255, .94);
  box-shadow: var(--shadow-sm);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .85rem;
  letter-spacing: -.01em;
  color: var(--ink);
  white-space: nowrap;
  opacity: var(--o);
  /* Individual transform properties: the browser applies these BEFORE
     `transform`, so JS can own translate/rotate/scale for the drift and
     the repel without ever clobbering the depth the CSS set here. */
  scale: var(--s);
  rotate: var(--rot);
  pointer-events: auto;            /* the container is none; pills opt in */
  cursor: pointer;
  transition: box-shadow .5s var(--ease), background-color .5s var(--ease);
}

/* Tone per pill — drawn from the five palette dots, nothing new. --glow is
   the pale version the ambient light borrows. */
.chip[data-tone="lav"]   { --tone: 183, 157, 240; --glow: 214, 200, 246; }
.chip[data-tone="rose"]  { --tone: 240, 127, 174; --glow: 248, 210, 226; }
.chip[data-tone="mint"]  { --tone: 127, 214, 160; --glow: 208, 240, 220; }
.chip[data-tone="sky"]   { --tone: 127, 199, 240; --glow: 206, 231, 249; }
.chip[data-tone="amber"] { --tone: 240, 178, 127; --glow: 250, 226, 204; }

.chip__dot {
  width: 12px;
  height: 12px;
  border-radius: 4px;
  background: rgb(var(--tone));
  transition: box-shadow .45s var(--ease), filter .45s var(--ease);
}

/* Cursor nearby: the pill lifts off the page and its dot lights up. Set by
   script.js as a class, so the browser's own transition does the easing and
   the rAF loop stays a pure transform writer. */
.chip.is-near {
  background: #fff;
  box-shadow: var(--shadow-md);
}
.chip.is-near .chip__dot {
  filter: saturate(1.2) brightness(1.06);
  box-shadow:
    0 0 0 4px rgba(var(--tone), .16),
    0 0 14px 3px rgba(var(--tone), .55);
}

/* Click ripple — one per click, removed when its tween finishes. */
.chip__ripple {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 150%;
  aspect-ratio: 1;
  margin: -75% 0 0 -75%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
              rgba(var(--tone), .42), rgba(var(--tone), 0) 70%);
  pointer-events: none;
}

/* --- the composition ----------------------------------------------------
   Three depth tiers around the sentence: near pills sit in the outer
   corners, mid pills tuck in above and below the text, and two far pills
   hang at the edges. Everything stays out of the band the text occupies
   (roughly 38–62% of the scene's height). */

/* near — largest, sharpest, fully opaque */
.chip--1  { top: 14%;    left: 13%;  --s: 1.06; --rot: -4deg; }
.chip--2  { top: 11%;    right: 12%; --s: 1.02; --rot:  5deg; }
.chip--6  { bottom: 15%; left: 15%;  --s: 1.04; --rot:  3deg; }
.chip--7  { bottom: 11%; right: 15%; --s: 1;    --rot: -3deg; }

/* mid — a step back */
.chip--3  { top: 25%;    left: 33%;  --s: .88; --o: .84; --rot: -6deg; }
.chip--4  { top: 23%;    right: 31%; --s: .9;  --o: .84; --rot:  4deg; }
.chip--8  { bottom: 25%; right: 35%; --s: .86; --o: .8;  --rot:  5deg; }
.chip--9  { bottom: 27%; left: 34%;  --s: .88; --o: .82; --rot: -5deg; }

/* far — small, faded, a touch out of focus */
.chip--5  { top: 40%;    left: 4%;   --s: .76; --o: .56; --rot: -8deg; }
.chip--10 { bottom: 38%; right: 4%;  --s: .74; --o: .54; --rot:  7deg; }
.chip--5,
.chip--10 {
  filter: blur(.5px);
  box-shadow: var(--shadow-xs);
}

/* ------------------------------------------- 6. One connected system */
/* ONE SCREEN, HARD CONSTRAINT. This scene used to run ~978px against an 805px
   viewport, which put step 03 permanently below the fold at the exact scroll
   position where the section is centred — the loop was never visible as a
   loop. So the vertical budget is spent explicitly here, in vh, and every
   number below is part of it. Worst case, a 700px viewport:

     nav 78 + 12 gap · head ~170 · steps 3 × 108 + 2 × 10 · 20 bottom = ~624

   The photo no longer participates in that sum at all: it is capped in vh
   below, so the LEFT column sets the height and the right one fits itself to
   whatever is left. If you add anything to this scene, re-check that sum at
   700px before you decide it fits. */
.system {
  background: var(--cream);
  padding-block: calc(var(--nav-h) + clamp(12px, 2vh, 30px))
                 clamp(20px, 3vh, 46px);
  scroll-margin-top: var(--nav-h);
}
/* Soft lavender wash so the section has depth rather than flat cream */
.system__bloom {
  position: absolute;
  top: -14%; right: -12%;
  width: min(760px, 68vw);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(201, 184, 240, .34), transparent 72%);
  pointer-events: none;
}

/* The left column is now the wider of the two — 1.22 against .78. The photo
   was rebalanced, not shrunk for its own sake: at parity it read as the
   subject of the scene and the three steps as a caption beside it, which is
   backwards. This is the ratio at which the steps are unmistakably the
   content and the photo is unmistakably support. */
.system__grid {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.22fr) minmax(0, .78fr);
  gap: clamp(32px, 4.4vw, 64px);
  align-items: center;
}

.system__head { margin-bottom: clamp(18px, 2.6vh, 34px); }
.system__head .section__title {
  font-size: clamp(1.8rem, 2.4vw + .5rem, 2.7rem);
  line-height: 1.1;
}
.system__head .section__lead {
  max-width: 46ch;
  margin-top: clamp(10px, 1.4vh, 18px);
  font-size: clamp(.98rem, .9vw + .5rem, 1.06rem);
}

/* The letter entrance for this title lives in the shared rule at §0c. */

/* --- the steps, as a vertical connected loop ---------------------------- */
.steps {
  position: relative;
  display: grid;
  gap: clamp(8px, 1.2vh, 14px);
}

/* Wire runs down through the centre of the markers (52px wide → 26px). */
.steps__wire {
  position: absolute;
  left: 25px;
  top: 30px;
  bottom: 30px;
  width: 2px;
  border-radius: 2px;
  background: var(--line);
  pointer-events: none;
}
/* Grown 0 → 1 by scroll progress (see script.js §6) */
.steps__wire-fill {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 2px;
  background: linear-gradient(to bottom, var(--lav), var(--lav-600));
  transform: scaleY(0);
  transform-origin: top center;
}

/* TWO LIT STATES, not one. `is-on` means the wire has reached this step: it
   is lit and it stays lit, because the loop being complete behind you is the
   point of the section. `is-active` is the node the wire head is passing
   right now — one at a time — and it is the only one that gets the full
   lavender glow. With a single state all three ended up shouting at once by
   the bottom of the scene and nothing read as current. */
.step {
  position: relative;
  display: grid;
  grid-template-columns: 52px minmax(0, 1fr);
  gap: clamp(14px, 1.1vw, 20px);
  align-items: center;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .7s var(--ease), transform .7s var(--ease);
}
.step.is-on { opacity: 1; transform: none; }

.step__marker {
  position: relative;
  z-index: 2;
  width: 52px; height: 52px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--white);
  border: 1px solid var(--line-2);
  color: var(--lav-600);
  box-shadow: var(--shadow-sm);
  transition: transform .7s var(--ease), box-shadow .7s var(--ease),
              background .5s var(--ease), color .5s var(--ease),
              border-color .5s var(--ease);
}
.step.is-on .step__marker { transform: scale(1.05); box-shadow: var(--shadow-md); }
/* The current node inverts: solid lavender, white glyph, lavender bloom. */
.step.is-active .step__marker {
  background: var(--lav-600);
  border-color: var(--lav-600);
  color: var(--white);
  transform: scale(1.12);
  box-shadow: 0 10px 30px rgba(145, 121, 210, .34);
}
.step__marker svg { width: 25px; height: 25px; }

/* Halo that pulses once the step lights up */
.step__marker::after {
  content: "";
  position: absolute;
  inset: -7px;
  border-radius: 50%;
  border: 1px solid var(--lav-300);
  opacity: 0;
  transform: scale(.85);
}
.step.is-on .step__marker::after { animation: halo 2.6s var(--ease-soft) infinite; }
@keyframes halo {
  0%   { opacity: .85; transform: scale(.88); }
  70%  { opacity: 0;   transform: scale(1.22); }
  100% { opacity: 0;   transform: scale(1.22); }
}

/* Icon internals animate once lit */
.node__pulse { stroke-dasharray: 40; stroke-dashoffset: 40; }
.step.is-on .node__pulse { animation: trace 2.4s var(--ease-soft) infinite; }
@keyframes trace {
  0%   { stroke-dashoffset: 40; }
  55%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -40; }
}
.step.is-on .node__scanline { animation: sweep 2.8s var(--ease-soft) infinite; }
@keyframes sweep {
  0%, 100% { transform: translateY(0);   opacity: .35; }
  50%      { transform: translateY(7px); opacity: 1; }
}

/* Each step's copy is an elevated card sitting off the timeline, rather than
   text floating beside a dot. That is what gives the three of them the weight
   the section needs, and it is what the glow has to land on to be legible. */
.step__body {
  padding: clamp(12px, 1.5vh, 18px) clamp(16px, 1.5vw, 24px);
  border-radius: var(--r-sm);
  background: var(--white);
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-xs);
  transition: background .5s var(--ease), border-color .5s var(--ease),
              box-shadow .5s var(--ease), transform .5s var(--ease);
}
.step.is-active .step__body {
  border-color: var(--lav-300);
  background: var(--lav-50);
  box-shadow: 0 14px 40px rgba(145, 121, 210, .16);
  transform: translateX(3px);
}

/* The number as a micro-badge rather than a floating label — it reads as a
   marker on the card, which is what stops it looking like stray small type. */
.step__no {
  display: inline-block;
  margin-bottom: 6px;
  padding: 2px 9px;
  border-radius: 999px;
  background: var(--lav-100);
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .64rem;
  letter-spacing: .16em;
  color: var(--lav-700);
  transition: background .5s var(--ease), color .5s var(--ease);
}
.step.is-active .step__no { background: var(--lav-600); color: var(--white); }

/* Bigger, and with room to breathe — these three lines are the section. */
.step__body h3 {
  font-size: clamp(1.14rem, .55vw + .95rem, 1.38rem);
  line-height: 1.25;
  margin-bottom: 5px;
}
.step__body p {
  color: var(--ink-soft);
  font-size: clamp(.95rem, .3vw + .86rem, 1.04rem);
  line-height: 1.55;
  max-width: 44ch;
}

/* --- the skincare photo -------------------------------------------------- */
.system__media {
  position: relative;
  margin: 0;
}
.system__frame {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  background: var(--cream-2);
}
.system__frame img {
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 5;           /* portrait slot */
  /* THE CAP THAT KEEPS THE SCENE ON ONE SCREEN. Left uncapped, a 4:5 portrait
     in a ~470px column is ~590px tall and it, not the content, decided how
     tall this section was. Now the left column sets the height and the photo
     fits whatever is left. */
  max-height: min(56vh, 560px);
  object-fit: cover;
  object-position: center center; /* subject is centred in the source */
  transform: scale(1.02);
  transition: transform 1.4s var(--ease);
}
.system__media:hover .system__frame img { transform: scale(1.06); }

.system__caption {
  position: absolute;
  left: 18px;
  bottom: 18px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
  border-radius: 999px;
  background: rgba(255, 255, 255, .9);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--shadow-sm);
  font-family: var(--font-head);
  font-weight: 600;
  font-size: .82rem;
  color: var(--ink);
}
.system__pulse {
  width: 9px; height: 9px;
  border-radius: 50%;
  background: var(--lav-600);
  animation: pulseDot 2.2s var(--ease-soft) infinite;
}
@keyframes pulseDot {
  0%, 100% { box-shadow: 0 0 0 0 rgba(145, 121, 210, .55); }
  70%      { box-shadow: 0 0 0 9px rgba(145, 121, 210, 0); }
}

/* SHORT VIEWPORTS. The vh clamps above bottom out at their minimums around
   780px of height, and below that the type — which is sized in rem, as it
   must be — carries on at full size and pushes step 03 back off the bottom.
   This is the same device the app scene uses at max-height: 700px: one block
   that takes a considered step down in type and padding rather than letting
   the layout decide where to break. Measured at 1280 x 665:

     nav 78 + 12 · head ~177 · steps 3 × 107 + 2 × 8 · 20 = ~624

   Width-gated to 981px so it can never fire on the stacked mobile layout,
   which is scrollable and has no one-screen budget to keep. */
@media (min-width: 981px) and (max-height: 780px) {
  .system__head { margin-bottom: 14px; }
  .system__head .section__title { font-size: clamp(1.55rem, 2vw + .4rem, 2.1rem); }
  .system__head .section__lead { font-size: .94rem; margin-top: 8px; }

  .steps { gap: 8px; }
  .step { grid-template-columns: 46px minmax(0, 1fr); gap: 14px; }
  .step__marker { width: 46px; height: 46px; }
  .step__marker svg { width: 22px; height: 22px; }
  .steps__wire { left: 22px; top: 26px; bottom: 26px; }

  .step__body { padding: 10px 16px; }
  .step__no { font-size: .6rem; margin-bottom: 4px; padding: 2px 8px; }
  .step__body h3 { font-size: 1.06rem; margin-bottom: 3px; }
  .step__body p { font-size: .9rem; line-height: 1.45; max-width: 54ch; }

  .system__frame img { max-height: 52vh; }
}

/* ------------------------------------------------------ 7. The Celes app */
/* ======================================================================
   7. THE CELES APP — scroll-driven assembly
   ----------------------------------------------------------------------
   The scene is pinned and scrubbed by script.js §5b. Two rules here exist
   purely to keep out of GSAP's way, and both matter:

   1. NOTHING in this scene may be centred with `transform: translate(-50%)`.
      The timeline owns `transform` on the phone, the copy and every card, so
      a centring transform would be wiped on the first frame. The stage is a
      one-cell grid instead and centres by `place-items`, which costs nothing
      and leaves transform free.

   2. Each card is two elements. The outer .appcard takes the scrubbed
      assembly (transform); the inner .appcard__in takes the endless float
      (the `translate` property, via @keyframes) AND the cursor parallax
      (transform again, but on the inner box). Three effects, three separate
      property/element pairs, no two ever writing the same one.

   Card colours are the app's own: royal blue for the schedule card, green
   for improvement, lavender for the mission — see §7c.
   ====================================================================== */
.app {
  background: var(--paper);
  /* Lifted off the real app screens so the cards read as the product. */
  --app-blue:   #4b6be8;
  --app-blue-2: #3a55d4;
  --app-green:  #2fb457;
  --card-r:     18px;

  /* Overrides .section's --sec-y. This scene is PINNED to a single screen, so
     its whole composition has to fit one viewport — the page's normal rhythm
     (up to 168px top and bottom) would push the device off the bottom.
     Everything here is sized in vh for the same reason: the scene must answer
     the height it is given, not the width.

     THE TOP PADDING CARRIES THE NAV. This is the one section on the page that
     has to, and the reason is the pin: `start: "top top"` parks the section's
     top edge at the viewport's top and holds it there for four viewports of
     scroll. Every other section passes under the fixed nav in a second or two
     and the overlap is never seen; this one would sit under it for the entire
     sequence. Nothing here can scroll clear of it, so the section has to
     reserve the nav's height itself — hence var(--nav-h) rather than a
     hand-tuned number, so the two can never drift apart.

     The rest of the budget, worst case (a short 700px laptop viewport):
       nav 78 + 17 gap  ·  78vh stage  ·  21 bottom  =  ~94vh
     which leaves the section inside one screen at every height it is given.
     Change any of the three and check that sum. */
  padding-block: calc(var(--nav-h) + clamp(16px, 2.4vh, 34px))
                 clamp(22px, 3vh, 44px);
}

/* There is NO centred heading in this scene — it lives in the left column
   instead (.app__title). That is what leaves the section height to the stage,
   and it is the only reason the phone can be ~70vh tall and legible rather
   than a thumbnail squeezed under a headline. */

/* --------------------------------------------------------- 7a. the stage */
.app__stage {
  position: relative;
  width: min(var(--maxw), calc(100% - var(--pad) * 2));
  margin: 0 auto;
  /* What is left of the viewport once the section's padding has taken the nav
     and the bottom margin. Deliberately NOT 100vh-minus-something: the stage
     is the box the composition is centred in, and it has to be smaller than
     the space it sits in or the phone inside it has nowhere to be centred TO.
     The phone (--ph-h, 70vh) settles here with a clear ~4vh band above and
     below, on top of the nav clearance the section already reserved. */
  min-height: min(78vh, 840px);

  /* The device's settled height. Lives here rather than on .app__phone
     because the pair-centring below has to know how wide the phone is, and
     the phone derives its width from this. Inherited down to .app__phone. */
  --ph-h: min(70vh, 760px);

  /* One cell, everything stacked in it and centred — see rule 1 above. */
  display: grid;
  place-items: center;
}

/* ===================================================== 7-pair. the pair ====
   COPY + PHONE ARE CENTRED AS ONE GROUP, not individually.

   Before this the copy was pinned to the stage's left edge and the phone was
   centred in the stage, which put the pair's own centre ~240px left of the
   screen's and left the right-hand third of the section empty. Centring the
   two things separately is what produced a composition that was not centred
   at all.

   THE MECHANISM IS MARGIN ON A CENTRED ITEM. A grid item with
   justify-self: center is centred by its MARGIN box, so a margin on one side
   shifts the item by half of it. Give the copy a right margin of
   (gap + phone) and the phone a left margin of (copy + gap) and the two land
   exactly `gap` apart, with the whole group centred. The arithmetic is in the
   note at the end of this block.

   WHY MARGINS AND NOT transform: translateX(). The floating cards are
   absorbed into the phone's screen, and script.js §5b computes that target
   from offsetLeft/offsetTop — pure layout, deliberately not getBoundingClient-
   Rect, because the timeline has already transformed everything by the time it
   measures. A transform would move the phone on screen without moving the
   number the cards are aiming at, and every card would fly to where the phone
   used to be. Margins are layout, so offsetLeft follows them.

   Scoped to the pinned range. Below 1001px the stage is a plain stacked grid
   (§7f) — there is no pair to centre, and script.js §5b has stood down too.

     C = cell, Wc = copy, G = gap, Wp = phone
     copy  left = (C − Wc − (G + Wp)) / 2          = (C − W) / 2
     phone left = (C − Wp + (Wc + G)) / 2          = (C − W) / 2 + Wc + G
     …so the group spans W = Wc + G + Wp, centred.                        */
@media (min-width: 1001px) {
  .app__stage {
    /* THE THREE COLUMNS. 1fr / 2fr / 1fr, so the outer two are identical and
       the composition is symmetrical about the middle of the screen by
       construction rather than by arithmetic.

       --col-1 is the width of ONE outer column, derived from the same numbers
       the track sizing uses:  4 equal fr units + 2 gaps = 100%. It is what
       the floating cards are sized and anchored against below, so a card can
       never be wider than the column it is supposed to live in, at any window
       size, without anyone having to remember to keep two numbers in step. */
    /* The track ratio, as two numbers rather than baked into the template, so
       that --col-1 below cannot drift out of step with it. 1 / 2 / 1 is the
       composition; the narrow band re-weights it (see the end of this block)
       and everything else follows automatically. */
    --fr-out: 1;
    --fr-mid: 2;

    --col-gap: clamp(28px, 3.4vw, 48px);
    /* The width of ONE outer column: the fr share of what is left once the
       two gaps are taken out. This is what the floating cards are sized and
       anchored against, so a card can never be wider than the column it is
       supposed to live in, at any window size or ratio, without anyone having
       to remember to keep two numbers in step. */
    --col-1: calc((100% - 2 * var(--col-gap)) * var(--fr-out) /
                  (2 * var(--fr-out) + var(--fr-mid)));
  }

  /* GATED ON .motion, WHICH IS THE CONDITION THAT MATTERS.

     This layout is for the PINNED composition, where the cards are only ever
     on screen while they are flying into the phone and the stage around the
     device is otherwise empty. On any path where script.js §5b returns early
     — reduced motion, or GSAP failing to load — there is no timeline, the
     cards rest where CSS puts them spread across the stage, and moving the
     device 216px right to centre it against the copy drives it straight
     through the "Upcoming Schedule" and stats cards on that side. Two things
     that never share the screen in the pinned scene would share it
     permanently in that one.

     .motion is set (§1c) exactly when GSAP has taken over and reduced motion
     is off, so gating on it covers both fallbacks with one condition instead
     of two rules that have to be kept in step. Those paths keep the layout
     they were designed around: copy left, phone centred.

     PARENT-SCOPED ON PURPOSE TOO. §7c below sets `.app__copy { justify-self:
     start }` for the stacked layout; a bare `.app__copy` here would tie with
     it on specificity and then LOSE on source order, because this block comes
     first in the file — the margins would still apply and the copy would stay
     hard left, which is the original bug wearing a hat. */
  /* The stage stops being one stacked cell and becomes the three-column
     track. Everything below places itself in it explicitly — with three
     columns declared, an item left on `grid-area: 1 / 1` would sit in column
     one, which is where the copy lives. */
  /* THE OUTER TRACKS ARE STATED, THE MIDDLE ONE TAKES THE REST — which is the
     same 1 / 2 / 1 geometry written the only way that actually works here.

     The obvious spelling, `calc(var(--fr-out) * 1fr)`, is invalid CSS: `fr` is
     a <flex> and cannot appear inside calc() at all. The declaration is
     dropped whole, the stage falls back to implicit auto-placement, and what
     you get is a TWO-column grid with the phone jammed against the copy —
     which is exactly what it did.

     Naming the outer track and letting `1fr` absorb the remainder gets the
     identical result, keeps the ratio in one variable, and has the useful
     property that the cards below are anchored against the very same value
     the track is sized from rather than a second copy of the arithmetic. */
  .motion .app__stage {
    grid-template-columns: var(--col-1) 1fr var(--col-1);
    column-gap: var(--col-gap);
    align-items: center;
    justify-items: center;
  }

  /* LEFT COLUMN — the reading column, and nothing else. It fills its track
     rather than sitting at a fixed 360px: the column IS the measure now, so
     the copy and the two badges above and below it share one edge. */
  .motion .app__stage .app__copy {
    grid-area: 1 / 1;
    justify-self: stretch;
    width: auto;
  }

  /* CENTRE COLUMN — the device, alone in it. */
  .motion .app__stage .app__phone,
  /* The bloom goes with the device — it is lighting the phone, not the
     section, and left at the stage's centre it would sit in the gap. */
  .motion .app__stage .app__glow {
    grid-area: 1 / 2;
    justify-self: center;
    margin-left: 0;
  }

  /* SAT LOWER THAN DEAD CENTRE, deliberately. The stage is already inset
     below the nav, but the device is 70vh of bright rectangle and centring it
     in the remaining band still left it reading as crowded up against the
     bar. Pushing it down by a few vh puts a clear breathing band between the
     two and drops the phone's optical centre nearer the middle of the screen,
     which is where the eye expects the subject of the section to be.

     A MARGIN, NOT A TRANSFORM. The cards are absorbed into this element and
     script.js §5b computes that target from offsetTop — pure layout, because
     by the time it measures, the timeline has already transformed everything.
     A transform would move the phone on screen without moving the number the
     cards aim at, and every card would fly to where the phone used to be. */
  .motion .app__stage .app__phone { margin-top: clamp(24px, 4.5vh, 60px); }

  /* The opening statement is not part of the three-column composition — it
     owns the whole stage for its beat and then leaves. Spanning every track
     keeps it centred on the SCREEN rather than on the middle column. */
  .motion .app__stage .app__intro { grid-area: 1 / 1 / 2 / -1; }

  /* ------------------------------------------- the cards, inside the columns
     Anchored to the OUTER EDGE of their own column and capped at its width,
     so the two sides mirror each other and neither can reach into the middle
     where the phone is. This replaces a set of hand-picked percentages that
     were tuned against a stage with no columns in it — they were what let the
     right-hand three drift over the device. */
  .motion .app__stage .appcard { width: min(var(--w), var(--col-1)); }

  /* Left column: one above the copy, one below it. They share the column with
     the reading block and are separated from it VERTICALLY — there is no room
     to put them beside it, and no reason to. */
  .motion .app__stage .appcard--selfie  { --t: 2%;  --l: 0%; --r: auto; }
  .motion .app__stage .appcard--mission { --b: 3%;  --l: 0%; --r: auto; }

  /* Right column: top, middle, bottom — the middle one level with the phone's
     centre, which is what "directly right of the phone" means once the device
     is properly centred. */
  .motion .app__stage .appcard--sched   { --t: 3%;  --r: 0%; --l: auto; }
  .motion .app__stage .appcard--stats   { --t: 43%; --r: 0%; --l: auto; }
  .motion .app__stage .appcard--trend   { --b: 5%;  --r: 0%; --l: auto; --t: auto; }
}

/* ---------------------------------------- the narrow end of the composition
   1 / 2 / 1 IS THE WRONG RATIO BELOW ~1200px, and the failure is vertical
   rather than horizontal. At 1024 an outer column is 215px, which is narrow
   enough that all three of the copy's points wrap onto two lines each: the
   list grows from 125px to 171px, the whole reading block from 338 to 380,
   and — since the block is centred in a 667px stage — it eats the clearance
   above and below that the two badges live in. Measured, `Today's Selfie`
   ended up 17px into the top of the column.

   The middle column is where the room is. The phone is ~215px wide there and
   the track is 430 — nearly twice what it needs, because the ratio was chosen
   for the wide layout where 2fr is a proportion rather than a surplus. So the
   outer columns take some of it back at the narrow end: 1.35 / 1.7 / 1.35
   puts an outer column at ~264px, which is enough for the points to sit on
   one line again, while still leaving the centre 330+ for a 215px device.

   Widening the columns rather than shrinking the type is deliberate. The text
   is the thing being read; the surplus is in the empty track next to it. */
@media (min-width: 1001px) and (max-width: 1200px) {
  .app__stage { --fr-out: 1.35; --fr-mid: 1.7; }
}

/* Soft lavender bloom behind the phone. Inset 0 horizontally on purpose: a
   negative side inset pushed it past the viewport on narrow screens and gave
   the document a horizontal scroll. */
.app__glow {
  grid-area: 1 / 1;
  width: min(760px, 92%);
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, var(--lav-100), transparent 72%);
  pointer-events: none;
}

/* ------------------------------------------- 7a-i. the opening statement
   The section's first beat. Same 1/1 grid cell as the phone and the cards,
   so it is centred OVER the composition rather than stacked above it — that
   overlap is the whole point: the statement is still clearing as the cards
   begin to arrive under it, and the two never have to be separate screens.

   DISPLAY: NONE BY DEFAULT, and `.is-live` is added by script.js §5b at the
   end of building the pinned timeline — never by the stylesheet, and never
   off `.motion`. The distinction matters: `.motion` only means GSAP is
   running, but §5b also stands down below 1001px where the stage is a plain
   stacked grid, so there is a window (wide enough for GSAP, too narrow for
   the pin) where `.motion` is set and nothing exists to animate this out.
   Tying the reveal to the timeline that removes it makes "visible" and
   "will be taken away" the same condition. Reduced motion and no-GSAP fall
   out of the same rule: the scene simply opens on the settled phone, which
   is what those paths are each supposed to show.

   pointer-events: none so the waitlist button underneath stays clickable
   through the beat, whatever the timeline has left this at. */
.app__intro {
  display: none;
  grid-area: 1 / 1;
  z-index: 5;
  width: min(900px, 88%);
  text-align: center;
  pointer-events: none;
}
.app__intro.is-live { display: block; }

/* ================================== 7a-ii. THE STAGE STATEMENT — shared ====
   TWO SECTIONS OPEN THE SAME WAY and they must look the same doing it: the
   app scene's "Introducing the Celes app." and the features scene's "Built
   around how skin actually works." Both are a full-screen line that owns the
   stage on its own, types itself in letter by letter, holds, and then clears
   upward so the real content can take the screen.

   They were built at different times and had drifted badly apart —

     app intro     67.2px / 800 / −.038em / 900px wide
     features head 54.4px / 600 / −.035em / 720px wide

   — which is a third bigger and two weights heavier for two things doing the
   identical job, one after the other, four seconds apart. Unified here, once,
   on the app intro's values: these lines are stage statements, not column
   headings, and the heavier setting is the one that carries a whole screen.

   NOT FOLDED INTO .section__title. That class is also the heading of `how`
   and `clinic`, where it sits beside or above content in
   normal flow and 67px would simply be too loud. The features head is the one
   place it is used as a full-stage statement, so the override is scoped to
   exactly that. */
.app__intro-text,
.features__head .section__title {
  margin: 0;
  font-family: var(--font-head);
  font-weight: 800;
  font-size: clamp(2.2rem, 5.2vw, 4.2rem);
  line-height: 1.06;
  letter-spacing: -.038em;
  color: var(--ink);
  text-wrap: balance;
}

/* The same block width, so the two lines break in the same rhythm rather than
   one running to three lines where the other runs to two. */
.app__intro,
.features__head { width: min(900px, 88%); max-width: none; }

.app__intro .eyebrow,
.features__head .eyebrow { margin-bottom: 18px; }

/* --------------------------------------------------------- 7b. the phone */
.app__phone {
  grid-area: 1 / 1;

  /* The scene is PINNED to a single screen, so the device is sized off
     viewport height. Everything below derives from this one number.

     DECLARED ON .app__stage, NOT HERE, and inherited down. The stage needs
     the same number to work out how wide the copy+phone pair is when it
     centres it (§7-pair below), and two copies of the expression would drift
     apart the first time one of them was retuned. Every media query that
     resizes the device therefore overrides it on the STAGE.

     This is the SETTLED size — the phone is the hero of the section and has
     to be readable, so at ~70vh it renders roughly 260-350px wide across the
     desktop range, which is about a real device held at arm's length. The
     timeline starts it scaled DOWN and grows it to 1, so the settled frame is
     this size rather than an upscale of something smaller.

     WHY NOT BIGGER. 86vh was tried and is the wrong number twice over: it put
     the device's top edge under the fixed nav, and — because a 9:19.5 slab
     that tall is nearly the whole screen — it left no margin for the eye to
     read the composition against. The section is a phone AND a column of
     copy AND four cards; the device has to be the largest thing in it, not
     the only thing that fits. 70vh is where it still dominates and the
     composition still breathes. — the value itself is on .app__stage. */
  z-index: 2;
  will-change: transform;
}

/* WIDTH IS STATED, NOT LEFT TO aspect-ratio.
   `width: auto` on a block box does NOT mean "derive me from aspect-ratio" —
   it means "fill the container". .phone sits in a grid cell as wide as the
   whole stage, so `width: auto` made it ~1240px wide, and .phone__screen's
   own `aspect-ratio: 9/19.5` then made it ~2640px TALL: three viewports of
   phone, bottom far off-screen, only the top sliver of the screen visible
   (which is what read as a "blank" phone) and the cards flying at an
   absorption target a thousand pixels below the fold. Stating the width from
   the height is the fix. */
.app__phone .phone {
  height: var(--ph-h);
  width: calc(var(--ph-h) * 9 / 19.5);   /* the 9:19.5 device ratio */
  padding: 9px;
  border-radius: 34px;
}

/* And the screen fills the frame it is given rather than deriving its own
   height from its width — the ratio is the phone's job now, so the screen's
   own aspect-ratio has to stand down or the two fight. */
.app__phone .phone__screen {
  width: 100%;
  height: 100%;
  aspect-ratio: auto;
  border-radius: 26px;
  /* Only ever seen if the recording fails to load — the video sits on top of
     it at full opacity from the first frame. */
  background: linear-gradient(168deg, var(--lav-50), var(--lav-100) 62%, var(--lav-300));
}

/* The recording is ALWAYS visible. It is held paused on its first frame
   until the cards have been absorbed (§5b) and only then plays, so the screen
   shows real app UI the whole time and is never blank. Gating its opacity
   instead — the previous approach — meant any path that never reached the
   play threshold left an empty phone on screen. */

/* ---------------------------------------------------------- 7c. the copy */
.app__copy {
  grid-area: 1 / 1;
  /* `start` below 1001px, where the stage is stacked and there is no pair;
     the pinned range overrides this to `center` + a margin (§7-pair). */
  justify-self: start;
  /* ABOVE THE FLOATING CARDS (z-index 4), not below them.

     The cards' convergence is clamped in script.js so none of them drifts
     onto this column during the spread or the convergence — that is the real
     fix and it is measured. This is the backstop for the beat the clamp
     cannot cover: the absorption itself, where every card travels all the way
     to the phone's centre and the two on the left have no route there that
     does not cross this text. They cross BEHIND it, at speed, already
     fading. Reading material is never the thing that gets covered. */
  z-index: 6;
  width: min(360px, 31%);
}
.app__copy .eyebrow { margin-bottom: 14px; }

.app__title {
  font-family: var(--font-head);
  font-weight: 800;
  font-size: clamp(1.7rem, 2.5vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: -.025em;
  color: var(--ink);
  text-wrap: balance;
}
/* On the wrapper, not the button: the wrapper is the box the layout and
   the scene beat both act on. See the note in index.html §5. */
.app__cta { margin-top: 30px; }

.app__points {
  margin-top: 26px;
  display: grid;
  gap: 16px;
}
.app__points li {
  position: relative;
  padding-left: 28px;
  font-size: 1rem;
  line-height: 1.45;
  color: var(--ink-2);
}
.app__points li::before {
  content: "";
  position: absolute;
  left: 0; top: .5em;
  width: 16px; height: 2px;
  border-radius: 2px;
  background: var(--lav);
}

/* ------------------------------------------------------- 7d. the cards --
   Positioned against the stage by edge (top/left/right), never centred by
   transform — again, rule 1. Each card's slot is a plain block of custom
   properties so the composition can be re-tuned without touching the JS. */
.appcard {
  position: absolute;
  z-index: 4;
  width: var(--w);
  top: var(--t, auto);
  left: var(--l, auto);
  right: var(--r, auto);
  bottom: var(--b, auto);
  will-change: transform;
  /* Display only. They are also faded to nothing once absorbed, and an
     invisible card must not sit over the phone eating clicks. */
  pointer-events: none;
}
.appcard__in {
  padding: 14px 16px;
  border-radius: var(--card-r);
  background: var(--white);
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-md);
  /* The float writes `translate`; the cursor writes `transform`. Declaring
     the identity here keeps the first frame of each from jumping. */
  translate: 0 0;
}

.appcard__name {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .93rem;
  color: var(--ink);
  display: flex;
  align-items: center;
  gap: 8px;
}
.appcard__meta {
  margin-top: 3px;
  font-size: .8rem;
  color: var(--ink-soft);
}

/* --- the blue Upcoming Schedule card ------------------------------------ */
/* SLOTS ARE ANCHORED TO THE STAGE EDGES, NEVER FLOATED INWARD.
   The phone occupies the middle third and nearly the full stage height, so
   there is no usable gap between the copy column and the device — measured,
   it is 86px, and the narrowest card is 186px. Anything positioned into that
   gap lands ON the phone, which is exactly what was happening.

   So the left pair sits in the SAME horizontal band as the copy (0–24%) and
   is separated from it VERTICALLY instead: card, copy, card, stacked down the
   left edge. The right three hang off the right edge. Both bands clear the
   device by a measured margin at every window size. */
.appcard--sched { --w: clamp(232px, 24vw, 300px); --t: 7%; --r: 0%; }
.appcard--sched .appcard__in {
  padding: 15px;
  background: linear-gradient(160deg, var(--app-blue), var(--app-blue-2));
  border-color: transparent;
  color: #fff;
}
.sched__title {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .95rem;
  color: #fff;
}
.sched__who {
  margin-top: 13px;
  display: flex;
  align-items: center;
  gap: 10px;
}
.sched__avatar {
  flex: 0 0 auto;
  width: 38px; height: 38px;
  border-radius: 50%;
  overflow: hidden;
}
.sched__avatar svg { width: 100%; height: 100%; display: block; }
.sched__text { min-width: 0; flex: 1 1 auto; }
.sched__name {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .92rem;
  color: #fff;
}
.sched__role { font-size: .78rem; color: rgba(255,255,255,.76); }
.sched__call {
  flex: 0 0 auto;
  width: 32px; height: 32px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: #fff;
  color: var(--app-blue);
}
.sched__call svg { width: 16px; height: 16px; }

.sched__when {
  margin-top: 13px;
  padding: 9px 11px;
  border-radius: 12px;
  background: rgba(255,255,255,.16);
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: .74rem;
  color: #fff;
}
.sched__when span { display: flex; align-items: center; gap: 6px; white-space: nowrap; }
.sched__when svg { width: 13px; height: 13px; flex: 0 0 auto; }
.sched__sep { width: 1px; height: 15px; background: rgba(255,255,255,.34); }

/* --- the stat trio ------------------------------------------------------ */
.appcard--stats { --w: clamp(238px, 25vw, 312px); --t: 43%; --r: 0%; }
.appcard--stats .appcard__in {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 4px;
  padding: 15px 12px;
}
.stat { text-align: center; flex: 1 1 0; min-width: 0; }
.stat__num {
  font-family: var(--font-head);
  font-weight: 800;
  font-size: 1.22rem;
  letter-spacing: -.02em;
  color: var(--ink);
}
.stat__num--up {
  color: var(--app-green);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1px;
}
.stat__num--up svg { width: 14px; height: 14px; }
.stat__cap { margin-top: 2px; font-size: .72rem; color: var(--ink-soft); }
.stat__rule { width: 1px; height: 30px; background: var(--line-2); flex: 0 0 auto; }

/* --- the improvement trend ---------------------------------------------- */
.appcard--trend { --w: clamp(168px, 17vw, 208px); --t: 78%; --r: 4%; }
.trend__num {
  font-family: var(--font-head);
  font-weight: 800;
  font-size: 1.5rem;
  letter-spacing: -.02em;
  color: var(--app-green);
  display: flex;
  align-items: center;
  gap: 2px;
}
.trend__num svg { width: 18px; height: 18px; }
.trend__spark {
  margin-top: 10px;
  width: 100%; height: 26px;
  color: var(--app-green);
  opacity: .55;
}

/* --- Today's Selfie ----------------------------------------------------- */
.appcard--selfie { --w: clamp(186px, 19vw, 232px); --t: 4%; --l: 0%; }
.selfie__pill {
  margin-top: 12px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  border-radius: 999px;
  background: var(--ink);
  color: #fff;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: .78rem;
}
.selfie__pill svg { width: 14px; height: 14px; }

/* --- Glow Mission ------------------------------------------------------- */
.appcard--mission { --w: clamp(190px, 19vw, 236px); --b: 5%; --l: 2%; }
.mission__day {
  margin-left: auto;
  padding: 3px 9px;
  border-radius: 999px;
  background: var(--lav-100);
  color: var(--lav-700);
  font-size: .69rem;
  font-weight: 700;
  white-space: nowrap;
}
.mission__bar {
  margin-top: 11px;
  display: block;
  height: 6px;
  border-radius: 999px;
  background: var(--lav-100);
  overflow: hidden;
}
.mission__bar i {
  display: block;
  height: 100%;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--lav), var(--lav-600));
}

/* ------------------------------------------------- 7e. the endless float
   Runs on the INNER box and only on `translate`, so the scrubbed assembly
   (outer, transform) and the cursor parallax (inner, transform) both stay
   untouched. Offset delays keep the group from breathing in unison. */
@keyframes appfloat {
  0%, 100% { translate: 0 0; }
  50%      { translate: 0 -9px; }
}
.appcard--sched   .appcard__in { animation: appfloat 7.5s var(--ease-soft) infinite; }
.appcard--stats   .appcard__in { animation: appfloat 8.6s var(--ease-soft) -2.1s infinite; }
.appcard--trend   .appcard__in { animation: appfloat 6.9s var(--ease-soft) -4.3s infinite; }
.appcard--selfie  .appcard__in { animation: appfloat 9.2s var(--ease-soft) -1.2s infinite; }
.appcard--mission .appcard__in { animation: appfloat 7.9s var(--ease-soft) -5.6s infinite; }

/* ------------------------------------------------------ 7f. narrow screens
   The floating composition needs real width to breathe. Below it the scene
   becomes an ordinary stacked layout: copy, phone, then the cards as a plain
   grid. Nothing is hidden — every card is still readable, just not flying. */
@media (max-width: 1000px) {
  /* The nav clearance goes with the pin. Unpinned, this section scrolls past
     the nav like every other one and needs no reservation for it — keeping
     the calc() here would open ~100px of dead cream above the copy for a
     collision that can no longer happen. Back to the page's own rhythm. */
  .app { padding-block: clamp(52px, 9vw, 104px); }

  /* Cards drop out of absolute positioning and become a plain two-up grid;
     copy and phone span it. script.js §5b and §5c both stand down at this
     width, so nothing is left driving transforms here. */
  .app__stage {
    min-height: 0;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px;
    place-items: stretch;
  }
  .app__glow { display: none; }

  /* The opening statement belongs to the pinned timeline, and the pin does
     not exist at this width — so the beat does not either. `!important`
     rather than trusting §5b's early return: the class is added once at build
     time, and a window resized from wide to narrow afterwards would otherwise
     leave the statement stranded over the stacked layout with no timeline
     left to clear it. */
  .app__intro.is-live { display: none !important; }

  .app__copy {
    grid-area: auto;
    grid-column: 1 / -1;
    justify-self: center;
    width: 100%;
    max-width: 46ch;
    text-align: center;
  }
  .app__points { justify-items: center; }
  .app__points li { text-align: left; }

  .app__phone {
    grid-area: auto;
    grid-column: 1 / -1;
    display: flex;
    justify-content: center;
    margin: clamp(32px, 6vw, 56px) 0;
  }
  /* Retune the one number the device derives from — overriding `height`
     alone would leave the width still calculated from the old value and
     stretch the phone out of its 9:19.5 ratio. Stacked here, so it sizes off
     width rather than viewport height. */
  .app__stage { --ph-h: clamp(400px, 66vw, 520px); }

  .appcard {
    position: static;
    width: auto;
  }
  .appcard--sched { grid-column: 1 / -1; }
  .appcard__in { height: 100%; }
}
@media (max-width: 620px) {
  .app__stage { grid-template-columns: minmax(0, 1fr); }
  .appcard--stats .appcard__in { padding: 15px 10px; }
}

/* Short but wide — a 1200x600 window, or a laptop with a lot of browser
   chrome. The composition is all shares of vh so it scales itself; the only
   thing that does not is the nav, which is a fixed 78px however short the
   viewport gets. At 600px tall that is 13% of the screen, so the clamps above
   are squeezed to their floors and the clearance itself is trimmed to the
   minimum that still reads as a gap rather than a near-miss.

   The nav is still RESERVED here, just tightly. Dropping it (as this rule
   used to, with a flat 32px) is what would put the device back under the bar
   on exactly the screens with the least room to spare. */
@media (min-width: 1001px) and (max-height: 700px) {
  .app { padding-block: calc(var(--nav-h) + 12px) 18px; }
}

/* -------------------------------------------- 8. Key features switcher */
/* THE TOP PADDING CARRIES THE NAV, for the same reason the app scene's does:
   §6c pins this section with `start: "top top"`, which parks its top edge at
   the viewport's top and holds it there for the whole hand-off. Nothing in
   here can scroll clear of the fixed header, so the section reserves that
   height itself — var(--nav-h), never a hand-tuned number.

   The whole budget, worst case (a short 700px laptop viewport):
     nav 78 + 15 gap  ·  ~516 stage  ·  18 bottom  =  ~627
   which leaves the pinned scene inside one screen at every height it is
   given. The stage figure comes from the vh-driven phone below; change one
   and check the other. */
.features {
  background: var(--cream);
  padding-block: calc(var(--nav-h) + clamp(14px, 2.2vh, 30px))
                 clamp(18px, 2.6vh, 40px);
  scroll-margin-top: var(--nav-h);
}

/* The scene's own responsive rule, kept with the scene: 980px is where the
   panel goes single-column and becomes far too tall to pin inside one screen,
   so §6c stands the pin down at exactly the same width. With no pin there is
   no one-screen budget to keep, and the scene goes back to the page's
   ordinary vertical rhythm. */
@media (max-width: 980px) {
  .features { padding-block: var(--sec-y); }
}

/* --- the statement → switcher hand-off ----------------------------------
   Normal flow by default (no JS, prefers-reduced-motion, or narrow): the
   statement sits above the switcher and both are visible, which is also the
   printed/no-JS reading order. §6c adds .is-seq ONLY on the pinned desktop
   path, and that collapses the two into a single grid cell so one can hand
   the screen to the other without the section changing height. */
.features__seq.is-seq {
  display: grid;
  align-items: center;
  justify-items: center;
}
.features__seq.is-seq > * {
  grid-area: 1 / 1;
  width: 100%;
}

/* The statement sits UNDER the card and never over it, and GSAP's autoAlpha
   takes it to visibility:hidden the instant it reaches zero. Between the two
   it can neither show through the card during the crossover nor sit there
   catching clicks once it has gone. */
.features__seq.is-seq .features__head {
  position: relative;
  z-index: 0;
  margin-bottom: 0;
  /* `> *` above stretches every child to the full grid cell so the switcher
     fills it. The statement must NOT be stretched with it: it is the shared
     stage-statement block (§7a-ii) and has to keep the same 900px measure as
     the app scene's opening line, or the two break across different numbers
     of lines and stop reading as one device used twice. */
  width: min(900px, 88%);
  justify-self: center;
  pointer-events: none;
  will-change: transform, opacity;
}
.features__seq.is-seq .switcher {
  position: relative;
  z-index: 1;
  will-change: transform, opacity;
}

/* Pinned, the card gets one screen and no more — so the phone is sized
   against the viewport's HEIGHT here, not just its width. The width clamp
   still wins on any tall screen; the vh term only takes over when the screen
   is too short to seat the card whole, which is exactly when a pinned card
   would otherwise be cut off at the bottom. */
.features__seq.is-seq .panel { padding-block: clamp(26px, 3.4vh, 48px); }
.features__seq.is-seq .panel .phone {
  width: min(clamp(186px, 17vw, 228px), 23vh);
}

.switcher {
  max-width: 1120px;
  margin: 0 auto;
}

/* --- tabs --------------------------------------------------------------- */
.switcher__tabs {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px;
  padding: 6px;
  margin: 0 auto clamp(36px, 4.5vw, 56px);
  width: fit-content;
  max-width: 100%;
  border: 1px solid var(--line-2);
  border-radius: 999px;
  background: var(--white);
  box-shadow: var(--shadow-xs);
}

/* The lavender lozenge that slides under the active tab (moved by JS) */
.switcher__pill {
  position: absolute;
  top: 6px;
  left: 0;
  height: calc(100% - 12px);
  border-radius: 999px;
  background: var(--lav-100);
  transition: transform .5s var(--ease), width .5s var(--ease);
  pointer-events: none;
}

.switcher__tab {
  position: relative;
  z-index: 1;
  padding: 12px 22px;
  border-radius: 999px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: .92rem;
  letter-spacing: -.015em;
  color: var(--ink-soft);
  white-space: nowrap;
  overflow: hidden;
  transition: color .3s var(--ease-soft);
}
.switcher__tab:hover { color: var(--ink); }
.switcher__tab[aria-selected="true"] { color: var(--lav-700); }
.switcher__tab[aria-selected="true"] .tag { background: var(--white); }

/* Hairline that runs the dwell on the active tab, so the auto-advance is
   legible instead of surprising. JS restarts it on every switch AND writes
   --dwell first, so the bar always takes exactly as long as the timer it is
   drawing: 3s on the auto cycle, 5s during a post-click reading pause. */
.switcher__timer {
  position: absolute;
  left: 0; bottom: 0;
  height: 2px;
  width: 100%;
  border-radius: 2px;
  background: var(--lav-600);
  transform: scaleX(0);
  transform-origin: left center;
  opacity: 0;
}
.switcher__tab[aria-selected="true"] .switcher__timer { opacity: .55; }
.switcher.is-running [aria-selected="true"] .switcher__timer {
  animation: dwell var(--dwell, 3000ms) linear forwards;
}
/* The longer bar after a click reads as a deliberate hold rather than a
   stall: same sweep, drawn a shade heavier for the 5s it is given. */
.switcher.is-reading [aria-selected="true"] .switcher__timer {
  height: 3px;
  opacity: .85;
  background: var(--lav-700);
}
@keyframes dwell {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* --- the single panel --------------------------------------------------- */
.switcher__stage {
  position: relative;
  display: grid;             /* all panels share one cell → crossfade in place */
  border-radius: var(--r-lg);
}

.panel {
  grid-area: 1 / 1;          /* stack every panel on top of each other */
  display: grid;
  grid-template-columns: minmax(0, .85fr) minmax(0, 1fr);
  gap: clamp(36px, 6vw, 88px);
  align-items: center;
  padding: clamp(30px, 4vw, 56px) clamp(40px, 6vw, 82px);
  border-radius: var(--r-lg);
  background: var(--white);
  border: 1px solid var(--line-2);
  box-shadow: var(--shadow-md);

  opacity: 0;
  visibility: hidden;        /* also removes it from the accessibility tree */
  transform: translateY(16px) scale(.985);
  transition:
    opacity .55s var(--ease),
    transform .6s var(--ease),
    visibility 0s linear .6s;
}
.panel.is-active {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    opacity .55s var(--ease) .06s,
    transform .6s var(--ease) .06s,
    visibility 0s;
}
/* No-JS baseline: the [hidden] panels stay hidden and the first one shows.
   JS strips [hidden] on init and drives visibility instead. */
.panel[hidden] { display: none; }

.panel__visual {
  position: relative;
  display: flex;
  justify-content: center;
}
/* Slightly smaller inside the panel so the card doesn't tower */
.panel .phone { width: clamp(186px, 17vw, 228px); }

/* The screen and the copy settle a beat after the panel crossfades, so
   switching reads as the phone changing screen — never a hard cut. */
.panel .phone__screen > img {
  opacity: 0;
  transform: scale(1.08);
  transition: opacity .6s var(--ease), transform .8s var(--ease);
}
.panel.is-active .phone__screen > img {
  opacity: 1;
  transform: scale(1.02);
  transition-delay: .1s;
}
.panel__copy > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .55s var(--ease), transform .6s var(--ease);
}
.panel.is-active .panel__copy > *          { opacity: 1; transform: none; }
.panel.is-active .panel__copy > *:nth-child(1) { transition-delay: .10s; }
.panel.is-active .panel__copy > *:nth-child(2) { transition-delay: .16s; }
.panel.is-active .panel__copy > *:nth-child(3) { transition-delay: .22s; }
.panel.is-active .panel__copy > *:nth-child(4) { transition-delay: .28s; }

.panel__copy .eyebrow { margin-bottom: 16px; }
.panel__copy h3 { font-size: clamp(1.5rem, 2.9vw, 2.3rem); }
.panel__copy h3 em { font-style: italic; color: var(--lav-600); }
.panel__copy > p {
  margin-top: 18px;
  font-size: 1.05rem;
  color: var(--ink-soft);
}

.panel__list {
  margin-top: 26px;
  display: grid;
  gap: 11px;
}
.panel__list li {
  position: relative;
  padding-left: 28px;
  font-size: .95rem;
  color: var(--ink-soft);
}
.panel__list li::before {
  content: "";
  position: absolute;
  left: 0; top: .52em;
  width: 16px; height: 2px;
  border-radius: 2px;
  background: var(--lav);
}

/* The unreleased panel reads slightly cooler than the live ones. Its
   "Coming soon" flags live on the tab and the eyebrow, not over the screen. */
.panel--soon .phone { opacity: .9; }

/* --- prev / next --------------------------------------------------------
   Both sit on the panel's vertical centre line (top: 50% against the stage,
   which is exactly as tall as the card) so they stay level with each other at
   every panel height. Cream chip, lavender rule and glyph — the same pairing
   the tabs use — so they belong to the card instead of floating over it. */
.switcher__arrow {
  position: absolute;
  top: 50%;
  z-index: 4;
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--cream);
  border: 1px solid var(--lav-300);
  color: var(--lav-700);
  box-shadow: var(--shadow-sm);
  transform: translateY(-50%);
  transition: transform .3s var(--ease), background .25s,
              border-color .25s, color .25s;
}
.switcher__arrow svg { width: 19px; height: 19px; }
.switcher__arrow:hover {
  background: var(--lav-600);
  border-color: var(--lav-600);
  color: var(--white);
  transform: translateY(-50%) scale(1.07);
}
.switcher__arrow--prev { left: -22px; }
.switcher__arrow--next { right: -22px; }

/* ------------------------------- 9. The media pair used by §10's split
   .understood__grid and .understood__media are what the old "Your skin,
   understood." scene was built from. That scene is gone; its VIDEO is not —
   it is now the left half of the early-access section, and it keeps these
   class names so §0d's media beat, the cursor-follow tilt (§19c) and the
   1200/860 collapses all keep finding it without a second set of rules.
   ========================================================================== */
.understood__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr);
  gap: clamp(36px, 5.5vw, 78px);
  align-items: center;
}
.understood__media {
  position: relative;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  background: var(--cream-2);
}
.understood__video {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
}
/* ----------------------------------------- 10. Celes Early Access (waitlist)
   The LARGE waitlist. It is now a two-column band: the story video left, the
   ask and the form right.

   THE BASE RULES STILL DESCRIBE A CENTRED COLUMN. That is deliberate — it is
   the shape this section takes once .understood__grid collapses at 1120px,
   and it is what the phone gets. .waitlist--split only overrides what the
   two-column layout actually needs, inside the same query that keeps the grid
   two-column, so the stacked case cannot be broken by a rule written for the
   split one.

   The blooms and the background are unchanged, so the colour rhythm of the
   closing sequence is the same as before the merge.
   ========================================================================== */
.waitlist {
  background: linear-gradient(160deg, var(--cream) 0%, var(--lav-50) 52%, var(--cream) 100%);
}
.waitlist__bloom {
  position: absolute;
  border-radius: 50%;
  pointer-events: none;
}
.waitlist__bloom--a {
  top: -22%; left: -8%;
  width: min(640px, 58vw); aspect-ratio: 1;
  background: radial-gradient(closest-side, rgba(201, 184, 240, .42), transparent 70%);
}
.waitlist__bloom--b {
  bottom: -26%; right: -6%;
  width: min(520px, 48vw); aspect-ratio: 1;
  background: radial-gradient(closest-side, rgba(226, 216, 248, .5), transparent 72%);
}

.waitlist__inner {
  position: relative;
  max-width: 680px;
  margin-inline: auto;   /* it is a grid item now, so it centres itself */
  text-align: center;
}
.waitlist__title {
  font-size: clamp(1.9rem, 4vw, 3.05rem);
  line-height: 1.1;
  margin-bottom: 20px;
}
.waitlist__lead {
  color: var(--ink-soft);
  font-size: 1.06rem;
  max-width: 48ch;
  margin: 0 auto;
}

/* --- the split, wide only --------------------------------------------------
   Gated at 1121px, the exact width .understood__grid stops being two-column
   at, so the left-aligned column and the two-column grid switch on and off
   together. Below it nothing here applies and the section is the centred
   block it always was.

   The grid overrides the media-heavy 1.1fr / .9fr split the old scene used:
   here the right column carries a headline, a paragraph AND a form, so the
   two halves are even rather than weighted towards the picture. */
@media (min-width: 1121px) {
  .waitlist--split .waitlist__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }
  .waitlist--split .waitlist__inner {
    max-width: none;
    text-align: left;
  }
  .waitlist--split .waitlist__lead { margin-inline: 0; }
  /* .wl--full centres itself on the page in the stacked case; in the column
     it starts at the same left edge as the type above it. */
  .waitlist--split .wl--full { margin-inline: 0; }
  .waitlist--split .wl--full .wl__done { justify-content: flex-start; }
}

/* ------------------------------------------- 10b. The waitlist form (.wl)
   ONE component, TWO sizes. .wl--full is the section above; .wl--mini is the
   compact one in the hero. Everything structural lives on .wl so the two can
   never drift apart in behaviour; the modifiers change only scale, width and
   alignment.

   The row is a grid rather than a flex row: `1fr auto` pins the button to its
   own content width and hands every remaining pixel to the field, which is
   what keeps "Join" the same size in both variants while the input absorbs
   the difference. */
.wl {
  --wl-pad: 15px 24px;
  --wl-font: 1rem;
  --wl-btn-min: 104px;
  margin-top: 34px;
}
.wl--full { margin-inline: auto; max-width: 520px; }

.wl__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 10px;
  align-items: stretch;
}

/* The label is for assistive tech only — the placeholder carries the visible
   instruction, and a floating label above a one-field inline form would be
   more chrome than the form itself. */
.wl__label {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.wl__input {
  width: 100%;
  min-width: 0;
  font-family: var(--font-body);
  font-size: var(--wl-font);
  padding: var(--wl-pad);
  border: 1.5px solid var(--line);
  /* Pill, to agree with the button beside it and with the nav actions and
     trust pill elsewhere on the page. A 10px radius next to a 999px one
     reads as two controls that were designed separately. */
  border-radius: 999px;
  background: var(--white);
  color: var(--ink);
  transition: border-color .25s, box-shadow .25s, background .25s;
}
.wl__input::placeholder { color: var(--ink-soft); opacity: .8; }
.wl__input:focus {
  outline: none;
  border-color: var(--lav);
  box-shadow: 0 0 0 4px var(--lav-100);
  background: var(--white);
}
.wl__input.invalid {
  border-color: #d98b8b;
  box-shadow: 0 0 0 4px #f9e8e8;
}

/* Matches the field height exactly rather than being sized independently —
   the two sit in one row and any difference reads as a misalignment. */
.wl__btn {
  padding: var(--wl-pad);
  font-size: var(--wl-font);
  /* Without a floor, a four-letter label collapses the pill to a circle. */
  min-width: var(--wl-btn-min);
  white-space: nowrap;
}

.wl__msg {
  min-height: 1.2em;
  margin-top: 10px;
  font-size: .9rem;
  font-weight: 500;
  color: var(--ink-soft);
}
.wl__msg.is-error { color: #b4534f; }

.wl__count {
  margin-top: 12px;
  font-size: .88rem;
  color: var(--ink-soft);
}
.wl__count strong {
  font-family: var(--font-head);
  font-weight: 700;
  color: var(--ink-2);
}

/* --- the count as a SOCIAL-PROOF ROW ---------------------------------------
   Faces, then a bold number, then the sentence. The three parts are doing
   three different jobs and only work together: the avatars say "people", the
   weight on the number says "a lot of them", and the muted label says which
   people. Drop any one of them and it goes back to being a caption.

   IT WAS A FOOTNOTE BEFORE. Small, grey, same weight throughout, tucked under
   the form — the single strongest thing the hero had to say, set as the
   quietest thing on it. The fix is not a bigger font size on the whole line;
   it is CONTRAST between the number and everything around it.

   Baseline alignment, not centre: the number and the label are one line of
   type at two sizes, and centring them would float the label in the middle of
   a figure twice its height. The avatars are pulled onto that baseline by
   hand below, since a flex row of dots has no text baseline of its own. */
.wl__count--stat {
  margin-top: 22px;
  display: flex;
  align-items: center;
  gap: 12px;
}

/* THE NUMBER AND ITS SENTENCE ARE ONE BLOCK, SEPARATE FROM THE FACES, and
   that split is what makes this survive a narrow column. The hero's copy
   column is 420px; the faces, the figure and the sentence together want 447.
   With everything in one wrapping flex row the sentence dropped under the
   AVATARS, which reads as a caption for the dots. Given its own block it wraps
   under the NUMBER instead — where it belongs — and in the early-access
   section, which has 520px to play with, it never wraps at all.

   So the same markup is one line where there is room and a tidy two-line unit
   where there is not, with no width query deciding it. */
.wl__stat-text {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 2px 10px;
  min-width: 0;
}

/* THE AVATAR CLUSTER — FACES, NOT THE BRAND DOTS. The lavender/peach/blue
   gradient dots that were here first belong to the "Built with dermatologists"
   pill and stay there: beside a sentence that says "people", an abstract blob
   reads as decoration, and the whole point of this row is that a human being
   is on the other end of the number.

   Ring, overlap and order are the pill's though, so the two clusters on the
   page are still recognisably the same OBJECT — only the contents changed.

   The ring is --white rather than a cream, and it has to be: these sit on the
   hero's video, on the early-access section's lavender wash and on the phone
   hero's gradient. A cream ring is invisible against one of those; white
   separates the circles on all three. */
.wl__faces {
  display: inline-flex;
  align-items: center;
  flex: none;
}
.wl__faces img {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  /* Cover, not contain: the sources are square and centred, so this is a
     clean circular crop with nothing to distort. */
  object-fit: cover;
  display: block;
  border: 2.5px solid var(--white);
  margin-right: -10px;
  background: var(--lav-50);          /* the hole before the file arrives */
  box-shadow: 0 2px 6px rgba(27, 24, 34, .10);
}
/* The last one has nothing to tuck under, so it stops pulling and opens the
   gap to the number instead. */
.wl__faces img:last-child { margin-right: 8px; }

/* THE FOCAL POINT. Everything about this rule is contrast against the label
   beside it — roughly 2.4x its size, 700 against 400, full ink against
   --ink-soft. That ratio is the effect; scale them together and it is a
   footnote again at a larger size. */
/* Scoped to .wl__count--stat so it OUT-SPECIFIES `.wl__count strong` above,
   which sets --ink-2 for the plain caption form. Unscoped, this rule lost the
   colour by one element selector and the figure came out at #3b3546 — close
   enough to look intentional and wrong enough to cost the contrast the whole
   treatment is built on. */
.wl__count--stat .wl__stat-num {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.75rem, 2.5vw, 2.4rem);
  line-height: 1;
  letter-spacing: -.035em;
  color: var(--ink);
}
.wl__stat-label {
  font-size: .95rem;
  color: var(--ink-soft);
}

/* --- the odometer wheels (script.js 9b) ------------------------------------
   One box per digit, one line tall with overflow hidden, holding a strip of
   eleven cells (0-9 and 0 again). Only the strip's transform ever changes, so
   a roll is composited and costs no layout.

   --odo-h is on both the box and the cells and MUST stay equal on the two:
   the box is the window and the cell is the pane, and any difference between
   them shows a sliver of the next digit above or below the one that should be
   showing. */
.odo {
  --odo-h: 1.12em;
  display: inline-flex;
  align-items: flex-start;
  vertical-align: baseline;
}
.odo__digit {
  display: block;
  height: var(--odo-h);
  overflow: hidden;
}
.odo__strip {
  display: block;
  /* The wheels are moved every frame during a roll; promoting them once here
     is cheaper than letting the compositor discover it eleven times. */
  will-change: transform;
}
.odo__strip > span {
  display: block;
  height: var(--odo-h);
  line-height: var(--odo-h);
  /* Tabular figures, so a 1 occupies the same column as a 7 and the wheels
     never change width mid-roll. */
  font-variant-numeric: tabular-nums;
  font-feature-settings: "tnum" 1;
}
.odo__fixed {
  display: block;
  height: var(--odo-h);
  line-height: var(--odo-h);
}
/* The value in plain text for assistive tech; the wheels beside it are
   aria-hidden. Standard visually-hidden, not display:none — display:none is
   not read out. */
.odo__sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The confirmation takes the row's place: .is-done hides the control so the
   same address cannot be sent twice and no stale button sits under it. */
.wl.is-done .wl__row,
.wl.is-done .wl__msg,
.wl.is-done .wl__count { display: none; }

.wl__done {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 4px;
  font-family: var(--font-head);
  font-weight: 600;
  font-size: 1rem;
  color: var(--ink);
}
.wl__done:focus { outline: none; }
.wl--full .wl__done { justify-content: center; }

.wl__tick {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 26px; height: 26px;
  border-radius: 50%;
  background: var(--lav-100);
  color: var(--lav-700);
}
.wl__tick svg { width: 15px; height: 15px; }

/* ---- the mini variant, hero only -------------------------------------- */
.wl--mini {
  --wl-pad: 13px 20px;
  --wl-font: .94rem;
  --wl-btn-min: 88px;
  margin-top: 0;
  width: min(420px, 100%);
}
.wl--mini .wl__count { margin-top: 10px; }

/* --------------------------------------------------- 11. Clinic section
   Built to mirror "The Celes app": copy on one side, a single large piece of
   media on the other. Here the image is on the RIGHT. */
.clinic { background: var(--paper); }
.clinic__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(40px, 6vw, 88px);
  align-items: center;
}
.clinic__copy { max-width: 46ch; }
.clinic__copy .section__title { font-size: clamp(2rem, 4.2vw, 3.2rem); }
.clinic__copy .section__lead { max-width: 34ch; }
.clinic__cta { margin-top: 30px; }   /* on the wrapper — see .app__cta */

.clinic__points {
  margin-top: clamp(32px, 4vw, 46px);
  display: grid;
  gap: 12px;
}
.clinic__points li {
  display: grid;
  grid-template-columns: 46px minmax(0, 1fr);
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  border-radius: var(--r-sm);
  background: rgba(255, 255, 255, .72);
  border: 1px solid var(--line-2);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  transition: transform .45s var(--ease), box-shadow .45s var(--ease);
}
.clinic__points li:hover { transform: translateY(-3px); box-shadow: var(--shadow-sm); }
.clinic__icon {
  width: 46px; height: 46px;
  display: grid; place-items: center;
  border-radius: var(--r-xs);
  background: var(--lav-100);
  color: var(--lav-700);
}
.clinic__icon svg { width: 23px; height: 23px; }
.clinic__point-lead {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1rem;
  letter-spacing: -.02em;
}
.clinic__point-note { font-size: .87rem; line-height: 1.45; color: var(--ink-soft); }

.clinic__media { margin: 0; }
.clinic__frame {
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-md);
  background: var(--cream-2);
}
.clinic__frame img {
  width: 100%;
  height: 100%;
  aspect-ratio: 4 / 3.4;
  object-fit: cover;
  object-position: center center;
  transform: scale(1.02);
  transition: transform 1.4s var(--ease);
}
.clinic__media:hover .clinic__frame img { transform: scale(1.06); }

/* ------------------------------------------------------- 11b. Founders
   EDITORIAL SPLIT, not a team grid. The title block and the contact action
   hold the left third; the two portraits take the rest.

   THE RATIO IS LOAD-BEARING. .60fr / 1fr resolves to about 37.5% / 62.5% —
   the copy end of the 35-40% the composition allows, because the left column
   carries five blocks now rather than three and the headline has to break
   where it is written to break. Even the split up and the cards drop to
   roughly 280px wide inside a 1240px container, at which point the faces are
   thumbnails and the section is no longer about the two people in it. Change
   this and check the cards AND the headline: "health personal." on one line
   is what sets the floor under the left column.

   NOTHING IN HERE IS A ONE-OFF. The button is .btn--primary, the card surface
   and hover are the same translucent white and lift the clinic points use,
   the LinkedIn chip is the .clinic__icon treatment at a smaller size, and the
   heading takes the base h2 weight and tracking. The section owns its LAYOUT
   and nothing else - no local palette, no local button, no local card. An
   earlier pass gave this section a sage-green button and a plain white box
   with a grey drop shadow, and both read instantly as a different website.

   NOTHING HERE MOVES ON ITS OWN EITHER. No tilt, no parallax, no cursor
   follow - .founder__frame is deliberately not in script.js 1e's list, and the
   only motion is the scene entrance every other section gets. The cards are
   immediately clickable, on the ordinary system cursor.
   ========================================================================== */
.founders {
  background: var(--cream);
}
.founders__grid {
  display: grid;
  grid-template-columns: minmax(0, .60fr) minmax(0, 1fr);
  gap: clamp(36px, 4.4vw, 72px);
  /* Centred rather than top-aligned: the left column is three short things
     against two tall cards, and hanging it from the top leaves it stranded
     over a column of empty cream. */
  align-items: center;
}

/* --- left column ---------------------------------------------------------- */
.founders__intro {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Font, weight and tracking all come from the base h2 (§1a) - the same
   treatment as .section__title and .waitlist__title. Only the SIZE is set
   here, and the ceiling is set by the COLUMN, not by taste: "health personal."
   has to hold one line inside ~408px at 1240, which puts the cap at about
   52px. Raise it and the second line wraps to three, and the authored break
   stops meaning anything. */
.founders__title {
  font-size: clamp(1.95rem, 3.6vw, 3.25rem);
  color: var(--ink);
}

/* Measure, not column width. The spec asks for 400-450px and the column is
   about that, so the ch cap is what actually does the work at smaller
   viewports — it keeps the line length readable rather than letting the
   paragraph run the full width of whatever box it lands in. */
.founders__lead {
  margin-top: clamp(18px, 1.8vw, 26px);
  max-width: 46ch;
  font-size: 1.04rem;
  line-height: 1.62;
  color: var(--ink-soft);
}

/* THE BELIEF BLOCK. Separated by space and a quieter label, nothing else —
   no rule, no border, no fill. It is the one place on the page where .eyebrow
   is used WITHOUT --accent, which is the whole point: same device as the
   label above it, one level down, so the eye reads it as a sub-heading rather
   than as a second section opening. */
.founders__belief {
  margin-top: clamp(26px, 2.8vw, 40px);
}
.founders__belief-label {
  margin-bottom: 10px;
  font-size: .68rem;
  color: var(--ink-mute);
}
.founders__belief-text {
  max-width: 44ch;
  font-size: .95rem;
  line-height: 1.6;
  color: var(--ink-soft);
}

/* THE CONTACT BUTTON IS .btn--primary. Everything that makes it a button -
   the lavender fill, the ink text, the sheen, the hover lift and glow, the
   press dip, the reduced-motion stand-down - is inherited. What is left here
   is the arrow chip and the room it needs, and that is all that should ever
   be here: anything else in this rule means the button is drifting away from
   the ones in the app, clinic and CTA blocks. */
/* The beat target. It exists so that GSAP animates THIS element's transform
   and .btn's hover animates its own — see the note in index.html §8. Nothing
   else belongs on it. */
.founders__cta { margin-top: clamp(30px, 3.4vw, 46px); }

.founders__contact {
  gap: clamp(14px, 1.4vw, 20px);
  /* Asymmetric: a full lg pad on the word, a chip-sized inset on the arrow. */
  padding: 9px 9px 9px clamp(26px, 2.4vw, 34px);
}
.founders__contact-arrow {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  /* Ink at low alpha, so the chip reads on the lavender fill without being a
     second colour. It inverts to white when :hover turns the button lav-600. */
  background: rgba(36, 31, 51, .12);
  transition: transform .4s var(--ease), background .3s var(--ease-soft);
}
.founders__contact-arrow svg { width: 18px; height: 18px; display: block; }
.founders__contact:hover .founders__contact-arrow {
  background: rgba(255, 255, 255, .24);
  transform: translateX(3px);
}

/* --- the two cards -------------------------------------------------------- */
.founders__cards {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(20px, 2.2vw, 32px);
  /* Equal height is the default here and stays that way: stretch, plus a
     fixed image ratio, means the two cards agree without either one having to
     know how tall the other is. */
  align-items: stretch;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* THE SITE'S CARD SURFACE, not a white box. Translucent white over the cream
   with a blur behind it and a lavender-tinted hairline - the same build as
   .clinic__points li and .rail__icon - so the card sits IN the page rather
   than on top of it. The resting shadow is lavender rather than the neutral
   --shadow-* ink: at this size a grey drop reads as a UI panel, and every
   other soft edge on this page is tinted.

   The lift itself is in §19c with the rest of the site's cards, not here. */
.founder {
  display: flex;
  flex-direction: column;
  background: rgba(255, 255, 255, .72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  border: 1px solid var(--line-2);
  border-radius: var(--r-lg);
  padding: clamp(14px, 1.2vw, 18px);
  box-shadow: 0 18px 48px rgba(145, 121, 210, .13);
}

/* ONE RATIO FOR BOTH PORTRAITS. The two source photographs do not agree -
   Nithin's is square, Michael's is roughly 5:7 - so the frame, not the file,
   decides the crop. 4:5 sits between the two: the square loses a little off
   each side and the tall one a little off top and bottom, and neither loses a
   face. The images are shipped uncropped so this number can be retuned here
   without going back to the assets. */
.founder__frame {
  position: relative;
  aspect-ratio: 4 / 5;
  /* Outer radius minus the padding, so the two curves are concentric. */
  border-radius: calc(var(--r-lg) - 16px);
  overflow: hidden;
  background: var(--cream-2);
  /* An inset hairline rather than a border, so it draws ON the photo instead
     of adding a pixel to the box. Without it the two cards do not agree:
     Nithin's headshot is shot on a white sweep that runs straight into the
     card and his portrait loses its edge entirely, while Michael's
     environmental shot keeps one. This gives both the same edge whatever the
     photograph behind it does. */
  box-shadow: inset 0 0 0 1px rgba(122, 97, 189, .10);
}
.founder__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  display: block;
}

.founder__meta {
  padding: clamp(18px, 1.8vw, 24px) clamp(6px, .8vw, 12px) clamp(6px, .6vw, 10px);
}

/* Name and chip on one line: inline-flex rather than a flex row, so a long
   name wraps as text and takes the chip with it instead of the chip being
   pinned to a far edge the name never reaches. Weight and tracking are
   .clinic__point-lead's - the site's card-title setting. */
.founder__name {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: clamp(1.08rem, 1.25vw, 1.24rem);
  letter-spacing: -.02em;
  color: var(--ink);
  margin: 0;
}
.founder__role {
  margin-top: 6px;
  font-size: .92rem;
  color: var(--ink-soft);
}

/* THE ONLY SOCIAL LINK ON THE PAGE, and it is the .clinic__icon chip at a
   smaller size: lavender-100 tile, lavender-700 mark, the page's small radius.
   As a bare glyph it read as a typo beside the name; as a tile it reads as the
   one thing on the card you can press. The scale-and-glow hover is the same
   one every other icon tile on the site gets - it is applied in §19c. */
.founder__li {
  flex: none;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: var(--r-xs);
  background: var(--lav-100);
  color: var(--lav-700);
}
.founder__li svg { width: 17px; height: 17px; display: block; }
.founder__li:hover { background: var(--lav-300); }
.founder__li:focus-visible {
  outline: 2px solid var(--lav-600);
  outline-offset: 3px;
}

/* ------------------------------------- 12. Closing section (CTA + footer)
   One unit: shared surface, no section gap between them, so the end of the
   page lands as a single closing screen. */
/* The one scene that deliberately holds two things at once: the CTA band and
   the footer are meant to land together as a single closing view. */
.closing {
  background: var(--cream);
  padding-top: var(--sec-y-tight);
}
/* Footer rows share the CTA card's width, so the logo, the columns and the
   card all start on one left edge — the group reads as a single block. */
.closing .container { max-width: 1560px; }

/* --- the CTA band ------------------------------------------------------- */
.cta {
  padding: 0;
  background: transparent;
}
.cta__block {
  position: relative;
  width: min(1560px, calc(100% - var(--pad) * 2));
  margin: 0 auto;
  /* ~60% of the previous block height — a compact closing band that leaves
     the footer room in the same view. */
  min-height: clamp(256px, 30vw, 358px);
  display: flex;
  align-items: center;        /* copy rides vertically centred, not on the floor */
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--shadow-lg);
  isolation: isolate;
}

/* The photo. Composed with the subject on the RIGHT, so it is anchored right:
   as the card gets wider than the frame it is the empty left side that gives,
   never the face. */
.cta__img {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;

  /* Overscanned on purpose. script.js §6 drifts this image vertically for
     parallax, so it is built taller than its frame and pulled up by half the
     surplus — the drift then happens inside the overscan and the card's top
     and bottom edges never uncover. .cta__block already clips. */
  height: 112%;
  top: -6%;
  bottom: auto;

  object-fit: cover;
  object-position: right center;
}

/* Legibility wash. Heaviest on the left where the type sits, clearing well
   before the subject on the right — plus a light overall darkening so the
   card keeps its deep purple/black character. */
.cta__ground {
  position: absolute;
  inset: 0;
  z-index: 1;
  background:
    linear-gradient(to right,
      rgba(16, 13, 24, .93) 0%,
      rgba(19, 15, 28, .82) 26%,
      rgba(22, 18, 32, .46) 52%,
      rgba(24, 20, 36, .16) 74%,
      rgba(24, 20, 36, .06) 100%),
    linear-gradient(to top, rgba(16, 13, 24, .5) 0%, transparent 55%),
    radial-gradient(80% 110% at 8% 50%, rgba(122, 97, 189, .38) 0%, transparent 62%);
}

.cta__inner {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 40rem;
  padding: clamp(28px, 3.6vw, 54px);
  color: #fff;
}
.cta__mark {
  display: block;
  width: clamp(30px, 3vw, 40px);
  height: clamp(30px, 3vw, 40px);
  margin-bottom: clamp(14px, 1.8vw, 22px);
  color: rgba(255, 255, 255, .78);
}
.cta__mark svg { width: 100%; height: 100%; }
.cta__title {
  font-size: clamp(1.7rem, 3.4vw, 2.7rem);
  line-height: 1.05;
  letter-spacing: -.04em;
  max-width: 18ch;
}
.cta__lead {
  margin-top: 14px;
  max-width: 52ch;   /* keeps the line from orphaning its last word */
  font-size: 1rem;
  color: rgba(255, 255, 255, .78);
}
.cta__inner .btn { margin-top: clamp(20px, 2.2vw, 28px); }

/* ---------------------------------------------------------- 13. Footer
   Sits inside .closing, directly under the CTA band — no top border and no
   surface change, so the two read as one block rather than two sections. */
.footer { background: transparent; }
.footer__top {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px 24px;
  padding-top: clamp(22px, 2.6vw, 34px);
}
.footer__tagline { color: var(--ink-soft); font-size: .94rem; }

.footer__main {
  padding-top: clamp(18px, 2.2vw, 28px);
  padding-bottom: clamp(20px, 2.4vw, 30px);
}
/* FIVE COLUMNS, NOT FOUR, SINCE THE LEGAL COLUMN WAS ADDED. Location, Contact,
   Follow, Navigation, Legal. The six policy pages have to be reachable from
   every page, and a link strip in the bar below could not carry six titles as
   long as "Data Deletion Request" without wrapping into a second line of
   small print.

   THE GAP COMES DOWN WITH THE COLUMN COUNT. At 32px, five columns inside the
   1240px container left each one 213px, and "Data Deletion Request" wraps to
   three lines at 213px. 26px gives them 227px and holds it to two. Below
   1240 the two existing breakpoints already take this to 2 columns and then
   to 1, so the five-up arrangement only ever has the full container to work
   in and never has to survive being squeezed. */
.footer__cols {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 26px;
}
/* ONE ROW PER ENTRY, ENFORCED BY THE COLUMN — not by the links' own display.
   The rule below sets `display: block` on them, and it was being silently
   overridden: §19d gives every footer link `display: inline-block` so its
   growing-underline ::after has a box to measure against, and §19d wins on
   source order. The links have therefore been flowing INLINE wherever the
   column was wide enough to fit two of them — "LinkedIn Instagram TikTok" on
   one line — which was invisible at the old 1240px container and obvious the
   moment the clinics footer went full width.

   A grid fixes it without touching either rule: every child gets its own row
   whatever its display is, so §19d keeps the inline-block it needs and the
   column keeps the stack it needs. `justify-items: start` so the underline is
   as wide as the word rather than as wide as the column. */
.footer__col {
  display: grid;
  align-content: start;
  justify-items: start;
}
.footer__col h4 {
  font-family: var(--font-head);
  font-weight: 700;
  font-size: .72rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 13px;
}
.footer__col a,
.footer__col p {
  display: block;
  padding: 5px 0;
  font-size: .94rem;
  color: var(--ink-2);
  transition: color .25s;
}
.footer__col a:hover { color: var(--lav-600); }

.footer__bar { border-top: 1px solid var(--line); }
.footer__bar-inner {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
  padding: 15px var(--pad);
  font-size: .84rem;
  color: var(--ink-soft);
}
.footer__legal { display: flex; gap: 18px; }
.footer__legal a:hover { color: var(--lav-600); }

/* ------------------------------------------------- 14. Modals + forms */
.modal {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  padding: 20px;
}
.modal[hidden] { display: none; }

.modal__scrim {
  position: absolute;
  inset: 0;
  background: rgba(27, 24, 34, .42);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  opacity: 0;
  transition: opacity .35s var(--ease-soft);
}
.modal__box {
  position: relative;
  width: min(480px, 100%);
  max-height: calc(100svh - 40px);
  overflow-y: auto;
  padding: clamp(30px, 5vw, 46px);
  border-radius: var(--r);
  background: var(--white);
  box-shadow: var(--shadow-lg);
  opacity: 0;
  transform: translateY(18px) scale(.97);
  transition: opacity .4s var(--ease), transform .45s var(--ease);
}
.modal.is-open .modal__scrim { opacity: 1; }
.modal.is-open .modal__box   { opacity: 1; transform: none; }

.modal__x {
  position: absolute;
  top: 16px; right: 16px;
  width: 34px; height: 34px;
  display: grid; place-items: center;
  border-radius: 50%;
  color: var(--ink-soft);
  transition: background .25s, color .25s;
}
.modal__x svg { width: 17px; height: 17px; }
.modal__x:hover { background: var(--cream-2); color: var(--ink); }

.modal__box h2 { font-size: clamp(1.4rem, 3vw, 1.85rem); }
.modal__lead { margin-top: 14px; color: var(--ink-soft); font-size: .98rem; }
.modal .form { margin-top: 26px; }

/* Fine print under the early-access form: the consent sentence, then the
   reassurance line under it. PURELY ADDITIVE — no element that existed before
   this carries either class, so nothing already on the page renders any
   differently. Muted and small on purpose: it has to be readable and findable
   without competing with the field and the button above it. */
.modal__fine {
  margin-top: 14px;
  font-size: .78rem;
  line-height: 1.55;
  color: var(--ink-mute);
}
.modal__fine--center {
  margin-top: 10px;
  text-align: center;
}

/* Confirmation face */
.modal__done { text-align: center; }
.modal__check {
  width: 58px; height: 58px;
  margin: 0 auto 22px;
  display: grid; place-items: center;
  border-radius: 50%;
  background: var(--lav-100);
  color: var(--lav-700);
}
.modal__check svg { width: 28px; height: 28px; }
.modal__done p { margin-top: 13px; color: var(--ink-soft); }
.modal__done .btn { margin-top: 28px; }

/* --- forms (shared with the clinics page) ------------------------------- */
.form { display: grid; gap: 16px; }
.field { display: grid; gap: 7px; }
.field label {
  font-family: var(--font-head);
  font-weight: 600;
  font-size: .82rem;
  color: var(--ink-2);
}
.field input,
.field select,
.field textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  padding: 14px 16px;
  border: 1.5px solid var(--line);
  border-radius: var(--r-xs);
  background: var(--cream);
  color: var(--ink);
  transition: border-color .25s, box-shadow .25s, background .25s;
}
.field textarea { resize: vertical; min-height: 96px; }
.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--lav);
  box-shadow: 0 0 0 4px var(--lav-100);
  background: var(--white);
}
.field input.invalid,
.field select.invalid,
.field textarea.invalid {
  border-color: #d98b8b;
  box-shadow: 0 0 0 4px #f9e8e8;
}
.form__msg {
  min-height: 1.2em;
  font-size: .92rem;
  font-weight: 500;
}
.form__msg.error   { color: #b4534f; }
.form__msg.success { color: var(--lav-700); }

/* --------------------------------- 15. Device frames + placeholders */
/* THE BEZEL IS SIZED AGAINST THE DEVICE, NOT PICKED BY EYE.
   It was 11px of padding on a 40px radius, which on the ~198px the feature
   panels actually render at is 5.3% of the width per side against roughly
   2.5–3% on a real iPhone, and a radius of 22% of the width against roughly
   12%. Twice the frame and twice the round, on the one element whose entire
   job is to read as a real phone — it framed the screenshot instead of
   presenting it. 6px is ~3% at this size and 28px is ~14%: unmistakably a
   device, no longer a picture frame.

   .phone__screen's radius follows this one down by the padding (28 − 6 = 22),
   which keeps the two curves concentric — the same relationship the old pair
   had (40 − 11 = 29 ≈ 30).

   THE WIDTH IS UNCHANGED AND SO IS EVERY LAYOUT NUMBER AROUND IT. Padding is
   inside the stated width, so the device occupies exactly the box it did
   before; only the proportion of frame to screen moves. */
.phone {
  position: relative;
  width: clamp(210px, 22vw, 268px);
  aspect-ratio: 9 / 19.5;
  padding: 6px;
  border-radius: 28px;
  background: #1f1b28;
  box-shadow: var(--shadow-lg);
}
.phone::before {
  content: "";
  position: absolute;
  top: 17px; left: 50%;
  width: 46px; height: 6px;
  border-radius: 6px;
  background: rgba(255,255,255,.24);
  transform: translateX(-50%);
  z-index: 2;
}

/* --- phones holding a real screen asset ---------------------------------
   The supplied screenshots and the app recording already contain the device
   notch and status bar, so this variant drops our drawn speaker bar and lets
   the screen's own 9:19.5 ratio drive the frame's height. */
.phone--asset { aspect-ratio: auto; }
.phone--asset::before { content: none; }

/* The app section's hero mockup — deliberately large, it is the focal point. */
.phone--xl {
  width: clamp(268px, 30vw, 392px);
  padding: 13px;
  border-radius: 48px;
}
.phone--xl .phone__screen { border-radius: 36px; }

.phone__screen {
  width: 100%;
  aspect-ratio: 9 / 19.5;
  border-radius: 22px;      /* .phone's 28px less its 6px of bezel — concentric */
  overflow: hidden;
  background: #fff;
  /* isolate so the scaled media below can't bleed past the rounded corners */
  transform: translateZ(0);
}
.phone__screen > img,
.phone__screen > video {
  width: 100%;
  height: 100%;
  object-fit: cover;      /* fills the screen, never distorts */
  object-position: center top;
  /* The source mockups leave a few pixels of their own bezel at the extreme
     edge; a hair of scale pushes it out of frame. */
  transform: scale(1.02);
}
.phone__screen > video { transform: scale(1.04); }

/* --- outstanding-asset placeholders -------------------------------------
   The dashed lavender fill shown until a real asset is dropped in. Not used
   by clinics.html any more — that page's media is real photography and
   footage in .mediaframe (clinics.css §4b). */
.frame {
  width: 100%;
  max-width: 540px;
  aspect-ratio: 4 / 3;
  border-radius: var(--r);
  overflow: hidden;
  box-shadow: var(--shadow-md);
}
.ph {
  width: 100%; height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  text-align: center;
  color: var(--lav-700);
  border: 1px dashed var(--lav);
  background: repeating-linear-gradient(
    45deg,
    var(--lav-50), var(--lav-50) 13px,
    #fff 13px, #fff 26px
  );
}
.ph span  { font-family: var(--font-head); font-weight: 700; font-size: 1rem; }
.ph small { font-size: .74rem; color: var(--ink-soft); letter-spacing: .03em; }
.ph--phone  { border-radius: 29px; }
.ph--square { border-radius: var(--r-sm); aspect-ratio: 1 / 1; }
.ph--wide   { border-radius: 0; }

/* -------------------------------------------- 15b. The colour journey
   One fixed pane over the page. script.js §1f cross-fades its colour as you
   scroll, so the whole site is graded section by section.

   `multiply` is what keeps this safe: a pale tint multiplied into a light
   surface reads as mood, while text — already near-black — is mathematically
   almost untouched, so contrast survives every tint in the journey. A plain
   alpha wash would instead lift the blacks and grey the type out.

   Below the nav (z 90) and the modals (z 200) on purpose: chrome and dialogs
   stay true, only the page itself is graded. */
.pagetint {
  position: fixed;
  inset: 0;
  z-index: 80;
  pointer-events: none;
  mix-blend-mode: multiply;
  background-color: rgba(255, 255, 255, 0);
  will-change: background-color;
}

/* ----------------------------------------------- 16. Motion utilities */

/* Generic fade-up, toggled by IntersectionObserver */
[data-reveal] {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity .85s var(--ease), transform .85s var(--ease);
}
[data-reveal].in-view { opacity: 1; transform: none; }

/* Hero entrance — lines wipe up once the page is ready */
.hero__title > span > *,
.hero__copy .eyebrow,
.hero__sub,
.hero__actions,
.hero__rail .rail__item,
.trust {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity .9s var(--ease), transform .9s var(--ease);
}
.is-ready .hero__copy .eyebrow      { opacity: 1; transform: none; transition-delay: .10s; }
.is-ready .hero__title > span > *     { opacity: 1; transform: none; }
.is-ready .hero__title > span:nth-child(1) > * { transition-delay: .18s; }
.is-ready .hero__title > span:nth-child(2) > * { transition-delay: .26s; }
.is-ready .hero__title > span:nth-child(3) > * { transition-delay: .34s; }
.is-ready .hero__sub                { opacity: 1; transform: none; transition-delay: .44s; }
.is-ready .hero__actions            { opacity: 1; transform: none; transition-delay: .52s; }
.is-ready .trust                    { opacity: 1; transform: none; transition-delay: .60s; }
.is-ready .hero__rail .rail__item:nth-child(1) { opacity: 1; transform: none; transition-delay: .58s; }
.is-ready .hero__rail .rail__item:nth-child(2) { opacity: 1; transform: none; transition-delay: .66s; }
.is-ready .hero__rail .rail__item:nth-child(3) { opacity: 1; transform: none; transition-delay: .74s; }

/* ---------------------------------------------------------- 17. Responsive */

/* The statement's side gutters close as the sentence grows toward 92vw, so
   the composition thins from the outside in. */
@media (max-width: 1240px) {
  .chip--5, .chip--10 { display: none; }   /* the edge pair loses its gutter */
  .chip--1 { left: 9%; }
  .chip--2 { right: 8%; }
  .chip--6 { left: 10%; }
  .chip--7 { right: 10%; }
}

@media (max-width: 1120px) {
  .hero__inner { flex-direction: column; align-items: flex-start; gap: 34px; }
  .hero__rail {
    width: 100%;
    grid-auto-flow: column;
    justify-items: start;
    justify-content: start;
    text-align: left;
    gap: clamp(20px, 4vw, 48px);
    padding-top: 24px;
    border-top: 1px solid rgba(27, 24, 34, .12);
  }
  .rail__item { grid-template-columns: auto; }
  .rail__icon { grid-row: auto; margin-bottom: 12px; }
  .rail__lead { font-size: 1.35rem; }

  .system__grid,
  .understood__grid,
  .clinic__grid,
  /* The founders split stacks here, but its two CARDS do not: the right
     column is now the full container, which is more room for the pair than
     the 65% they had at desktop. They collapse on their own at 768. */
  .founders__grid { grid-template-columns: 1fr; }
  .system__media { order: -1; max-width: 460px; }   /* stays portrait when stacked */

  /* Stacked, the 16:9 video is as wide as the container and therefore about
     half a laptop screen tall — the reader would meet the picture and nothing
     else before the ask it belongs to. Capped so the eyebrow and the headline
     are in the same view as it. Two-column (>=1121px) is untouched: the column
     already bounds it there. */
  .understood__media { max-width: 620px; margin-inline: auto; }

  /* THE ONE BAND WHERE THE PORTRAIT IS SQUARE. Stacked, the pair suddenly
     has the entire container instead of 65% of it, and a 4:5 crop on a 456px
     card is a 570px portrait — two of those plus the title block is well over
     a screen for a section that is three lines and two faces. 1:1 is the same
     photograph, framed to the width the cards actually have here. Below 768
     they go one per row and the portrait crop returns to 4:5. */
  .founder__frame { aspect-ratio: 1 / 1; }

  /* The marquee is fitted by scaling the whole plane, never by re-laying it
     out: the deck stays 2560 x 2160 at every width, so the tilt, the gaps and
     the type inside all keep their proportions. One number does it.

     The ladder tracks the FRAME, not the viewport. Hold --mq-s while the
     frame narrows and the tiles grow against it — the grid thins out to three
     big plates, which is the look this section was widened to escape. Each
     step keeps frame-width ÷ tile-width at roughly 4.3, and every value still
     clears the coverage floor (below ~0.42 at these frame sizes the tipped
     rhombus stops reaching the corners) with room to spare. */
  .marquee { --mq-s: .60; }
}

@media (max-width: 980px) {
  .nav__links { display: none; }
  .nav__actions { display: none; }
  .nav__toggle { display: flex; }

  /* The app scene's own responsive rules live with the scene, in §7f — it
     collapses at 1000px, which is also where its pin and cursor parallax
     stand down, so the two have to switch on the same breakpoint. */

  .panel { grid-template-columns: 1fr; padding: clamp(28px, 6vw, 44px); }
  .panel__visual { order: 2; }
  .switcher__arrow--prev { left: 8px; }
  .switcher__arrow--next { right: 8px; }
  .switcher__arrow { top: auto; bottom: -22px; transform: none; }
  .switcher__arrow:hover { transform: scale(1.07); }

  .footer__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  .marquee { --mq-s: .52; }
}

/* Hero: at this width the subject fills the frame, so the copy can no longer
   sit over it. The footage takes the top band and the copy sits below it on
   clean background — still full clarity, still no overlay, and readable. */
@media (max-width: 760px) {
  .hero {
    display: block;
    min-height: 0;
    padding-top: var(--nav-h);
    background: var(--paper);
  }
  .hero__stage {
    position: relative;
    inset: auto;
    width: 100%;
    height: min(52svh, 420px);
  }
  /* Back to `cover` here: the hero stacks at this width, so the footage has
     its own band and can never collide with the nav. `contain` would only
     letterbox the band and leave dead space above the copy. */
  .hero__video { object-fit: cover; object-position: center 16%; }
  /* Pill drops into normal flow between the footage and the headline */
  .trust {
    position: static;
    margin: 22px var(--pad) 0;
  }
  .hero__inner {
    padding-top: 26px;
    padding-bottom: clamp(56px, 9vh, 88px);
    gap: 28px;
  }
  .hero__title { font-size: clamp(2.4rem, 11vw, 3.4rem); }
  .hero__rail {
    grid-auto-flow: row;
    gap: 18px;
    margin-top: 8px;
  }
  .rail__item {
    grid-template-columns: auto 1fr;
    align-items: center;
    gap: 0 14px;
  }
  .rail__icon { grid-row: 1 / 3; margin-bottom: 0; width: 40px; height: 40px; }
  .rail__lead { font-size: 1.2rem; }
  .rail__note { font-size: .78rem; max-width: none; }

  .scan { display: none; }         /* too busy at this size */
  .scrollcue { display: none; }

  /* Six pills at this size, not ten — the sentence is nearly full-bleed and
     the rest would crowd it. */
  .chip--3, .chip--8 { display: none; }
  .chip { font-size: .76rem; padding: 7px 13px; }
  .chip--1 { top: 9%;  left: 5%;  }
  .chip--2 { top: 6%;  right: 5%; }
  .chip--4 { top: 20%; right: 26%; }
  .chip--9 { bottom: 22%; left: 24%; }
  .chip--6 { bottom: 9%; left: 6%;  }
  .chip--7 { bottom: 5%; right: 6%; }
  .statement__glow { width: 420px; height: 420px; margin: -210px 0 0 -210px; }

  .steps__wire { left: 21px; }
  .step { grid-template-columns: 44px minmax(0, 1fr); gap: 16px; }
  .step__marker { width: 44px; height: 44px; }
  .step__marker svg { width: 21px; height: 21px; }

  .clinic__points li { grid-template-columns: 38px minmax(0, 1fr); gap: 13px; }
  .clinic__icon { width: 38px; height: 38px; }

  .cta__block { min-height: 420px; }

  .marquee { --mq-s: .48; height: clamp(360px, 60vw, 440px); }

  .footer__cols { grid-template-columns: 1fr; gap: 26px; }
  .footer__bar-inner { flex-direction: column; text-align: center; }
}

@media (max-width: 560px) {
  .switcher__tabs {
    flex-wrap: nowrap;
    overflow-x: auto;
    justify-content: flex-start;
    border-radius: var(--r);
    scrollbar-width: none;
  }
  .switcher__tabs::-webkit-scrollbar { display: none; }
  .switcher__pill { border-radius: var(--r-sm); }
  .switcher__tab { font-size: .85rem; padding: 11px 16px; }
  .trust__text { font-size: .72rem; padding: 8px 14px 8px 20px; }

  /* The ladder stops here rather than following the frame all the way down.
     Below this the statements stop being readable, and an unreadable
     statement tile is just a grey rectangle — better to show fewer of them. */
  .marquee { --mq-s: .44; }
}

/* ------------------------------------------------------ Reduced motion */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  *,
  *::before,
  *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }

  [data-reveal],
  .step,
  .panel__copy > *,
  .hero__title > span > *,
  .hero__copy .eyebrow,
  .hero__sub,
  .hero__actions,
  .hero__rail .rail__item,
  .trust {
    opacity: 1 !important;
    transform: none !important;
  }
  .statement__text .w { opacity: 1 !important; }
  .scan { opacity: 1; }
  .switcher__timer { display: none; }
}


/* ===================================================== 18. Signature motion
   Added by script.js §1c. `html.motion` is set only when GSAP is present AND
   the visitor has not asked for reduced motion — so everything here is opt-in
   and the no-JS / reduced-motion paths keep the plain, finished layout.
   ========================================================================== */

/* GSAP owns the hero entrance when .motion is on, so the CSS transitions that
   used to run it must stand down or the two interpolate against each other. */
.motion .hero__title > span > *,
.motion .hero__copy .eyebrow,
.motion .hero__sub,
.motion .hero__actions,
.motion .hero__rail .rail__item,
.motion .trust { transition: none; }

/* The line clip is what made the old wipe work. The words now streak in
   horizontally, so the clip has to come off or it shears them mid-flight. */
.motion .hero__title > span { overflow: visible; }

/* The line wrapper <i> is hidden by `.hero__title span > *` for the CSS
   entrance, which only ever un-hid it via .is-ready. GSAP animates the words
   INSIDE it and never touches the wrapper, so it has to be reset here — with
   it left at opacity 0 the whole headline stays invisible no matter what the
   words themselves are doing. */
.motion .hero__title > span > i { opacity: 1; transform: none; }

/* One word of the headline. inline-block so it can take a transform, and its
   own compositor layer while the blur resolves. */
.hero__word {
  display: inline-block;
  will-change: transform, filter;
}
/* Dropped the moment the entrance finishes — a permanent will-change keeps a
   layer alive for nothing. */
.hero__word.is-settled { will-change: auto; }

/* Pre-entrance state. Only applied under .motion, so without JS the hero is
   simply visible. */
.motion .hero__word { opacity: 0; }
.motion .scan { opacity: 0; }
.motion .scrollcue { opacity: 0; }
.motion .hero__stage { opacity: 0; }
.motion .chip { opacity: 0; }

/* Under .motion the reveal is no longer a container fading as one lump: §1f
   animates the parts inside it in tiers (label → copy → media). So the
   container itself must stand down, and its CSS transition with it, or it
   would interpolate against GSAP's inline writes. The block above is still
   the no-JS / reduced-motion path, untouched. */
.motion [data-reveal] {
  opacity: 1;
  transform: none;
  transition: none;
}

/* script.js writes one composed `transform` per pill per frame — drift,
   cursor-repel and click-bounce folded into a single string, so the three
   never fight over the property. The depth `scale`/`rotate` set in §5 are
   standalone properties and compose underneath it. */
.chip { will-change: transform; }

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: .motion is never set under reduced motion, but if it
     somehow were, nothing may start hidden. */
  .motion .hero__word,
  .motion .scan,
  .motion .scrollcue,
  .motion .hero__stage { opacity: 1 !important; }
  /* Pills stay visible, but keep their depth — the tiers are composition,
     not motion. */
  .motion .chip { opacity: var(--o) !important; transform: none !important; }
  .statement__text .w { color: var(--ink) !important; }

  /* No cursor light: it is motion by definition, and the scene reads
     perfectly well without it. */
  .statement__glow { display: none; }

  /* The journey keeps its colour, but stops travelling: one faint, fixed
     lavender grade over the whole page instead of a scrubbed cross-fade. */
  .pagetint {
    background-color: rgba(201, 184, 240, .06) !important;
    will-change: auto;
  }

  /* The app scene rests fully settled. §5b and §5c both return early under
     reduced motion, so the phone, copy and cards are never transformed in
     the first place — all that is left to stand down is the endless float.

     Its opening statement needs nothing here either: §5b bailing is exactly
     what leaves .app__intro without its `.is-live` class, so the beat never
     renders and the scene simply opens on the settled phone (§7a-i). */
  .appcard__in {
    animation: none !important;
    translate: none;
  }
  .appcard { will-change: auto; }
  .app__phone { will-change: auto; }

  /* The marquee keeps its tipped plane — the geometry is composition, not
     motion — but nothing on it moves any more.

     `animation: none`, not the blanket rule at the top of this block. That
     rule only collapses the duration, which runs each animation once and
     leaves it holding its LAST keyframe: every glow stuck at full scale, every
     aurora slammed to the same end position, every tile identical. Removing
     the animations outright hands each element back to its declarations,
     which is where the authored resting state lives. */
  .marquee__col,
  .mtile--glow::before,
  .mtile--glow::after,
  .mtile--aurora,
  .mtile--globe::after,
  .mtile--ring::before {
    animation: none !important;
  }
  .marquee__col {
    translate: none !important;
    will-change: auto;
  }
  /* Mid-breath, not the extremes the keyframes travel between. */
  .mtile--glow::before,
  .mtile--glow::after { opacity: .88; }
  /* Each aurora keeps the position it was authored with (§6b-iii), so the
     three tiles still differ from one another at rest. */

  /* The meteor is a streak by definition — there is no still frame of it
     worth keeping. */
  .mtile--aurora::after { display: none; }

  /* The scene rests as a plain two-column layout: copy left, phone centred,
     cards where CSS puts them. The device gives back a little height so the
     whole thing sits in one screen without a timeline to stage it. */
  .app__stage { --ph-h: min(64vh, 600px); }

  /* The pair-centring (§7-pair) is gated on .motion and so has already stood
     down here — see the note there. */

  /* And the nav clearance comes back off the top, for the same reason §7f
     drops it: §5b returns early here, so there is no pin, so the section
     scrolls past the fixed nav like every other one and has nothing to
     reserve against. Left in, it would be ~100px of empty cream above the
     copy on the one path that most wants the scene to fit a single screen. */
  .app { padding-block: clamp(48px, 7vh, 88px); }
}


/* ================================================ 19. Global interactions
   Button/card micro-interactions and link underlines.
   Everything here is transform/opacity only, and every piece is disabled or
   softened under prefers-reduced-motion at the end of this section.
   ========================================================================== */

/* -------------------------------------------------- 19b. Buttons, global
   Every .btn on both pages: nav actions, hero CTAs, Book a call, waitlist
   submits, modal buttons. */
.btn {
  position: relative;
  overflow: hidden;          /* clips the sheen sweep */
  transform: translateZ(0);  /* own layer, so the sweep does not repaint text */
}
.btn:hover  { transform: translateY(-3px) scale(1.025); }
/* Dips under 1 on press and springs back on release -- that is the tactility. */
.btn:active {
  transform: translateY(-1px) scale(.97);
  transition-duration: .09s;
}

/* Sheen: a soft highlight sweeping once across on hover. */
.btn::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(105deg,
    transparent 35%,
    rgba(255, 255, 255, .38) 50%,
    transparent 65%);
  transform: translateX(-120%);
  transition: transform .65s var(--ease);
}
.btn:hover::after { transform: translateX(120%); }
/* On dark buttons a full-strength white sheen is too hot. */
.btn--dark::after,
.btn--light::after {
  background: linear-gradient(105deg,
    transparent 35%, rgba(255, 255, 255, .22) 50%, transparent 65%);
}
/* Outline/ghost buttons invert on hover, so a sheen would fight the fill. */
.btn--outline::after, .btn--ghost::after { content: none; }

/* Deepen the glow on the filled lavender button. */
.btn--primary:hover { box-shadow: 0 18px 40px rgba(145, 121, 210, .42); }

/* ---------------------------------------------------- 19c. Cards, global
   One shared behaviour for every tile-like thing on the site. */
.step,
.rail__item,
.clinic__points li,
.app__points li,
.founder {
  transition: transform .4s var(--ease), box-shadow .4s var(--ease);
}
.step:hover,
.rail__item:hover,
.clinic__points li:hover,
.app__points li:hover,
.founder:hover { transform: translateY(-4px); }

/* The founder card is the largest thing in this group, so its resting and
   hover shadows are a size up from the tiles' — same lavender, same ratio
   between the two states, just scaled to the box. */
.founder:hover { box-shadow: 0 30px 72px rgba(145, 121, 210, .22); }

/* The icon inside a card lifts and warms with it. */
.step__marker,
.rail__icon,
.clinic__icon,
.founder__li {
  transition: transform .4s var(--ease), box-shadow .4s var(--ease),
              background .3s var(--ease-soft);
}
.step:hover .step__marker,
.rail__item:hover .rail__icon,
.clinic__points li:hover .clinic__icon,
/* The LinkedIn chip answers BOTH: the card lifting under it, and the pointer
   arriving on the chip itself. Same scale and same glow either way, so
   hovering the card and then the chip is one continuous state, not two. */
.founder:hover .founder__li,
.founder__li:hover {
  transform: scale(1.08);
  box-shadow: 0 10px 26px rgba(145, 121, 210, .28);
}

/* Media frames that take the cursor-follow tilt (script.js 1e). The tilt is
   written to `transform`; the scroll parallax writes to `translate`, so the
   two compose rather than one clobbering the other. */
.system__frame,
.clinic__frame,
.understood__media {
  transition: box-shadow .45s var(--ease);
}
.system__frame:hover,
.clinic__frame:hover,
.understood__media:hover { box-shadow: var(--shadow-lg); }

/* ---------------------------------------------------- 19d. Links, global
   The nav underline used to grow from the centre; it now draws from the left,
   and the footer/legal links pick up the same treatment. */
.nav__links a::after { left: 0; transform: none; }
.nav__links a:hover::after { width: 100%; }

.footer__col a,
.footer__legal a {
  position: relative;
  display: inline-block;
}
.footer__col a::after,
.footer__legal a::after {
  content: "";
  position: absolute;
  left: 0; bottom: 1px;
  width: 0; height: 1px;
  background: var(--lav-600);
  transition: width .35s var(--ease);
}
.footer__col a:hover::after,
.footer__legal a:hover::after { width: 100%; }

/* ---------------------------------------------------- 19e. Reduced motion
   Hovers keep a colour/shadow response but stop moving. */
@media (prefers-reduced-motion: reduce) {
  .btn:hover, .btn:active { transform: none; }
  .btn::after { content: none; }
  .step:hover,
  .rail__item:hover,
  .clinic__points li:hover,
  .app__points li:hover,
  .founder:hover { transform: none; }
  .step:hover .step__marker,
  .rail__item:hover .rail__icon,
  .clinic__points li:hover .clinic__icon,
  .founder:hover .founder__li,
  .founder__li:hover { transform: none; }
  .founders__contact:hover .founders__contact-arrow { transform: none; }
}

/* ==========================================================================
   20. MOBILE ISOLATION — everything below 768px, and nothing above it
   --------------------------------------------------------------------------
   ONE BREAKPOINT, ONE DIRECTION. Every rule in this block is inside
   `max-width: 768px`, so nothing here can reach a desktop layout: at 1024px
   and above the cascade never sees it. That is a deliberate containment
   boundary rather than a coincidence of ordering — mobile fixes have a habit
   of leaking upward through a shared class, and the only reliable way to
   promise "no desktop impact" is for the rules not to exist up there.

   dvh, NOT vh, AND NOT svh EITHER. Three viewport units and each is right
   somewhere different:

     vh   the LARGE viewport — as if the address bar were hidden. A 100vh
          section is taller than what you can actually see while the bar is
          up, so its last line sits under the fold on first paint.
     svh  the SMALL viewport — as if the bar were showing. Safe, and what
          §1c uses as the general floor, but it under-fills once the bar
          retracts and leaves a strip of the next section peeking in.
     dvh  the DYNAMIC viewport — whatever is visible right now. It tracks the
          bar as it hides and returns, which is exactly what a full-height
          section wants and is why the jump happens without it.

   The trade is that dvh RE-LAYS OUT as the bar moves. That is fine for these
   sections, which are a centred column of text, and would not be for a pinned
   scrubbed scene — which is why the pins are gated off below 981px anyway
   (script.js §6d/§6f, clinics.js §2) and this block never introduces one.
   ========================================================================== */
@media (max-width: 768px) {

  /* The full-height rule from §1c, re-measured against the live viewport. */
  .section { min-height: 100dvh; }
  .chero,
  .scene.hero { height: 100dvh; }

  /* A section whose content is genuinely taller than a phone screen must be
     allowed to be taller — min-height is a floor, and `justify-content:
     center` on an overflowing flex column would otherwise push its first
     child off the top of the box where it cannot be scrolled back to. */
  .section { justify-content: flex-start; }
  .section > * { margin-block: auto; }

  /* --- everything multi-column becomes one column -----------------------
     Listed explicitly rather than with a blanket `grid-template-columns:
     1fr !important`, so each one keeps its own gap and its own order and
     nothing inherits a stacking it was not designed for. */
  .system__grid,
  .features__seq,
  .understood__grid,
  .clinic__grid,
  .founders__grid,
  /* One card per row. Two of these on a phone is two 160px portraits, which
     is the one thing this section must never become — and the order is
     already right, so stacking needs nothing but the column count. */
  .founders__cards,
  .footer__cols {
    grid-template-columns: minmax(0, 1fr);
  }

  /* --- FOUNDERS ON A PHONE: A LIST, NOT TWO PICTURE CARDS --------------
     Above 768 the founders are a showcase — two large portraits carrying the
     right-hand two thirds of an editorial split. On a phone that same card
     becomes a full-width photograph roughly 430px tall, twice over, and the
     section turns into two and a half screens of portrait with the names
     stranded underneath. Nobody scrolls a founder photo.

     So the phone gets the shape this content takes everywhere else on the
     web: a round avatar, the name beside it, the role under the name. It is
     the .clinic__points li row — same surface, same hairline, same radius —
     which is the site's existing list idiom, so this is a re-use rather than
     a second design.

     STRICTLY BELOW 768. The desktop and tablet composition is untouched by
     every rule in here: this block is inside §20, which only ever narrows. */
  .founders__cards { gap: 12px; }

  .founder {
    flex-direction: row;
    align-items: center;
    gap: 15px;
    padding: 12px 15px;
    border-radius: var(--r-sm);
    /* A list row sits closer to the page than a showcase card does. */
    box-shadow: 0 6px 18px rgba(145, 121, 210, .10);
  }

  .founder__frame {
    width: 62px;
    flex: none;
    aspect-ratio: 1;
    border-radius: 50%;
  }
  /* The square crop cuts top and bottom off Michael's 5:7 frame, and centred
     that leaves his head against the top of the circle. 22% slides the window
     up the source so both faces sit in the middle of their avatar. Nithin's
     photograph is square, so this changes nothing for it. */
  .founder__img { object-position: center 22%; }

  .founder__meta { padding: 0; }
  .founder__name { font-size: 1.02rem; gap: 9px; }
  .founder__role { margin-top: 2px; font-size: .85rem; }
  .founder__li { width: 29px; height: 29px; border-radius: 8px; }
  .founder__li svg { width: 15px; height: 15px; }

  /* --- the left column, centred, AND THE BUTTON MOVED BELOW THE CARDS ---
     On a phone the column IS the page — there is no second column for it to
     be aligned against, and left-aligned type running the full width of a
     390px screen with a centred button under it reads as two decisions
     rather than one. Everything centres together.

     THE ORDER CHANGES TOO. Stacked, "Talk to us" sat between the belief line
     and the two founders, so the section asked for the contact before it had
     introduced anyone. It belongs after the faces.

     `display: contents` IS WHAT MAKES THAT POSSIBLE WITHOUT TOUCHING THE
     MARKUP. The button lives inside .founders__intro, and no amount of
     `order` moves a child past its own parent's sibling. Dropping the
     intro's BOX — not its contents — promotes its five children to grid items
     of .founders__grid alongside .founders__cards, and from there `order` can
     put the cards fourth and the button fifth.

     It is a plain <div> with no role, so removing its box costs nothing in
     the accessibility tree. The beat targets in script.js §0d are the
     children (.eyebrow, .founders__title, .founders__belief-text,
     .founders__cta, .founder), never the intro itself, so the entrance is
     unaffected.

     ROW GAP GOES TO ZERO because of it. The grid's gap is sized for the
     desktop split's two columns; applied between five newly-promoted children
     it would put 40px between the eyebrow and its own heading. Each element
     keeps its own margin instead, which is what was spacing them before. */
  .founders__intro { display: contents; }
  .founders__grid { row-gap: 0; text-align: center; }
  .founders__cards { order: 4; margin-top: clamp(26px, 6.5vw, 34px); }
  .founders__cta   { order: 5; }

  /* THE LEAD PARAGRAPH IS OUT ON THE PHONE, not restyled. It is the longest
     thing in the column and the one piece of it that repeats what the
     headline and the belief line already say between them — four lines of
     supporting copy between a three-word heading and the belief block is what
     was making this screen feel long. It stays on desktop, where the column
     has the room and the paragraph is what fills it.

     display:none rather than deleted from the markup: the desktop composition
     is built around it, and §0d's body beat still resolves against it
     harmlessly when it is not rendered. */
  .founders__lead { display: none; }

  /* The belief block keeps its measure but centres it, so the two lines break
     symmetrically instead of leaving a short ragged second line. */
  /* 28-34, not the 20 it first got: with the paragraph gone the heading sits
     directly above this label, and the gap has to match the one under the
     eyebrow above it or the three blocks read as unevenly spaced. */
  .founders__belief { margin-top: clamp(28px, 7vw, 34px); }
  .founders__belief-text { margin-inline: auto; }

  /* It follows the cards now, not the belief line, so it gets the gap a
     block-to-block break wants rather than the tighter one it had inside the
     text column. */
  .founders__cta { margin-top: clamp(26px, 6.5vw, 34px); }

  /* --- the cards, centred as groups -----------------------------------
     The row keeps its shape — avatar, then name and role beside it — and the
     PAIR is centred in the card rather than hung off the left edge.

     The meta block is given a fixed basis so the two rows agree. Centring a
     row whose width depends on the name in it puts "Nithin Sabu" and "Michael
     George" at different offsets, and two avatars a dozen pixels apart down a
     stacked list reads as a mistake rather than as centring. One width, both
     cards, avatars in a column. */
  .founder { justify-content: center; }
  .founder__meta { width: 168px; }

  /* --- ONE ALIGNMENT FOR THE WHOLE SCROLL ------------------------------
     The page is one storytelling scroll, and on a phone every scene is a
     single column with nothing beside it. The hero, the statement, the app,
     the features, the early-access section and the founders were already
     centred; `how` and `clinics` were not, so the story changed alignment
     twice on the way down for no reason the reader can see. They are the
     last two, and they centre the same way the rest do.

     WHAT DOES NOT CENTRE, AND WHY:

     · The LIST ROWS — .clinic__points li and the .steps cards. Their bodies
       run to two and three lines, and centred wrapped body copy inside a row
       that also holds an icon is unreadable. The founders rows centre because
       their content is one short line each; these are not the same object.

     · The CLOSING CTA. Its copy sits over a photograph composed with the
       subject on the right, and .cta__ground lays a scrim down the LEFT to
       keep white type legible over it. Centring the copy walks it onto the
       face and off the scrim.

     · The FOOTER. Six columns of links; centred link lists are harder to
       scan than left-aligned ones, and no other footer on the web does it. */
  .system__head,
  .clinic__copy {
    text-align: center;
    margin-inline: auto;
  }
  /* The leads carry their own measure (34ch / 46ch); without this they keep
     it and hug the left edge of a centred block. */
  .system__head .section__lead,
  .clinic__copy .section__lead {
    margin-inline: auto;
  }
  /* text-align inherits, so centring .clinic__copy centred the point cards
     inside it too — the exact thing ruled out above. The rows are inside the
     block being centred, so they have to opt back out explicitly. (The .steps
     cards need no equivalent: they are a sibling of .system__head, not a
     child, so the centring never reached them.) */
  .clinic__points li { text-align: left; }

  /* --- THE WAITLIST SOCIAL PROOF, CENTRED ------------------------------
     Both copies of it: the phone hero's and the early-access section's. Every
     other thing in both of those columns is centred on a phone, and a row of
     faces hanging off the left margin under a centred form was the one piece
     that was not.

     THE CLUSTER STACKS ABOVE THE LINE, and it has to. Side by side the row
     wants 445px — 98 for the faces, 12 for the gap, then 115 and 210 for the
     figure and its sentence — against the 350 a 390px phone actually has. It
     was not off-centre, it was FULL WIDTH: faces jammed against the left
     margin, sentence running to the right one, nothing in the middle to
     centre. No amount of justify-content moves a row that already spans
     everything.

     Stacked, the faces get their own centred line and the figure and its
     sentence fit side by side underneath on one — 335px, which does fit —
     so the whole thing reads as one centred unit instead of a wide row with
     a hole in it. Below ~320px the sentence wraps under the figure and stays
     centred, which is the same shape one line further down. */
  .wl__count--stat {
    flex-direction: column;
    align-items: center;
    gap: 10px;
  }
  .wl__stat-text { justify-content: center; }
  /* The last avatar carries an 8px right margin to open the gap to the figure
     beside it. Stacked there is nothing to its right, and that margin is
     inside the centred box — so it pushed the visible circles 8px left of the
     centre they were supposed to be on. */
  .wl__faces img:last-child { margin-right: 0; }

  /* The app scene's three columns collapse to a single centred flow: copy,
     then the device. The floating cards are decoration around a composition
     that no longer exists at this width, so they are not stacked into the
     column — they are removed from the flow entirely. */
  .app__stage { grid-template-columns: minmax(0, 1fr); }
  .app__copy  { text-align: center; align-items: center; }
  .appcard    { display: none; }

  /* Text must never sit under the fixed nav on a short phone screen. */
  .section { padding-block: calc(var(--nav-h) + 28px) 56px; }
}


/* ==========================================================================
   20. iOS SAFE AREAS — notch, Dynamic Island, home indicator
   --------------------------------------------------------------------------
   THIS SECTION AND `viewport-fit=cover` ARE ONE CHANGE, NOT TWO. Without the
   meta tag iOS reports 0 for all four insets and everything here is inert.
   WITH it, iOS stops insetting the viewport for us and hands the full screen
   over — including the strip behind the notch and the home indicator. So the
   tag alone would push content UNDER the notch; these rules are what put it
   back. Ship them together or neither.

   Every value below is `env(..., 0px)` or wrapped in `max()`, so on desktop,
   Android and every non-notched device the insets are 0 and each rule
   resolves to exactly the value it had before. The desktop layout cannot move.
   ========================================================================== */

:root {
  --safe-t: env(safe-area-inset-top, 0px);
  --safe-r: env(safe-area-inset-right, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
  --safe-l: env(safe-area-inset-left, 0px);

  /* THE HIGHEST-LEVERAGE LINE IN THIS FILE. The nav bar is the datum the whole
     page is laid out against: scroll-padding-top, every .section top pad, the
     mobile drawer's `top`, the app and features scenes, .mhero's top padding
     in mobile.css — eleven call sites, all of them `calc(var(--nav-h) + x)`.
     Folding the top inset in here fixes all eleven at once and leaves them
     literally unedited. On a non-notched screen --safe-t is 0 and this is
     78px, exactly as before. */
  --nav-h: calc(var(--nav-h-base) + var(--safe-t));
}

/* --- horizontal: the landscape notch ------------------------------------
   Rotated, the notch eats ~59px off one side. max() means the gutter is the
   larger of the design padding and the inset, never their sum — so portrait
   and desktop keep the exact --pad they always had. */
.container {
  padding-left: max(var(--pad), var(--safe-l));
  padding-right: max(var(--pad), var(--safe-r));
}

/* --- fixed nav ------------------------------------------------------------
   The bar's BACKGROUND still has to reach the physical top edge — a white
   strip above a translucent bar is worse than no bar. So the element is not
   moved; --nav-h above made it taller and this pads its content down, which
   centres the logo and links in the region below the island. */
.nav { padding-top: var(--safe-t); }

/* --- the mobile drawer ----------------------------------------------------
   `top` already follows --nav-h. The bottom inset keeps the last button clear
   of the home indicator, and the max-height/scroll pair is for LANDSCAPE:
   six stacked items in a 390px-tall window is taller than the screen, and
   without this the last one is unreachable. overscroll-behavior stops the
   rubber-band from chaining to the page behind it. */
.nav__mobile {
  padding-left: max(var(--pad), var(--safe-l));
  padding-right: max(var(--pad), var(--safe-r));
  padding-bottom: calc(28px + var(--safe-b));
  max-height: calc(100dvh - var(--nav-h));
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;
}
/* The max-height above is what makes the drawer usable in landscape, but the
   drawer is a flex COLUMN, and a flex column that cannot fit its children
   shrinks them — measured in landscape the "For clinics" button collapsed from
   48px to 28px tall, i.e. the height cap turned into a squash instead of a
   scroll. Pinning flex-basis stops the shrink so the overflow scrolls. */
.nav__mobile > * { flex-shrink: 0; }

/* A CLOSED DRAWER IS STILL IN THE PAGE. It is only pushed out of frame by a
   transform, which hides it from the eye and from nothing else: its six links
   keep their place in the tab order, screen readers still read them, and in
   landscape its bottom edge is hit-testable underneath the nav bar. Adding
   visibility takes it out of all three. The 0s transition is DELAYED by the
   length of the slide on close, so the drawer stays visible while it travels
   and only then disappears — and is applied with no delay on open so it is
   present the instant it starts moving back in. */
.nav__mobile {
  visibility: hidden;
  transition: transform .45s var(--ease), visibility 0s linear .45s;
}
.nav__mobile.open {
  visibility: visible;
  transition: transform .45s var(--ease), visibility 0s;
}

/* --- overlays -------------------------------------------------------------
   The modal is a fixed grid centring a card; padding is its only gutter, so
   the insets go straight onto it or the card can sit under the island. */
.modal {
  padding-top: calc(20px + var(--safe-t));
  padding-right: calc(20px + var(--safe-r));
  padding-bottom: calc(20px + var(--safe-b));
  padding-left: calc(20px + var(--safe-l));
}

/* --- the floor ------------------------------------------------------------
   The home indicator sits over the last ~34px of the screen. The footer is
   the last thing on every page, so it is the only place this has to be paid. */
.footer { padding-bottom: var(--safe-b); }

/* The hero's scroll cue is bottom-anchored and would land behind the
   indicator on an iPhone. */
.scrollcue { bottom: calc(22px + var(--safe-b)); }


/* ==========================================================================
   21. TOUCH — hit areas, tap behaviour, and hovers that must not stick
   --------------------------------------------------------------------------
   Measured on emulated iPhone/iPad viewports, not assumed. Everything here is
   either scoped to a coarse pointer, or applies to a control that is
   `display: none` on desktop, so the ≥1024px layout cannot move.
   ========================================================================== */

/* --- 44x44 minimum hit areas ---------------------------------------------
   Apple's floor is 44x44pt. Each fix grows the HIT AREA and leaves the
   artwork alone — no icon, bar or glyph changes size. */

/* The hamburger measured 39x44 portrait and 39x32 landscape: three 23px bars
   inside 8px of padding. Centring them in a 44x44 box fixes both orientations
   at once. It is `display: none` above 980px, so desktop never renders it. */
.nav__toggle {
  box-sizing: border-box;
  min-width: 44px;
  min-height: 44px;
  align-items: center;
  justify-content: center;
}

/* The logo lockup is 36px tall — under the floor on iPad and in landscape,
   where the nav shortens. Coarse pointers only: with a mouse 36px is already
   a generous target and the desktop nav must not move by even a pixel. */
@media (pointer: coarse) {
  .logo { min-height: 44px; }
}

/* --- tap behaviour --------------------------------------------------------
   `manipulation` drops the ~300ms double-tap-to-zoom wait on CONTROLS while
   leaving pinch-to-zoom fully intact — that is the accessibility line, and it
   is why this is a control-level rule and not `touch-action` on the page. */
a, button, label, input, select, textarea,
[role="button"], [role="tab"],
[data-modal-open], [data-modal-close] {
  touch-action: manipulation;
  /* iOS flashes an opaque grey box over the whole tapped element. Replaced,
     not removed: killing it outright takes away the only feedback a tap gets
     before the page responds. This is the brand lavender at low alpha. */
  -webkit-tap-highlight-color: rgba(145, 121, 210, .14);
}

/* --- hovers that would otherwise latch ------------------------------------
   iOS applies :hover on tap and KEEPS it until you tap somewhere else, so a
   card left lifted is the normal outcome of touching it. These are exactly
   the transform-based hovers §19e already lists for reduced motion; :active
   is deliberately untouched, because on touch that IS the feedback. */
@media (hover: none) {
  .btn:hover { transform: none; }
  .step:hover,
  .rail__item:hover,
  .clinic__points li:hover,
  .app__points li:hover { transform: none; }
  .step:hover .step__marker,
  .rail__item:hover .rail__icon,
  .clinic__points li:hover .clinic__icon { transform: none; }
}

/* --- form fields ----------------------------------------------------------
   iOS zooms the whole page when a field under 16px takes focus, and does not
   zoom back out. .field already sets 1rem; this is the belt-and-braces rule
   for any field that ever lands outside it. */
@media (pointer: coarse) {
  input, select, textarea { font-size: max(16px, 1rem); }
}

/* When the software keyboard opens, focus scrolling must not tuck the field
   under the fixed nav. --nav-h already carries the top safe-area inset. */
input, select, textarea {
  scroll-margin-top: calc(var(--nav-h) + 16px);
  scroll-margin-bottom: 24px;
}


/* ==========================================================================
   22. HONEYPOT
   --------------------------------------------------------------------------
   The anti-spam field in both forms. It must be unreachable by a person and
   fully present to a bot, so it is moved off-screen rather than hidden:
   `display:none` and `visibility:hidden` are the two things a competent bot
   checks for before deciding whether to fill a field in.
   ========================================================================== */
.hp {
  position: absolute;
  left: -9999px;
  top: auto;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}
