/* ==========================================================================
   CELES GLOBAL — mobile overrides
   --------------------------------------------------------------------------
   Loaded LAST, after styles.css and clinics.css. EVERY rule in this file is
   inside a max-width media query, and the widest ceiling used anywhere in it
   is 1023px. There is not one declaration at the top level, which is what
   makes desktop invariance structural rather than a promise: at ≥1024px the
   browser never applies a single line of it.

   THERE ARE TWO CEILINGS AND THEY MEAN DIFFERENT THINGS.

     768px — phone-shaped corrections. Tap targets, the booking calendar, held
       media, form sizing. The 769–1023px band keeps whatever the existing
       breakpoints in styles.css and clinics.css already gave it.

     1023px — §7 only, the hero. That section is a layout change rather than a
       correction, and it is meant to replace the desktop hero everywhere the
       desktop hero is not being used. Its ceiling therefore sits directly
       under the 1024px desktop floor rather than at 768.

   NOTHING HERE EDITS AN EXISTING RULE. Where mobile needs a different value
   the original declaration is left exactly as it is in its own file and
   overridden here by cascade order. Nothing is renamed, moved or deleted.

   CONTENTS
     1  Tap targets                      ≤768
     2  The clinics hero plane           ≤768
     3  The frame behind the hero        ≤768
     4  Modals and forms under a thumb   ≤768
     5  The booking calendar             ≤768
     6  Scroll feel                      ≤768
     7  The hero, rebuilt as one layer   769–1023
     8  The clinics hero, recomposed     ≤768
     9  The statement scene's tag field  ≤768
    10  How the sync works               ≤768
    11  THE MOBILE HERO — its own screen ≤768
   ========================================================================== */

@media (max-width: 768px) {

  /* --- 1. Tap targets ----------------------------------------------------
     Measured at 390px before this file existed: fifteen controls under the
     44px minimum — the logo at 36, the hamburger at 32, and every footer
     link at 34. The hamburger is the worst of them, because it is the only
     way to reach navigation on a phone.

     THE HIT AREA GROWS, THE GLYPH DOES NOT. Growing the icon would change the
     look of the header; growing the area around it does not.

     AND THE HORIZONTAL HALF OF IT MUST NOT TAKE LAYOUT WIDTH. This first used
     `min-width: 44px` with a negative margin to claw the space back, and on
     the clinics page at 360px that overflowed the document by 2px: .nav__inner
     is a flex row and the clinics logo is 263.5px wide with its FOR CLINICS
     pill, so the row had no slack left to give. The button is therefore left
     at its natural 39px and the extra reach comes from a centred overlay,
     which is out of flow and cannot widen anything.

     The vertical half is free — the header is 78px tall, so a 44px minimum
     never lifts it. */
  .nav__toggle {
    position: relative;
    min-height: 44px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .nav__toggle::after {
    content: "";
    position: absolute;
    top: 50%;
    left: 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
  }

  .logo { min-height: 44px; }

  /* Footer links sat at 34px in a dense column. The padding separates them
     as much as it enlarges them — mis-taps between adjacent links are the
     actual failure here, not the links being hard to hit. */
  .footer__col a,
  .footer__legal a {
    display: inline-block;
    padding-block: 6px;
    min-height: 44px;
    line-height: 32px;
  }

  .nav__mobile a {
    min-height: 48px;
    display: flex;
    align-items: center;
  }

  /* --- 2. The clinics hero plane -----------------------------------------
     ~250 drifting tiles behind the headline. On a phone the plane is at the
     same scale as the type in front of it, so the two compete — and at 32%,
     which is where this sat first, they still did: measured at 375px, the
     phrase "Booking your calendar" ran straight through the second line of
     the headline and a Celes wordmark crossed the outline button. A tile that
     is legible is a tile that is competing, whatever its opacity says.

     SO IT IS TAKEN ALL THE WAY DOWN TO TEXTURE. At 8% no tile resolves into a
     readable word at arm's length; what survives is the grain and the tilt,
     which is the part that was ever doing any work. It is still the page's one
     piece of atmosphere — a flat cream hero would be a different design — but
     it is now unambiguously behind the content rather than among it.

     NO BLUR, DELIBERATELY, THOUGH IT WOULD SUIT THE LOOK. The plane drifts
     continuously, so a filter on it re-rasterises ~250 tiles every frame on
     the GPU — a real frame-rate cost on a mid-range phone in exchange for
     softening something already at 8%. mobile.js §2 stops the drift instead,
     which is both cheaper and invisible at this opacity.

     AND AT 8% IT WAS STILL THE WRONG ANSWER, so the plane is now OFF on
     phones. Texture that faint is not read as atmosphere on a 375px screen —
     it is read as words that failed to load, and the tiles nearest the
     headline still put fragments of "Booking your calendar" behind a line of
     type. The whole layer goes rather than its opacity: `display: none` on
     .chero__bg takes ~250 absolutely positioned tiles out of paint and
     compositing entirely, which is the single biggest thing this file does for
     frame rate on a mid-range phone.

     THE DECK IS STILL BUILT AND STILL IN THE DOM. clinics.js §1 is not gated
     and is not edited — this hides what it made, at this width only. mobile.js
     §2 (which paused the drift) is now belt and braces on an invisible layer;
     it is left alone because it is harmless and removing it would be a second
     place to get the width gate wrong.

     Composition — the stack, the focal image, the buttons — is §8. */
  .page-clinics .chero__bg { display: none; }

  /* WITH THE PLANE GONE THE SCRIM STOPS BEING A SCRIM. Its wash existed to buy
     the headline contrast against drifting tiles, and over flat cream that
     wash is just a second flat cream — it would leave the hero a dead
     rectangle, which is the one thing worse than a busy one.

     So at this width it is re-tasked as the hero's only atmosphere: a soft
     lavender bloom sitting behind the focal image and the headline, and a
     cream floor under the buttons and the trust row so the bottom of the stack
     settles into the page rather than stopping. Same element, same z-layer,
     nothing added to the markup — and both stops are page tokens, so this
     borrows the design system rather than inventing a colour for mobile. */
  .page-clinics .chero__scrim {
    opacity: 1;
    background:
      radial-gradient(84% 42% at 50% 30%,
        rgba(226, 216, 248, .46) 0%,
        rgba(226, 216, 248, .18) 48%,
        rgba(226, 216, 248, 0)   76%),
      linear-gradient(180deg,
        rgba(250, 249, 246, 0)   58%,
        rgba(250, 249, 246, .85) 100%);
  }

  /* --- 3. (retired) The frame behind the hero -----------------------------
     A 26KB mobile JPEG used to be painted as a background on .hero__video, so
     the hero box was never a bare rectangle in the few hundred milliseconds
     between layout and the first video frame decoding.

     THERE IS NO LONGER A VIDEO ON THIS SCREEN TO FRAME. §11 replaces the
     consumer hero outright below 768px with .mhero — a lavender field, no
     footage — and hides .hero entirely, so this rule was styling a
     display:none element. mobile.js §3 no longer restores the hero source at
     this width either, which is where the 9.4MB saving actually comes from.

     assets/img/hero-poster-mobile.jpg IS LEFT ON DISK. The 769–1023 band (§7)
     still shows the real hero, and that band is one narrowed window away from
     wanting this again. mobile-boot.js's poster handling is likewise
     unchanged — it is what stops the 1.24MB PNG being decoded. */

  /* --- 4. Modals and forms under a thumb ---------------------------------
     The waitlist dialogs are the only transactional surface on the consumer
     page, and a keyboard takes roughly half a phone screen when it opens.

     dvh, NOT vh. On mobile browsers vh is the LARGEST viewport — the one with
     the toolbars retracted — so a modal sized in vh is taller than the window
     it is in whenever the toolbar is showing, and its close button sits under
     the chrome. dvh tracks the visible box. */
  .modal__box {
    max-height: 92dvh;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }

  /* 16px is the threshold below which iOS Safari zooms the page on focus.
     A form that zooms on every field is a form the reader has to pinch back
     out of after each one. */
  .field input,
  .field select,
  .field textarea {
    font-size: 16px;
    min-height: 48px;
  }
  .field textarea { min-height: 96px; }

  .btn { min-height: 48px; }

  /* The close control is the escape route from a full-height dialog and was
     the smallest thing in it. */
  .modal__x {
    min-width: 44px;
    min-height: 44px;
  }

  /* --- 5. The booking calendar -------------------------------------------
     The one place on either page where a reader has to hit a small target
     accurately, and the only transactional step on the clinics page.

     Measured at 360px before this block: every one of the 36 day cells was
     36×36. `.day` is `aspect-ratio: 1/1` in a `repeat(7, 1fr)` grid, so the
     cell size is whatever a seventh of the grid happens to be — and the grid
     had 38px of inherited padding either side (20 from .container, 18 from
     .booker) leaving it 282px wide. Seven columns and six 5px gaps out of
     282px is 36px each, four pixels of finger either side of a 28px digit.

     THE CELLS ARE NOT ENLARGED DIRECTLY — THE GRID IS WIDENED AND THEY
     FOLLOW. Growing `.day` against a fixed-width grid would just overflow the
     panel. Giving the grid the panel's full inner width gives the columns the
     room instead, and the aspect-ratio does the rest.

     THE PADDING IS PINNED HERE FIRST SO THE PULL CAN MATCH IT. The negative
     margin has to cancel .booker's horizontal padding exactly: too little and
     the cells stay under 44, too much and they hang outside the card. That
     padding is 18px below 560px but a different value in the 561–768 band, so
     rather than guess at a number this sets it to 14 and pulls by 14. The
     grid then lands flush with the card's edge at every width in this file,
     which is where a phone calendar's columns belong anyway.

     At 360 — the narrowest width this is built for — that is a 320px grid:
     (320 − 6 × 2) ÷ 7 = 44.0 exactly. Wider phones only gain. The gap is 2px
     because those twelve pixels are the difference between clearing 44 and
     missing it, and gaps between cells of one calendar are not what anyone is
     aiming at. The day-of-week header is pulled by the same amount or its
     labels stop lining up with the columns underneath them. */
  .booker { padding-inline: 14px; }

  .cal__dow,
  .cal__grid {
    margin-inline: -14px;
    gap: 2px;
  }

  /* NO `min-height` FLOOR ON `.day`, DELIBERATELY. One was tried and it is
     the wrong shape of fix: `.day` is `aspect-ratio: 1/1`, so a height floor
     becomes a width floor, and a width floor on a `1fr` column is a demand the
     grid cannot meet — it overflowed the clinics page by 2px at 360, which is
     a horizontal scrollbar on the whole document to guarantee a size the
     arithmetic above already guarantees. The columns are what size the cells;
     nothing else should try to. */

  /* The month steppers were 36px round, and they are how a reader reaches any
     date that is not in the current month. Padded out rather than redrawn:
     the circle keeps its size, the hit area grows around it. */
  .cal__nav {
    min-width: 44px;
    min-height: 44px;
  }

  /* --- 6. Scroll feel ----------------------------------------------------
     Lenis is left running — its touch handling defers to native momentum, and
     the measured scroll on this page is already smooth. What it must not do
     is fight the browser's own overscroll, so the chain is closed at the
     page rather than at each scene. */
  html.lenis body { overscroll-behavior-y: none; }

  /* --- 8. The clinics hero, recomposed -----------------------------------
     THE MOBILE HERO IS THE DESKTOP HERO AT PHONE SCALE. Same layers, same
     children, same order, same words:

         background layer (§2 takes it off at this width)
         badge  "For clinics & dermatologists"     centred
         headline                                  centred
         "Book a practice demo"        filled      full width
         "Explore platform features"   outline     full width
         trust row, two lines                      centred

     NOTHING IS ADDED AND NOTHING IS REORDERED TO GET THAT. A phone-only
     <figure> holding clinic-consult.jpg used to sit above the badge, so the
     mobile hero had a subject once the plane behind it went; it is gone from
     clinics.html on request, and this file no longer has a rule for it. The
     only child of .chero in flow is .chero__inner, and inside it the order is
     the markup's. .chero__bg, .chero__scrim and .chero__cue stay absolutely
     positioned exactly as they are on desktop.

     `height: auto` with a `min-height` floor, NOT a fixed one. The stack fits
     a screen comfortably now, but a fixed height is what crops a trust row off
     the bottom on a short phone in landscape — the floor gives the full screen
     the composition wants and lets the section grow if it ever has to.

     THE CENTRING IS THE FLEX COLUMN'S, WHICH IS WHY THIS IS NOT LEFT AS THE
     DESKTOP `place-items: center` GRID. `justify-content: center` centres the
     stack between the nav and the bottom edge with the padding respected; the
     grid's `place-items` centres it against the padding box and then the
     22px side padding cannot be read as a margin. */
  .page-clinics .chero {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    height: auto;
    min-height: 100svh;
    padding: calc(var(--nav-h) + 18px) 22px 34px;
  }

  /* The padding that centred the block under the fixed nav is now on .chero
     itself, and leaving it here too would double it. `min(940px, 92vw)` from
     clinics.css would also inset the column another 15px inside the 22px this
     section is built on, so the badge, the headline and the buttons would each
     sit on a different left edge from the trust row. One column, one margin. */
  .page-clinics .chero__inner {
    width: 100%;
    max-width: none;
    padding-top: 0;
  }

  /* 17ch was set for a 4.3rem desktop line. At phone sizes it forces a ragged
     four-line break; unset, `text-wrap: balance` gets three even lines. */
  .page-clinics .chero__title {
    max-width: none;
    font-size: clamp(1.95rem, 8.4vw, 2.45rem);
  }

  .page-clinics .chero__badge { margin-bottom: clamp(16px, 3.6vw, 22px); }

  /* Stacked and full-bleed to the container, so the filled/outline hierarchy
     reads down the screen instead of across it. Both were already .btn--lg;
     §4's 48px floor plus their own padding puts them at ~56. */
  .page-clinics .chero__actions {
    width: 100%;
    flex-direction: column;
    gap: 12px;
    margin-top: clamp(22px, 5vw, 30px);
  }
  .page-clinics .chero__actions .btn { width: 100%; }

  /* Two rows, not two columns — side by side these wrapped mid-phrase. The
     top margin is what keeps the row clear of the tiles below the fold. */
  .page-clinics .chero__meta {
    flex-direction: column;
    align-items: center;
    gap: 9px;
    margin-top: clamp(22px, 5vw, 30px);
  }

  /* The cue points at the next section from the bottom edge of a section that
     is now taller than the screen, so it is no longer pointing at anything the
     reader can see. The stack itself is the invitation to scroll. */
  .page-clinics .chero__cue { display: none; }

  /* --- 9. THE STATEMENT SCENE — THE TAG FIELD GETS ITS OWN BAND -----------
     "What's in Clinic OS" collided with its own decoration on a phone, and the
     cause is a desktop mechanism that is switched off at this width rather
     than a rule that is wrong.

     WHAT THE DESKTOP DOES. .scene2__stage is one pinned cell holding two
     things in sequence: the statement, then the six cards. The ambient tag
     field (.amb, clinics.css §4c) is `position: absolute; inset: 0` on that
     stage — the whole stage, because on desktop the stage is one screen and
     the two blocks take turns inside it. .scene2__wash, an opaque white pane,
     fades up on beat 2 as the statement leaves, so the tags are covered before
     the cards arrive. Statement on tags, cards on clean white, never both.

     WHAT HAPPENS BELOW 980px. clinics.js §2 gates that whole sequence on
     PIN_GATE, so .is-seq is never added: the stage is a plain flow column,
     statement above cards, both permanently visible and the stage as tall as
     the two of them together. No timeline runs, so the wash never fades up —
     it sits at opacity 0 for the life of the page. `inset: 0` therefore spans
     BOTH blocks, and the four surviving corner tags are pinned to the corners
     of that combined box: the top pair lands in the headline, the bottom pair
     lands between the feature cards, and the layer runs to the last pixel of
     the section — which is the reported bug, exactly.

     THE FIX IS TO GIVE THE LAYER THE RIGHT BOX, NOT TO DELETE IT. This block
     used to `display: none` the whole field. That removed the collision and
     the decoration with it; the brief is that the words stay. So .amb stops
     being an absolute overlay on the stage and becomes a GRID ITEM SHARING
     ROW 1 WITH THE STATEMENT — same cell, stretched to it. Its own
     `position: relative` then makes it the containing block for the ten
     absolutely positioned slots inside it, so `top: 11%` and `bottom: 12%`
     resolve against the statement's band instead of the whole section. The
     tags cannot reach the cards because their coordinate space stops where
     the statement does, and `overflow: hidden` (already on .amb) clips the
     drift at that boundary rather than at the section's.

     THIS IS THE HOMEPAGE STATEMENT'S STRUCTURE, WHICH IS WHAT WAS ASKED FOR.
     styles.css §5 gives .statement a full screen with the sentence centred in
     it and the chips living in the empty bands above and below — measured at
     375px: section 812 tall, sentence 1166–1308, chips at 882–1014 and
     1410–1580, and nothing within 150px of the type. The rules below build
     the same figure out of the pieces this page has: a tall centred band for
     the statement, the tag field scoped to exactly that band, the corner slots
     in the air above and below it, and the six cards starting after it.

     NOTHING IN clinics.js NEEDS TELLING. §5's float tween writes `y`, which is
     a transform and cannot move a tag out of a clipped box by more than its
     ±14px amplitude; §5b's cursor glow never starts on a phone — it gates on
     (hover: hover) and (pointer: fine).

     THE 980px BRANCH IS UNTOUCHED ABOVE 768. The 769–1023 band keeps the
     absolute-overlay behaviour it has today. */

  /* Two explicit rows, so .amb can be placed in the first one by name. The
     stage is already `display: grid` with one implicit column; stating the
     column here is what lets `grid-area: 1 / 1` mean the same thing to all
     three participants. */
  .page-clinics .modules .scene2__stage {
    grid-template-columns: minmax(0, 1fr);
    row-gap: clamp(40px, 10vw, 56px);
    padding-bottom: clamp(76px, 15vw, 108px);
  }

  /* THE BAND, AND ITS HEIGHT IS THE WHOLE MECHANISM. The tags are placed by
     PERCENTAGE — `top: 11%`, `bottom: 12%` — so the only thing that decides
     how far they land from the type is how tall their box is. Boxing the layer
     without also giving it room would put the corners straight onto a headline
     that fills its own container.

     72svh is the homepage's full screen less the room the stage's top padding
     is already holding under the nav. The header centres inside it, and the
     four corners then have a clear band to sit in. Measured at 375×812:

       band                 1935 – 2520
       top pair             2001 – 2041      96px clear of the eyebrow
       eyebrow + headline   2137 – 2317
       bottom pair          2400 – 2453      83px clear of the headline
       first card           2560             107px clear of the bottom pair

     Every gap is more than four times the ±14px the float tween (clinics.js
     §5) can move a tag, so nothing closes even at the extremes of the drift.
     At 414×896 the same figures are 116 / 106 / 116, and at 360×740 — the
     narrowest this file is built for — 73 / 63 / 100.

     92vw (clinics.css §3) leaves 15px of side padding at 375, narrower than
     the 22px the hero and the cards use, so a centred headline looked wider
     than the column it introduces. Full width plus explicit padding puts all
     three on the same margin. */
  .page-clinics .modules .scene2__statement {
    grid-area: 1 / 1;
    align-self: stretch;
    display: flex;
    flex-direction: column;
    justify-content: center;
    min-height: 72svh;
    width: 100%;
    max-width: none;
    margin: 0;
    padding-inline: 22px;
  }

  /* The field, re-boxed. `inset: auto` unwinds clinics.css's `inset: 0` — on a
     relatively positioned element those four offsets would be nudges rather
     than edges, and 0 nudges nothing, but leaving them would read as though
     the box were still being anchored by them. */
  .page-clinics .modules .amb {
    position: relative;
    inset: auto;
    grid-area: 1 / 1;
    align-self: stretch;
    justify-self: stretch;
    width: 100%;
  }

  /* The cursor glow is a 600px radial keyed to --mouse-x/--mouse-y, and
     clinics.js never starts the tracker on a touch device — so it would rest
     at its 50%/50% fallback forever, a soft violet blob sitting behind the
     headline that no gesture can move or explain. */
  .page-clinics .amb__glow { display: none; }

  /* Row 2, stated rather than left to auto-placement: with .amb explicitly in
     row 1 the cards would otherwise be placed by the auto-flow algorithm
     against a grid it does not know the shape of. */
  .page-clinics .modules .scene2__body { grid-area: 2 / 1; }

  /* The homepage's own phone size for the same object (styles.css, .chip at
     ≤760: .76rem / 7px 13px). The desktop tag is `clamp(12px, .9vw, 14px)`,
     which bottoms out at 12px and reads a shade too present against a 1.95rem
     headline; matching the consumer page settles it as background and makes
     the two statement sections look like one system on a phone. */
  .page-clinics .amb__tag {
    font-size: .76rem;
    padding: 7px 13px;
    gap: 7px;
  }

  /* The same 22px on the two sections whose blocks this file re-composes.
     `--pad` IS DELIBERATELY NOT TOUCHED: §5's calendar arithmetic is derived
     from its 20px value at this width — (320 − 6×2) ÷ 7 = 44.0 exactly — and
     raising it globally takes the day cells to 43.4 and back under the tap
     target. Two selectors here, and the booker keeps the number it was built
     against. */
  .page-clinics .modules .container,
  .page-clinics .syncs .container { padding-inline: 22px; }

  /* --- 10. HOW THE SYNC WORKS — the steps centre up --------------------
     One column since 760px (clinics.css §10) and the rail is gone with it, so
     what is left is five left-aligned blocks under a centred section head:
     the number, the title and the line each start on the left margin with the
     rest of the column empty to their right, and with no rail joining them
     they read as five stray items rather than one sequence.

     CENTRED ON THE COLUMN INSTEAD, which is what the section head above them
     is already doing. `justify-items` on the list centres each step in its
     row; the same property inside the step centres the lavender circle over
     its own title, and the numbers then stack straight down the middle of the
     screen — the vertical equivalent of the rail they lost.

     THE MEASURE IS WHAT MAKES IT READ AS A COLUMN. Centred text set to the
     full 331px is ragged on both edges at this size; 26ch holds each
     description to two lines with a deliberate shape. */
  .page-clinics .flow {
    justify-items: center;
    row-gap: 34px;
  }
  .page-clinics .flowstep {
    justify-items: center;
    text-align: center;
    gap: 8px;
    max-width: 26ch;
  }
  /* The circle's 6px was clearance under a marker with text starting beside
     it. Stacked and centred it is the top of a three-part block and wants the
     step's own rhythm, not its own. */
  .page-clinics .flowstep__no { margin-bottom: 2px; }

  /* ========================================================================
     11. THE MOBILE HERO — a separate screen, not a reflowed desktop one
     ------------------------------------------------------------------------
     WHAT THIS REPLACES. styles.css §hero puts the footage on the left and the
     copy beside it; §7 of this file rebuilt that for narrow screens by turning
     the video into a full-bleed backdrop with the copy bottom-anchored on top.
     Both are the same composition solved twice. Below 768px neither runs: the
     consumer hero is now .mhero (index.html §1m), an independent block with no
     photograph in it at all, and .hero is taken out of the layout.

     AND IT IS FIVE THINGS, NOT SIX. Badge, eyebrow, headline, description, two
     buttons — then it stops. Daily / Verified / Adaptive do not appear on a
     phone in any form: no column, no deck, no carousel. They are .hero__rail's
     content and .hero is the 769px-and-up hero, so they are not lost, they are
     simply not this screen's argument. A phone hero has room to say one thing
     well, and the thing is the headline and the two buttons under it.

     THE SWAP IS TWO DECLARATIONS AND IT IS DELIBERATELY THAT BLUNT. There is
     no width at which both are in the document, so nothing has to be reasoned
     about at the boundary: at ≤768 you get .mhero, at ≥769 you get .hero, and
     the two never negotiate.

     `!important` ON display IS NOT A SPECIFICITY SHORTCUT. .mhero carries the
     `hidden` attribute, and Chrome implements the HTML spec's suggested
     rendering as `[hidden]:not([hidden="until-found"]) { display: none
     !important }` — no author rule at any specificity beats that without one
     of its own. The attribute is what makes desktop invariance structural: at
     ≥769px this section is removed by the UA sheet, not by a media query of
     mine being written correctly. mobile.js §4 drops the attribute so the
     accessibility tree agrees, but the CSS is what paints it at first paint.

     §7's 1023px ceiling now carries a 769px floor for the same reason. Its
     rules only ever described .hero, and at ≤768 .hero is not rendered.
     ======================================================================== */
  .hero  { display: none; }
  .mhero { display: flex !important; }

  /* One screen, everything centred in it, and `min-height` rather than
     `height` so a small phone in landscape grows the section instead of
     cropping the buttons off the bottom.

     THE BOTTOM PADDING IS DOING TWO JOBS AND THE LARGER ONE IS COMPOSITION.
     It is the breathing room under the actions — with the deck gone the hero
     is five short blocks, and without it the last button lands near the fold
     with the next section's edge already showing beneath it, which reads as
     the screen having been cut off rather than having ended. It is also the
     band the cue sits in. It is deliberately bigger than the top padding: the
     column is centred between the two, so a heavier floor lifts the headline
     slightly above the optical centre, which is where a centred composition
     wants it.

     svh, NOT dvh. dvh remeasures as the browser toolbars retract, which on a
     centred column means the whole composition slides while it is being read.
     svh is the toolbars-visible height: laid out once, for the smallest the
     window ever gets, and never moves. */
  .mhero {
    position: relative;
    isolation: isolate;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    min-height: 100svh;
    /* The top pad already clears the Dynamic Island: --nav-h carries the top
       inset since styles.css §20. The bottom gains the home-indicator inset
       on top of its existing floor — both ends grow together, so the optical
       lift described above survives. On a non-notched phone --safe-b is 0 and
       this is the clamp it always was. */
    padding:
      calc(var(--nav-h) + 14px) 22px
      calc(clamp(76px, 14vh, 104px) + var(--safe-b));
    overflow: hidden;
    background: var(--cream);
  }

  /* --- the ambient field -------------------------------------------------
     A pale lavender wash with four soft blobs drifting on it. The wash is a
     linear-gradient on the layer itself and the blobs are radial-gradients on
     their own elements — NO `filter: blur()` ANYWHERE, deliberately. A blurred
     78vw element is a full-viewport surface re-rasterised on the GPU every
     frame it moves, which is the single most expensive thing an ambient
     background can do on a mid-range phone. A radial-gradient with a soft
     falloff is the same picture for free.

     Every animated property is transform. Nothing here can invalidate layout,
     so the drift cannot cost a reflow however long it runs. */
  .mhero__bg {
    position: absolute;
    inset: 0;
    z-index: 0;
    overflow: hidden;
    pointer-events: none;
    background: linear-gradient(168deg,
      #f6f2fd 0%,
      #faf8fe 38%,
      #fdfcff 68%,
      #ffffff 100%);
  }

  .mhero__blob {
    position: absolute;
    width: 82vw;
    height: 82vw;
    border-radius: 50%;
    will-change: transform;
    animation: mheroDrift 26s var(--ease-soft, ease-in-out) infinite alternate;
  }

  /* Four corners, four clocks. The durations are deliberately coprime-ish and
     the delays negative so the layer is already mid-drift at first paint —
     four blobs starting together read as one pulsing shape rather than a
     field. */
  .mhero__blob--1 {
    top: -30vw; left: -32vw;
    background: radial-gradient(closest-side,
      rgba(201, 184, 240, .58), rgba(201, 184, 240, 0));
    animation-duration: 26s;
    animation-delay: -3s;
  }
  .mhero__blob--2 {
    top: -26vw; right: -34vw;
    background: radial-gradient(closest-side,
      rgba(214, 226, 246, .55), rgba(214, 226, 246, 0));
    animation-duration: 31s;
    animation-delay: -11s;
  }
  .mhero__blob--3 {
    bottom: -34vw; left: -30vw;
    background: radial-gradient(closest-side,
      rgba(226, 216, 248, .5), rgba(226, 216, 248, 0));
    animation-duration: 23s;
    animation-delay: -7s;
  }
  /* THE WARM CORNER IS THE FAINTEST OF THE FOUR AND IT IS BARELY WARM. It
     started at rgba(244, 224, 214, .4) — the peach from the avatar stack — and
     at that strength it read as an orange corner on a lavender screen: the one
     thing in the field that named a second colour. It is kept, because four
     identical lavenders is a gradient rather than a field and the eye needs
     one of them to be doing something else, but it is pulled most of the way
     back toward the palette and dropped to .26 so it registers as warmth in
     the light rather than as a colour of its own. */
  .mhero__blob--4 {
    bottom: -30vw; right: -30vw;
    background: radial-gradient(closest-side,
      rgba(238, 224, 232, .26), rgba(238, 224, 232, 0));
    animation-duration: 35s;
    animation-delay: -17s;
  }

  /* Small numbers on purpose. At 82vw a 6% translate is ~23px over 26 seconds
     — under a pixel a second, which is the difference between a background
     that is alive and one that is moving. */
  @keyframes mheroDrift {
    from { transform: translate3d(0, 0, 0)       scale(1); }
    to   { transform: translate3d(6%, -5%, 0)    scale(1.14); }
  }

  /* mobile.js §4 adds this when the section leaves the viewport. Four
     compositor layers animating behind content nobody is looking at is pure
     battery, and `animation-play-state` stops them without unsetting
     anything, so scrolling back resumes mid-drift rather than snapping. */
  .mhero.is-idle .mhero__blob { animation-play-state: paused; }

  /* --- the copy ----------------------------------------------------------
     Its own namespace throughout. Nothing here reuses .hero__*, .trust or the
     .is-ready entrance system in styles.css — those carry `opacity: 0` resting
     states that are released by a class this component does not participate
     in, so borrowing any of them would leave this hero invisible. */
  .mhero__inner {
    position: relative;
    z-index: 1;
    display: grid;
    justify-items: center;
    text-align: center;
    width: 100%;
  }

  /* Glass, matching the desktop trust pill's recipe rather than inventing a
     second one. */
  .mhero__badge {
    display: inline-flex;
    align-items: center;
    gap: 2px;
    margin: 0 0 clamp(18px, 4.6vw, 24px);
    padding: 6px 15px 6px 9px;
    border-radius: 999px;
    background: rgba(255, 255, 255, .78);
    border: 1px solid rgba(255, 255, 255, .9);
    -webkit-backdrop-filter: blur(10px);
            backdrop-filter: blur(10px);
    box-shadow: 0 4px 16px rgba(90, 74, 140, .08);
  }
  .mhero__faces {
    display: inline-flex;
    align-items: center;
    padding-right: 9px;
  }
  /* PLACEHOLDER, same as .trust__faces: swap for real <img> when there are
     faces to show. The three tints are the desktop pill's exact three. */
  .mhero__faces i {
    width: 19px; height: 19px;
    border-radius: 50%;
    border: 2px solid #fff;
    margin-right: -7px;
    background: linear-gradient(140deg, var(--lav-300), var(--lav-600));
  }
  .mhero__faces i:nth-child(2) { background: linear-gradient(140deg, #f4d9c9, #d9a184); }
  .mhero__faces i:nth-child(3) { background: linear-gradient(140deg, #d5e3f5, #8fa8c9); }
  .mhero__badgetext {
    font-family: var(--font-head);
    font-weight: 600;
    font-size: .78rem;
    color: var(--ink-2);
    white-space: nowrap;
  }

  .mhero__eyebrow {
    margin: 0 0 clamp(10px, 2.6vw, 14px);
    font-family: var(--font-head);
    font-weight: 700;
    font-size: .7rem;
    letter-spacing: .2em;
    text-transform: uppercase;
    color: var(--lav-600);
  }

  /* 13ch is what holds this sentence to three lines. Unconstrained it breaks
     4/2/1 and the block reads as a paragraph; `balance` alone still leaves a
     short last line at this size. The two together give three even lines. */
  .mhero__title {
    margin: 0 0 clamp(14px, 3.6vw, 20px);
    max-width: 13ch;
    font-family: var(--font-head);
    font-weight: 700;
    font-size: clamp(2.05rem, 8.6vw, 2.6rem);
    line-height: 1.04;
    letter-spacing: -.042em;
    color: var(--ink);
    text-wrap: balance;
  }

  .mhero__sub {
    margin: 0 0 clamp(22px, 5.4vw, 30px);
    max-width: 33ch;
    font-size: .97rem;
    line-height: 1.6;
    color: var(--ink-soft);
    text-wrap: pretty;
  }

  /* The mini waitlist is the phone hero's ONLY action now - "For clinics"
     used to sit under it and has moved to the nav and the drawer. Full width
     of the copy column: an inline email field narrower than that starts
     truncating the placeholder. */
  .mhero__wl {
    width: 100%;
    max-width: 360px;
    margin: 0 auto;
    text-align: center;
  }
  .mhero__wl .wl__row { gap: 8px; }
  .mhero__wl .wl__done { justify-content: center; }

  /* The stat is centred with the rest of the phone hero, and it is the last
     thing in it - so it gets the room the clinics button used to occupy
     rather than that space being left as a hole. */
  .mhero__wl .wl__count--stat {
    justify-content: center;
    margin-top: 20px;
  }

  /* --- the scroll cue ----------------------------------------------------
     THE HERO ENDS AT THE BUTTONS. Daily / Verified / Adaptive are not restated
     on a phone in any form — they are .hero__rail's job, and .hero is the
     769px-and-up hero. What the composition needs instead of a deck is room to
     stop: `justify-content: center` on .mhero centres the copy in the screen,
     and the padding below is what stops the last button sitting on the fold
     with the next section's edge already showing under it.

     A LINE AND A TRAVELLING DOT, NOT A CHEVRON. A chevron at the bottom of a
     centred, still composition is an arrow drawn on it; a hairline with a dot
     running down it is the same information as movement, which is what the
     rest of the page is made of.

     ABSOLUTE, SO IT COSTS THE COLUMN NOTHING. In flow it would be a fifth item
     for the flex column to centre, which pulls the headline up off centre to
     make room for it. Out of flow, the copy is centred on the screen and the
     cue simply sits near the floor of it. */
  .mhero__cue {
    position: absolute;
    left: 50%;
    bottom: calc(clamp(18px, 3.4vh, 34px) + var(--safe-b));
    z-index: 1;
    transform: translateX(-50%);
    display: block;
    width: 1.5px;
    height: 38px;
    border-radius: 2px;
    /* Measured on screen at .34 the rail was invisible — a hairline at a third
       opacity over a near-white field is nothing at all, and the dot appeared
       to be travelling down empty space. The stops are stronger and the fade
       is now only at the two ends, so the middle of the line is solid enough
       to be read as a track without the line becoming a mark on the page. */
    background: linear-gradient(180deg,
      rgba(122, 97, 189, 0)   0%,
      rgba(122, 97, 189, .5) 32%,
      rgba(122, 97, 189, .5) 68%,
      rgba(122, 97, 189, 0) 100%);
  }
  /* The 44px tap target, as an overlay so a 1px line does not become a 44px
     column in the layout — the same trick §1 uses on the nav toggle. */
  .mhero__cue::after {
    content: "";
    position: absolute;
    top: 50%; left: 50%;
    width: 44px; height: 44px;
    transform: translate(-50%, -50%);
  }
  .mhero__cuedot {
    position: absolute;
    top: 0; left: 50%;
    width: 4px; height: 4px;
    margin-left: -2px;
    border-radius: 50%;
    background: var(--lav-600);
    animation: mheroCue 2.6s var(--ease-soft, ease-in-out) infinite;
  }
  /* The dot travels the rail's length less its own diameter, so it fades out
     ON the line rather than past the end of it. Opacity holds flat across the
     middle: a dot that fades the whole way reads as a pulse, and the thing
     being communicated is travel. */
  @keyframes mheroCue {
    0%   { transform: translateY(2px);  opacity: 0; }
    26%  { opacity: .95; }
    74%  { opacity: .95; }
    100% { transform: translateY(32px); opacity: 0; }
  }
  .mhero.is-idle .mhero__cuedot { animation-play-state: paused; }

  /* --- reduced motion ----------------------------------------------------
     The ambient drift and the travelling dot are precisely what this
     preference is asking not to see. Both become still — the field keeps its
     composition and the cue keeps its line with the dot resting on it, so
     nothing disappears and nothing moves. */
  @media (prefers-reduced-motion: reduce) {
    .mhero__blob { animation: none; }
    .mhero__cuedot {
      animation: none;
      top: 17px;                 /* the rail's midpoint, at rest */
      opacity: .95;
    }
  }

  /* §12, the app phone screen, is NOT in this block — it needs to cover the
     769–1023 band as well (an iPhone in landscape is ~844px wide and is the
     same device with the same WebKit), so it lives in its own max-width: 1023px
     block at the foot of this file. */
}

/* ==========================================================================
   12. THE APP PHONE SCREEN — the still is the floor, the video is the bonus
                                                    ≤ 1023px      iOS SAFARI

     THE GREEN RECTANGLE. iOS Safari gives a <video> its own compositing layer
     and paints that layer's buffer. Before any frame has been decoded into it
     the buffer is all zeros, and all-zero YUV converts to RGB(0,135,0) — the
     flat green box in the bug report, matched to the pixel. It is not a colour
     any stylesheet here declares, which is why searching for one found nothing.

     A `poster` DOES NOT COVER IT, AND THAT IS MEASURED, NOT ASSUMED. The first
     attempt at this fix shipped a poster to production; the poster was served
     (HTTP 200, right bytes) and the attribute was on the element, and the green
     box appeared anyway. Once WebKit has allocated the media layer — which
     load() and play() are enough to trigger, even with no source attached — the
     layer wins over the poster. The poster is only ever shown by an element
     that has NOT reached that state, so it cannot be relied on to cover it.

     SO THE VIDEO IS NEVER THE THING HOLDING THE SCREEN. The app screen is a
     real <img> (mobile-boot.js §b injects it), which is an ordinary painted box
     that no compositing surface can replace. The <video> lies fully transparent
     on top of it and is only faded in once it has PROVED it is presenting
     frames — requestVideoFrameCallback where the engine has it, a timeupdate
     past zero where it does not. If that proof never arrives the still simply
     stays, and the reader sees the app home screen rather than a green box.

     ON iOS THERE IS NO <video> HERE AT ALL — mobile-boot.js removes it. The
     gate above is not enough on that engine: the recording plays for about a
     second, so frames really are presented and the gate really does open, and
     the layer is lost afterwards. The rules below therefore describe the
     non-iOS case; on iOS only .phone__still is ever in the box, at full
     opacity, and `.is-live` is never set because nothing can set it.

     1023px AND NOT 768px, DELIBERATELY. An iPhone held in landscape reports a
     ~844px viewport, which is the same handset and the same WebKit but lands in
     a band mobile.js does not run in. Below 1024 the still is the floor
     everywhere. Above it, desktop is untouched — the recording autoplays from
     the markup exactly as it always has and none of this is reachable.

     ONLY THE APP PHONE, not `.phone--asset` at large. The feature panels use
     the same device frame, and styles.css §15 holds their screenshots at
     `opacity: 0` until their panel becomes active — a still painted on the
     shared class would show the app home screen through every inactive panel. */
@media (max-width: 1023px) {
  .app__phone .phone__screen {
    position: relative;
    background-color: #fff;
  }

  /* The still, geometrically identical to the footage: styles.css §15 gives
     .phone__screen > video `object-fit: cover`, `object-position: center top`
     and `scale(1.04)`, so matching those three makes the crossfade a dissolve
     between two aligned images rather than a jump. */
  .app__phone .phone__still {
    position: absolute;
    inset: 0;
    z-index: 1;
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center top;
    transform: scale(1.04);
    background-color: #fff;
  }

  /* Transparent until proven otherwise. `opacity: 0` still lets the layer exist
     and decode — which is what we want, since we are waiting on it to decode —
     but a fully transparent layer composites to nothing, so the green buffer
     underneath it cannot reach the screen. */
  .app__phone .phone__screen > video {
    position: relative;
    z-index: 2;
    opacity: 0;
    background-color: transparent;
    transition: opacity .5s ease;
  }
  .app__phone .phone__screen.is-live > video { opacity: 1; }

  /* Once the recording is up the still has nothing left to do. It stays in the
     document (the video can stall and we may need it back) but stops painting,
     so the two are never both compositing. */
  .app__phone .phone__screen.is-live .phone__still { opacity: 0; }
  .app__phone .phone__still { transition: opacity .5s ease; }

  @media (prefers-reduced-motion: reduce) {
    .app__phone .phone__screen > video,
    .app__phone .phone__still { transition: none; }
  }

  /* ------------------------------------------------------------------------
     13. THE APP PHONE'S BEZEL, IN PROPORTION                     ≤ 1023px

     THE FEATURE PANELS ARE NOT HERE, AND THAT IS THE POINT. Their bezel was
     the reported bug and it was wrong at every width, so it was corrected at
     source in styles.css §15 (11px → 6px, 40px → 28px) rather than patched for
     phones only. Nothing about it needs restating here.

     THE APP PHONE STILL DOES, BECAUSE ITS BEZEL WAS ONLY WRONG DOWN HERE.
     styles.css §7b gives it its own 9px, sized against the ~250px it renders
     at on desktop — 3.1%, which is right. But §7b also derives its width from
     viewport HEIGHT, and on a phone that lands at ~185px, where the same 9px
     is 4.88%. The number was correct for the size it was written for and the
     size is what changed.

     6px is ~3.25% at 185px, which matches both the corrected panels beside it
     and its own desktop proportion. The screen's radius follows the outer one
     down by the padding (28 − 6 = 22), keeping the curves concentric. */
  .app__phone .phone {
    padding: 6px;
    border-radius: 28px;
  }
  .app__phone .phone__screen {
    border-radius: 22px;
  }
}

/* ==========================================================================
   7. THE HERO, REBUILT AS ONE LAYER                          769 – 1023px
   --------------------------------------------------------------------------
   THIS IS THE ONE BLOCK THAT REPLACES A LAYOUT RATHER THAN CORRECTING ONE. It
   sits directly under the 1024px desktop floor because the treatment it
   replaces is the desktop hero, and there is no width below 1024 where that
   hero is wanted. (§12 above shares the 1023px ceiling but not the 769px floor:
   it governs every width below the desktop one.)

   IT NOW CARRIES A 769px FLOOR AS WELL, AND THAT CHANGED NOTHING. Every rule
   below describes .hero and its children; §11 takes .hero out of the layout
   entirely below 768px and puts .mhero there instead, so at those widths this
   block was styling a `display: none` subtree. The floor states the band it
   actually governs rather than leaving a reader to work out that half of its
   range is dead. Behaviour between 769 and 1023 is byte-for-byte what it was.

   WHAT IT REPLACES. styles.css §hero at ≤760px turns the hero into two
   stacked bands: `.hero` goes `display: block`, `.hero__stage` drops out of
   its absolute position into a `min(52svh, 420px)` block of its own, and the
   copy follows underneath on flat paper. The footage therefore reads as a
   photograph bolted to the top of the page rather than as the stage the
   section is built around — a ~420px slab of face above the headline, with
   the headline nowhere near it.

   WHAT IT DOES INSTEAD. The video goes back to what it is on desktop: an
   absolutely positioned full-bleed layer behind everything, `cover` rather
   than `contain` so it fills a portrait viewport without letterboxing. The
   copy, the trust pill and the rail sit on top of it, bottom-anchored, and a
   gradient between the two carries the page's own paper up from the bottom
   edge so the type is read against paper while the face is still visible
   above it.

   THE FOOTAGE IS STILL LIVE AND STILL THE HERO IN THIS BAND. mobile-boot.js
   and mobile.js are both gated at 768px, so between 769 and 1023 neither runs
   and the video autoplays natively from the markup exactly as on desktop.
   Nothing here swaps it for a still. What it is not, at this width, is the
   subject of the section — it is the ground the section stands on, which is
   why it is taken down to 40%.

   THE THREE LAYERS, EXPLICITLY:
     z 0   .hero__stage — the footage
     z 1   .hero::after — the gradient that turns footage into background
     z 2   .trust, .hero__inner — everything the reader actually reads
   ========================================================================== */
@media (min-width: 769px) and (max-width: 1023px) {

  /* Bottom-anchored rather than centred: the copy wants to sit against the
     paper end of the gradient, and the face wants the clean top half.
     `min-height` and not `height` — the rail is three items tall on a narrow
     phone and the section has to be free to grow past a screen rather than
     crop it.

     svh, NOT dvh. dvh remeasures as the browser toolbars retract, which on a
     bottom-anchored layout means the copy slides while the reader is reading
     it. svh is the toolbars-visible height: the section is laid out once, for
     the smallest the window ever gets, and never moves. */
  .hero {
    position: relative;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    min-height: 100svh;
    padding-top: var(--nav-h);
    overflow: hidden;
    /* The studio backdrop the footage was shot against. It shows through at
       60% everywhere the video is, so it has to be the same off-white the
       desktop hero uses or the blend goes grey. */
    background: linear-gradient(to right, #fafafa 0%, #f7f7f7 50%, #f5f5f5 100%);
  }

  /* Back out of the ≤760px band and into a bleed layer pinned to the top.
     `top: 0` overrides both the block-flow position the ≤760 rule gives it and
     the `top: calc(var(--nav-h) + …)` the base rule gives it — the footage now
     runs behind the nav, which is what makes it read as background.

     THE HEIGHT IS WHAT CONTROLS THE ZOOM, AND IT IS THE WHOLE REASON THIS IS
     NOT `inset: 0`. `cover` scales to fill whichever axis needs more, so on a
     390×844 portrait viewport a 16:9 clip is scaled by 844/1080 ≈ 0.78 and
     then cropped to 21% of its own width. That is not a background, it is a
     close-up of two eyes: the head alone filled the full 390.

     Sizing the layer at ~58svh instead scales by ~0.44, which shows a bit
     under half the frame and brings the head down to ~56% of the width —
     head and shoulders, sitting in the space above the copy. The cap stops
     tall devices from walking the zoom back up again. */
  .hero__stage {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: auto;
    width: auto;
    height: min(58svh, 480px);
    z-index: 0;
  }

  /* `cover` because a portrait viewport would letterbox `contain` into a
     band, which is the thing being removed. 22% keeps the face clear of the
     nav without cropping the top of her head. */
  .hero__video {
    width: 100%;
    height: 100%;
    object-fit: cover;
    object-position: center 22%;
    opacity: .4;
    /* The layer now ENDS somewhere, and an edge is exactly what a background
       must not have. Feathered to nothing over its last 45% so the footage
       dissolves into the page instead of stopping on a line — the same
       technique the desktop hero uses to stay overlay-free. */
    -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
            mask-image: linear-gradient(180deg, #000 0%, #000 55%, transparent 100%);
  }

  /* The reticles are anchored to the centre line and sized for a frame that
     is showing the whole subject. Over a cropped, 40% video they are noise
     drawn on top of noise. Already gone below 760; gone across the band. */
  .scan { display: none; }
  .scrollcue { display: none; }

  /* Footage → background. Transparent across the top so the face is untouched,
     reaching full paper by the time it is behind the headline. The stops are
     doing legibility work, not decoration: by 62% the type is on flat colour,
     so contrast under the headline and the buttons does not depend on which
     frame of the video happens to be showing. */
  .hero::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
    pointer-events: none;
    background: linear-gradient(
      180deg,
      rgba(251, 249, 245, 0)    0%,
      rgba(251, 249, 245, .35) 34%,
      rgba(251, 249, 245, .88) 62%,
      rgb(251, 249, 245)       84%
    );
  }

  /* --- the foreground ---------------------------------------------------
     Both of these are already in flow at ≤760; what they need here is to be
     above the gradient. `position: relative` is what makes z-index apply —
     on a static element it is ignored. */
  .trust {
    position: relative;
    inset: auto;
    z-index: 2;
    align-self: flex-start;      /* or the flex column stretches the pill */
    margin: 0 var(--pad) 14px;
  }

  .hero__inner {
    position: relative;
    z-index: 2;
    padding-top: 0;
    padding-bottom: clamp(36px, 6vh, 64px);
    gap: 26px;
  }
}
