/* ═══════════════════════════════════════════════════════════
   landing.css — everything the landing page draws that is NOT
   site chrome.

   What is NOT in here, and where it went:
     the button system  ->  public/css/site-nav.css
     the navbar + sheet ->  public/css/site-nav.css
     the footer         ->  public/css/site-footer.css

   Those three are shared templates now, so a change to any of
   them lands on every page that opts in rather than on this one.
   Everything below is this page and nothing else.
   ═══════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════
   MyPeptideApp · Landing
   No build step and no dependencies. Three things load beside
   this file: Google Fonts, public/css/site-nav.css and
   public/css/site-footer.css. The device shot used to be one
   inline base64 WebP; it is three real files in public/img/landing/
   now, one per feature panel, pointed at by --shot-a/b/c below.

   ─── WHERE THINGS ARE ───
   The stylesheet is NOT in document order. It runs:
     tokens · buttons · navbar · SCENE · MORE · FAQ ·
     FINAL CTA · WHY IT IS FREE · PROOF · placeholder
   the page runs:
     SCENE · MORE · WHY IT IS FREE · PROOF · FAQ · FINAL CTA
   Every block is self contained and prefixed, so nothing here
   depends on that order. It is only a thing to know before you go
   looking for a rule.

   ─── THE PINNED SCENE ───
   The hero and the first feature block are not two sections you
   scroll between. They are one pinned SCENE:

     scroll 0.00 → the hero, phone centred, cropped by the fold so
                   the whole hero is exactly one screen
     scroll rise → the copy fades, the phone travels right (desktop)
                   or lifts to the top (mobile) and settles
     then       → the phone is parked. Each further screen of scroll
                   swaps the copy on the free side and cross fades
                   the phone's screenshot to the matching tab

   Only transform and opacity are ever animated, and only ONE element
   (.device) is written to per frame. The panels and the screenshots
   switch on class changes so CSS owns their timing, not the scroll
   position: text that jitters with the scrollwheel is unreadable.

   The pinning is progressive enhancement. Without JS, or with
   prefers-reduced-motion, .scene never gets .is-live and the whole
   thing collapses into a plain stacked layout that still reads well.

   Type: THREE families, and no more than three. Inter for site
   chrome (navbar / buttons / micro labels), Instrument Sans for
   everything that is content, JetBrains Mono for the receipt.
   Earlier versions carried Instrument Serif for an italic headline
   accent; the accent is gone, so the family went with it rather
   than staying in the font request unpainted.

   Colour: app tokens (black / white / grey) plus the blog's
   pastel accents for atmosphere only. Every accent FILL on the
   page is a token in :root, including the ones that are only ever
   reached through an inline --c. The proof cards' --sh values stay
   as literal rgba, because a shadow is the fill at 42% alpha and
   there is no way to derive that from a hex token without
   color-mix, which is younger than the browsers this has to run on.
   ═══════════════════════════════════════════════════════════ */

/* ─── TOKENS ─────────────────────────────────────────────── */
:root{
  /* surface + ink: identical to the app's variables.css */
  --bg:#FFFFFF;
  --ink:#111827;
  --ink-2:#6B7280;
  --ink-3:#9CA3AF;
  --line:#E5E7EB;
  --line-soft:#F1F2F4;

  /* ─── the accents ───
     The five colours the compound chips, the vials and the reason
     dots run, in the order they read as a set. --lav and --peach
     come from the blog's content.css, the other three are this
     page's own. They live here rather than as hexes in the markup
     for one reason: they are set on elements through an inline
     --c, which looks like a local value but is not. Moving the
     lavender used to mean grepping fifteen style attributes.

     --lav-ink is the only one with a paired darker step, because
     it is the only one that is ever used as link text. The rest
     are decoration and never carry type. */
  --lav:#6C5CE0;
  --lav-ink:#4A3FB0;
  --violet:#B45CF0;
  --magenta:#F368D8;
  --peach:#D9822B;
  --mint:#0E9F6E;

  /* the proof cards' pair. As published, --mint and --peach both sit
     near 3:1 on white, which is fine behind a 5px chip in the orbit
     and not fine behind an 11px white label on a filled card. These
     are those two darkened until they clear it. The bright ones stay
     in the orbit, these two stay on the cards. */
  --mint-deep:#0C7A56;
  --peach-deep:#A85F14;

  /* type */
  --font-ui:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
  --font-sans:'Instrument Sans',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;
  --font-mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;

  /* rhythm */
  --pad:clamp(20px,5vw,40px);
  --maxw:1200px;
  /* --nav-h and --nav-shift are declared by
     public/css/site-nav.css. The navbar owns its own geometry;
     everything below just reads it. */

  --r-sm:8px;
  --r-lg:16px;
  --r-pill:9999px;
  --r-btn:12px;          /* soft square, the hero/CTA button shape */

  --ease:cubic-bezier(.16,1,.3,1);

  /* ─── THE DEVICE SCREENSHOTS ────────────────────────
     One token per feature panel, in panel order. The number in the
     filename is the panel, the word is what is on the screen:
       --shot-a   panel 1 "Set the protocol once"     shot-1-home
       --shot-b   panel 2 "Log the dose, see the level"  shot-2-levels
       --shot-c   panel 3 "Know whether it is working"   shot-3-metrics

     All three are exports of the SAME mockup: identical hand,
     identical phone, only the screen differs. That is load bearing,
     not a nicety. The silhouettes match pixel for pixel, so the
     stack below reads as the screen changing rather than as two
     photographs dissolving into each other.

     GEOMETRY, and the one rule for replacing these.
     Every file here is 786 x 1059 with the artwork bleeding to all
     four edges and NO transparent margin. .device is sized from the
     same two numbers (aspect-ratio:786/1059) and .shot paints with
     background-size:100% 100%, so any padding baked into an export
     squashes the phone and slides it off the notch offset that the
     pool, the orbit and transform-origin are all solved from.
     If an export arrives with a transparent border, crop it to the
     artwork before it ships. Do not compensate in CSS.

     Replacing one is: crop to 786 x 1059, save under a NEW filename,
     point the token at it, update the preload in
     templates/layouts/landing.php if it is --shot-a, bump
     ASSET_VERSION in config/app.php. A new name rather than the same
     one because .htaccess serves image/webp with a one month Expires
     and these URLs carry no ?v=, so an in place swap keeps showing
     the old picture to everyone who has already been here.

     No ?v= on these URLs: a stylesheet cannot read ASSET_VERSION,
     and this file is already versioned, so a new image ships behind
     a new stylesheet URL anyway. The first shot is preloaded in
     templates/layouts/landing.php so the hero does not wait for
     this file to be parsed before the request goes out. */
  --shot-a:url("/public/img/landing/shot-1-home.webp");
  --shot-b:url("/public/img/landing/shot-2-levels.webp");
  --shot-c:url("/public/img/landing/shot-3-metrics.webp");
}

/* typed so the browser can keep --hand on the compositor path and
   so calc() on it is always valid. Ignored by older engines, which
   fall back to plain token substitution and still work. */
@property --hand{syntax:'<number>';inherits:true;initial-value:0}

/* typed for a different reason: --dev-gap is the one spacing token the
   JS also has to know about, because the phone's resting position is
   solved in script, not in CSS. An UNregistered custom property comes
   back out of getComputedStyle as the literal text "clamp(26px,5svh,60px)",
   which parseFloat cannot read. Registered as a <length> it is resolved
   to absolute px at computed-value time, so one number in one place
   drives both sides. Engines without @property fall back to the JS
   default in measure(), which mirrors this clamp. */
@property --dev-gap{syntax:'<length>';inherits:true;initial-value:34px}

*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{
  font-size:16px;
  -webkit-text-size-adjust:100%;
  scroll-behavior:smooth;
  scroll-padding-top:calc(var(--nav-h) + 20px);
  -webkit-font-smoothing:antialiased;
  -moz-osx-font-smoothing:grayscale;
  color-scheme:light;
}
body{
  font-family:var(--font-sans);
  color:var(--ink);
  background:var(--bg);
  line-height:1.6;
  /* clip, not hidden. overflow:hidden on body propagates to the
     viewport and in a few engines quietly kills position:sticky for
     everything below it. clip never creates a scroll container. */
  overflow-x:hidden;
  overflow-x:clip;
}
a{color:inherit;text-decoration:none}
img,svg{display:block;max-width:100%}
/* the shared vial sprite. NOT display:none, which is the usual way
   to hide a sprite and the usual way to break it: see the note on
   the block itself, first thing in the document. */
.svg-defs{position:absolute;width:0;height:0;overflow:hidden}
button{font:inherit;color:inherit;border:0;background:none;cursor:pointer}
ul{list-style:none}
::selection{background:var(--ink);color:#fff}
:focus-visible{outline:2px solid var(--lav);outline-offset:3px;border-radius:6px}

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

/* .lnk — EVERY LINK THAT SITS INSIDE A PARAGRAPH, and only those.
   `a{color:inherit;text-decoration:none}` above strips the browser
   default site-wide. That is right for the twenty-odd anchors on this
   page that are buttons, nav items and cards, and wrong for the handful
   that are genuinely links in prose: without a rule they are invisible,
   which is a real accessibility failure and not a style preference.

   ONE rule, in the base layer, for the same reason the .em note above
   gives: the alternative is four near-identical blocks under .faq-a,
   .more-item, .why-reason and whatever comes next, and they drift.

   .faq__mail is folded in here rather than kept as its own copy of the
   same four declarations. It shipped first and this used to duplicate
   it; they are one selector now so the treatment cannot drift between
   the FAQ aside and the body copy.

   NOT BOLD, AND NO COLOUR OF ITS OWN. It was font-weight:600 and
   color:var(--ink), which on a 15px --ink-2 paragraph is two signals
   stacked: heavier AND darker than everything around it. The eye read
   the link before the sentence it sits in, which is backwards in the
   More grid and in "Why it is free", where two or three of them land
   in the same eyeful.

   What is left is ONE cue: the underline. The type itself is now
   identical to the paragraph it is set in, weight and colour both,
   which is the whole point. An underline on its own is the oldest and
   the most reliable link affordance there is, and it is the one that
   does not depend on colour vision, so dropping the other two is safe
   in a way that dropping the underline would not have been.

   The underline is --ink-3 rather than the old --line. --line is
   #E5E7EB, about 1.2:1 on white, which is a hairline you can only see
   if you already know it is there. That was fine as a THIRD cue under
   bold dark text and is nowhere near enough to be the only one.
   --ink-3 is #9CA3AF: still a step lighter than the --ink-2 body copy
   above it, so it reads as an underline and not as a border, but it
   is plainly visible. Inset shadow rather than text-decoration, so it
   sits a pixel lower and does not collide with descenders. Lavender
   on hover, like every other interactive accent on the page: that is
   now the only moment the link changes colour at all. */
.lnk,.faq__mail{
  color:inherit;
  box-shadow:inset 0 -1px 0 var(--ink-3);
  transition:box-shadow .2s var(--ease),color .2s var(--ease);
}
.lnk:hover,.faq__mail:hover{color:var(--lav-ink);box-shadow:inset 0 -1px 0 var(--lav)}
@media (prefers-reduced-motion:reduce){ .lnk,.faq__mail{transition:none} }

/* .em CARRIES NO STYLE, AND THAT IS THE CURRENT STATE, NOT AN
   OVERSIGHT. Every section headline wraps its payoff clause in
   <span class="em">, which used to set Instrument Serif italic.
   The accent was dropped; the spans stayed, because they mark
   where the emphasis IS and re-deriving that from the copy later
   is guesswork.

   What did NOT stay is what replaced it: six rules, one per
   section, each setting font-family:inherit, font-style:normal,
   font-weight:inherit, letter-spacing:inherit and padding-right:0.
   That is exactly what a bare span in those headings already
   computes to, so all six were no-ops overriding a rule that no
   longer existed.

   To bring an accent back, write ONE rule here. Do not write it
   per section again. */

/* ═══════════════════════════════════════════════════════════
   SCENE  ·  hero + product tour, one pinned stage
   ───────────────────────────────────────────────────────────
   .scene       the scroll track. Its height is what buys the
                pinned time, so it is the only knob for pacing.
   .scene__pin  the thing you actually look at: exactly one
                small viewport tall, sticky at the top, clipping
                whatever hangs below the fold.

   svh, not vh or dvh. dvh resizes when the mobile URL bar
   collapses, which would resize the pinned stage mid scroll and
   make the phone jump. svh is the toolbar-visible height, so it
   never changes and never lies about what fits.
   ═══════════════════════════════════════════════════════════ */
.scene{
  --steps:3;         /* number of feature panels */
  --handoff:.62;     /* scroll length of the hero → feature move, in screens */
  --dwell:.88;       /* scroll length each feature holds, in screens */

  /* ─── THE HERO'S VERTICAL RHYTHM, all four knobs in one place ───
     Read top to bottom, these ARE the hero: navbar, air, headline,
     air, subline, air, buttons, air, phone. Nothing else sets
     vertical space in here.

     Every one of them scales with viewport HEIGHT rather than width,
     and that is the whole trick. The hero is a fixed budget: exactly
     one screen, with the phone hanging off the bottom edge and the
     fold doing the cropping. Anything these four spend is paid for in
     phone. svh means a 1080p desktop, which has the room, gets the
     full generous amount, while a 13" laptop quietly hands some back
     instead of cropping the phone down to a sliver. A vw based value
     would do the exact opposite on a wide short window.

     The lower bound of each is the point below which the stack stops
     reading as a hierarchy and starts reading as a mistake. The upper
     bound is where more air stops being generous and starts being a
     gap. */
  --head-top:clamp(24px,7svh,84px);    /* navbar bottom → headline */
  --sub-gap:clamp(14px,2.1svh,24px);   /* headline → subline       */
  --act-gap:clamp(24px,3.8svh,42px);   /* subline → buttons        */
  /* buttons → phone. Bigger than the other three on purpose: this is the
     one gap in the hero that separates two DIFFERENT kinds of thing, copy
     and artwork, rather than two steps of the same block. The phone is
     cropped by the fold anyway, so paying for the air out of the bottom of
     the shot costs nothing that was being read. */
  --dev-gap:clamp(30px,6svh,62px);

  /* ─── the glow pool behind the phone ───
     Its box is expressed in PHONE WIDTHS, not in viewport units, because
     it is the phone's own light. Mobile needs its own numbers and not a
     scaled version of the desktop ones: there --dev-w is 90vw, so the
     desktop 1.55 would make the pool 140vw and its radius alone would
     cover 70% of the screen. The glow then stops being a halo and
     becomes a coloured screen the phone happens to be on top of. */
  --pool-w:1.25;   /* box width, in phone widths  */
  --pool-h:1.2;    /* box height, in phone widths */
  --pool-o:.55;    /* master dimmer               */

  /* The phone's width lives up here rather than on .device because it is
     no longer only the phone's business: the glow pool behind it is sized
     and centred from the same number, and a pool that is solved from a
     stale copy of the width is a pool that sits next to the phone instead
     of behind it. One declaration, two elements. */
  --dev-w:min(90vw,62svh);
  /* where the phone's top edge sits in the hero. Only a sane starting
     value: JS overwrites it with the real one, measured from the bottom
     of the headline block, so the copy can never be overlapped no matter
     how long it gets or how short the viewport is. */
  --dev-top:50svh;
  position:relative;
  isolation:isolate;
}
/* Desktop can afford a bigger drop to the phone, and needs it: the copy
   is centred and the phone is centred under it, so a tight gap makes the
   two read as one column of stuff rather than as a headline and a product
   shot. Mobile keeps the smaller value, where the phone is already the
   thing under most pressure from the fold. */
@media (min-width:1000px){
  .scene{
    --dev-gap:clamp(32px,6.8svh,82px);
    --dev-w:min(clamp(360px,33vw,500px),57svh);
    --pool-w:1.55;--pool-h:1.32;--pool-o:1;
  }
}
.scene__pin{position:relative;overflow:hidden}

/* the enhanced state, switched on by JS once it has measured */
.scene.is-live{
  height:calc((1 + var(--handoff) + var(--steps) * var(--dwell)) * 100vh);
  height:calc((1 + var(--handoff) + var(--steps) * var(--dwell)) * 100svh);
}
.scene.is-live .scene__pin{
  position:sticky;top:0;
  height:100vh;height:100svh;
}

/* the nav's "Features" link and the hero's secondary button aim here,
   not at the top of the scene, so they land on the first panel instead
   of on the hero you are already looking at. */
.scene__anchor{position:absolute;left:0;width:1px;height:1px;top:0}
.scene.is-live .scene__anchor{
  top:calc(var(--handoff) * 100svh);
  /* NEGATIVE, and it has to be. html carries a scroll-padding-top of
     nav plus 20px so that a heading jumped to does not land underneath
     the navbar, which is right for every other anchor on the page and
     wrong for this one: this is not a heading, it is a scroll position.
     Every pixel of that padding is a pixel of the handoff that does not
     get scrolled, and the handoff is only 0.62 of a screen, so on a
     laptop 92px is nearly a quarter of it. The hero is faded by
     (1 - hand) and reaches zero only at the end of the move, so what
     you get is the headline still sitting there at about a tenth
     opacity behind the first panel.

     Cancelling the padding with an equal negative margin puts the
     landing exactly on the end of the handoff. Knife edge in theory;
     in practice easeInOut is flat enough there that being a pixel or
     two short leaves the hero at an opacity with seven zeros after the
     decimal point. Keep the two expressions identical. */
  scroll-margin-top:calc((var(--nav-h) + 20px) * -1);
}

/* ─── atmosphere ───
   Lives inside the pin, so it holds still while the track scrolls
   underneath. Calms down as the tour starts: reading grey body copy
   over a moving lavender wash is a fight nobody wins. */
.scene__bg{position:absolute;inset:0;z-index:0;pointer-events:none;overflow:hidden}

/* ─── the top band ───
   Two accents at the very top and nothing else. What used to be here was
   a 78% x 58% plate of pale lilac at .72 sitting over the whole upper
   hero, plus a second lavender blob parked at dead centre. Between them
   the headline, the subline and the buttons all sat on tint, so the page
   had no white in it and every accent read as the same wash. Colour that
   is everywhere is not an accent, it is a background.

   Now the colour is spent in two places only: here, above the copy, and
   in the pool behind the phone below. Everything between the two is
   paper. That contrast is what makes the accents read as light.

   inset:0, not the old -12% box. In that box a gradient written
   "at 79% 2%" actually landed at 86% and ten percent ABOVE the fold, so
   no number in this rule meant what it said and tuning it was guesswork.
   The drift survives the honest box: scale(1.05) grows the layer 2.5%
   per side while the translate only moves it 1.6%, so an edge can never
   travel into view. */
.scene__wash{
  position:absolute;inset:0;
  background:
    /* RIGHT SHOULDER, lavender, the brand accent and the heavier of the
       two. Dies out at ~14% of the viewport, the headline starts at ~15%. */
    radial-gradient(40% 27% at 62% -6%, rgba(108,92,224,.35), transparent 76%),
    /* LEFT SHOULDER, warm, mirrored and deliberately smaller and weaker.
       Its job is to stop the band resolving into one flat sheet of purple:
       two hues meeting inside one shape read as light, one hue reads as a
       fill.

       38 and 62, not 33 and 67. The old pair still measured a dip in the
       middle: sampling the band's density across the top row at 1440x900
       gave 9-22-32-33-26-31-36-43-42-33-20-6, which rises, falls back to
       26 at dead centre and rises again to 43. That double hump is what
       the eye reads as two lamps rather than one light, and it is why the
       right blob looked like a separate object with its own edge.
       Closing the pair by five points each way and taking the alphas down
       to pay for the extra overlap (.42 -> .35, .30 -> .25) gives
       2-14-25-31-35-40-42-39-33-22-10: one hump, no dip, peak 42 against
       the old 43, so the band carries the same weight of colour and only
       the shape changed. The hue still runs warm at 38%, pale lilac at
       50%, lavender at 62%; the centre measures rgb(225,215,232), which is
       violet leaning and not grey. */
    radial-gradient(36% 24% at 38% -6%, rgba(217,130,43,.25), transparent 74%),
    /* the seam. Wider than it was and slightly lighter: with the shoulders
       closed up, this is no longer patching a hole in the middle, it is
       the thing that carries the band out past both shoulders so the two
       hues sit inside ONE envelope instead of each having its own edge. */
    radial-gradient(76% 22% at 50% -10%, rgba(233,229,251,.48), transparent 80%);
  animation:drift 26s ease-in-out infinite alternate;
  opacity:calc(1 - var(--hand) * .4);
}

/* ─── the top band, PORTRAIT ───
   Aspect ratio, not width, and that is the whole point of this block.
   Every number in the rule above is a percentage, so the band is
   identical in RELATIVE terms at any viewport: same position, same
   reach, same alpha. What is not identical is the SHAPE, because the
   two axes are measured against different sides. At 1440x900 the
   lavender resolves to 576x243, an ellipse nearly two and a half times
   wider than it is tall, which is what makes it read as light coming in
   from above the fold. At 393x852 the same 40% 27% resolves to 157x230,
   which is TALLER than it is wide: the light source turns into a round
   blob sitting in the top right corner with a visible edge on it, and
   the warm one turns into a second blob beside it. Nothing is wrong
   with the values, the box is just the wrong shape for them.

   So portrait gets its own three, solved for the same look rather than
   the same numbers: horizontal radii roughly a third wider, vertical
   radii a third shorter, alphas down to match. On a 393px phone that is
   204x187 for the lavender and a 92% seam under it, and the measured
   density across the top row comes out
   1-5-13-21-27-34-39-41-41-37-30-23-16-9-1: symmetric, single hump,
   both ends arriving at the screen edge near zero, peak 41 against the
   old 43. The old one measured 0-0-9-22-31-33-25-31-36-43-41-33-20-6-0
   on the same phone: dead white for the first 15% of the width, then
   still 20 units of purple at 92%, which is the lopsided right hand
   lobe that looked like it was falling off the screen. Same weight of
   colour, spread across the band instead of pooled in one corner.

   Vertical reach is deliberately unchanged at ~13% of the viewport, so
   the band still stops just under the navbar and the headline still
   starts on clean paper. */
@media (max-aspect-ratio:1/1){
  .scene__wash{
    background:
      radial-gradient(52% 22% at 60% -4%, rgba(108,92,224,.26), transparent 80%),
      radial-gradient(48% 19% at 38% -4%, rgba(217,130,43,.22), transparent 78%),
      radial-gradient(92% 19% at 50% -8%, rgba(233,229,251,.46), transparent 84%);
  }
}
@keyframes drift{
  from{transform:translate3d(0,0,0) scale(1)}
  to{transform:translate3d(-1.6%,1.4%,0) scale(1.05)}
}
/* ─── stop paying for motion nobody is looking at ───
   The wash is `infinite`, so without this it runs for the whole
   session, including the entire rest of the page below the hero.
   It is also the expensive one: .scene__grain sits over it
   with mix-blend-mode:multiply, and a blend group has to be
   recomposited every time anything inside it moves, so a 26s loop
   under a blend layer is a permanent compositor job on a page
   whose hero left the screen four sections ago.

   .is-near is the class the device's will-change already uses, set
   by an IntersectionObserver with a 50% margin either side, so this
   costs nothing new: one selector, no extra observer, and the
   animation resumes in phase because paused is a state, not a
   reset.

   .orb__chip used to be on this rule too, and so, further down the
   page, did .corb__chip. Both idle drifts have since been removed,
   which leaves the wash as the only looping animation on the page
   and this as the only rule that has to pause anything. */
.scene:not(.is-near) .scene__wash{animation-play-state:paused}
/* ─── the pool ───
   The second accent: the light the phone is standing in. Solved from the
   same two numbers the phone is, --dev-w and --dev-top, so it is centred
   on the artwork at every viewport instead of at a percentage that only
   happens to be right on one. The +.09 matches the notch offset the phone
   itself carries, so the glow is centred on the phone, not on the page.

   It is wider than it is tall and it sits low, which is what makes it
   read as light spilling from behind an object rather than as a circle
   someone drew. Where it reaches the button row it is under 6% alpha at
   its strongest and about 2% on average, which measures as rgb(252,252,254):
   enough to seat the CTA in something, not enough to tint it. The headline
   and the subline above are on 255,255,255 with nothing on them at all.

   It fades rather than travels. The phone leaves for the right column on
   scroll and a pool that chased it would be a second animated element
   fighting the first, so this one dissolves over the first two thirds of
   the handoff and the phone carries its own coloured drop shadow from
   there. Gone before the first feature panel is readable. */
.scene__pool{display:none}
.scene.is-live .scene__pool{
  display:block;
  position:absolute;
  left:50%;top:var(--dev-top);
  width:calc(var(--dev-w) * var(--pool-w));
  height:calc(var(--dev-w) * var(--pool-h));
  /* both margins are solved from the box tokens rather than typed in, so
     changing --pool-w can never leave the glow sitting next to the phone
     instead of behind it. The .09 is the notch offset the phone carries. */
  margin-left:calc(var(--dev-w) * (.09 - var(--pool-w) / 2));
  margin-top:calc(var(--dev-w) * (.5 - var(--pool-h) / 2));
  background:
    /* magenta kicker, right of the phone and low. This is the one that
       stops the pool being a flat lilac disc, and it is the same magenta
       that sits at the left end of the CTA button's glow gradient, so the
       two accents in the lower hero are lit by the same source.
       It is also the layer that shows most, because the artwork leaves
       more clear space on its right than on its left, so it is the first
       one to turn down when the pool gets loud. */
    radial-gradient(46% 40% at 69% 60%, rgba(243,104,216,.15), transparent 72%),
    /* a warm trace on the far side, answering the left shoulder above */
    radial-gradient(42% 34% at 25% 62%, rgba(217,130,43,.10), transparent 74%),
    /* The core. NOT closest-side: on a box wider than it is tall,
       closest-side resolves to a CIRCLE of the half height, so the glow
       ends well inside the box and every bit of it lands behind the
       artwork, which is opaque. An explicit 50% 50% fills the box as the
       ellipse this is supposed to be.
       The stops matter as much as the size. A normal centre-out ramp puts
       all its density in the middle, which is exactly the part the phone
       covers. Holding .14 out to 40% and .05 out to 72% moves the energy
       into the ring that actually clears the silhouette, so what you see
       is a halo hugging the phone rather than a bright patch behind it. */
    radial-gradient(50% 50% at 50% 50%,
      rgba(108,92,224,.24),
      rgba(108,92,224,.14) 40%,
      rgba(108,92,224,.05) 72%,
      transparent 92%);
  opacity:clamp(0,calc((1 - var(--hand) * 1.55) * var(--pool-o)),1);
}
.scene__grain{
  position:absolute;inset:0;opacity:.22;mix-blend-mode:multiply;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)' opacity='.20'/%3E%3C/svg%3E");
}

/* ═══ THE ORBIT ════════════════════════════════════════════
   Desktop only. Three concentric CIRCLES centred on the hero,
   with the compounds the app tracks threaded onto the innermost
   one like beads on a wire.

   TRUE CIRCLES, and the geometry is built around keeping them
   that way. The left and the right arc of a ring have to read as
   two ends of one circle, which they only do if the curvature is
   honest: any vertical stretch straightens the flanks into near
   parallel lines and the shape stops closing in the eye.

   The thing that makes circles survivable next to a 66px
   headline is not their size, it is where their poles go. The
   centre sits at 50svh and the smallest radius is at least
   54svh, so both poles fall outside the viewport and what is
   left on screen are two symmetric flanks bowing away from the
   copy. Nothing ever curves back over the type. Measured across
   twenty viewports from 1180x760 to 3440x1440, the tightest gap
   between the innermost circle and the copy is 28px, and it is
   60 to 100px at the sizes people actually use.

   That radius is tied to the viewport HEIGHT, so it needs a
   landscape window to fit across the width. Below 23/20 it would
   be wider than the viewport, so the whole ornament stands down
   rather than degrading.

   ONE COORDINATE SYSTEM. Every ring and every chip is solved
   from the same two numbers, so a chip is never near its ring,
   it is ON it:
     --rb  the base radius, the innermost circle
     --cy  the shared centre
   A chip on ring --k at angle t is simply
     x = 50%  + rb * k * cos t
     y = --cy + rb * k * sin t
   with cos t and sin t written into the markup, because CSS has
   no trigonometry that is safe to rely on yet.

   All six chips ride the innermost circle, in near mirrored
   pairs. That is deliberate: two chips at the same height and
   the same distance from the centre are two points on one
   chord, so the chips themselves spell out the circle the
   hairlines only suggest. The pairs are offset by a few degrees
   so the arrangement reads as placed rather than plotted.

   The angles are not eyeballed. They were solved against the
   measured headline box, the device box and the fold across all
   twenty viewports. The two outer circles carry no chips for a
   simple reason: at any width they are already past the edge of
   the screen at their equator, which is exactly what gives them
   their sweep.

   Motion: none of its own. The chips carried a 3px idle drift
   until it was removed; the layer still fades out as the tour
   takes over, but that fade is written from JS instead of from a
   calc on --hand, and the
   subtree shadows --hand to opt out of inheriting it at all.
   Reason, measured rather than guessed: --hand is an inherited
   custom property, so every change to it on .scene invalidates
   the style of everything below that inherits it. With the orbit
   in that path a frame costs 12.7ms instead of 3.3, almost all
   of it re-resolving the chips' position calc chains. Cutting
   the subtree out of the invalidation puts it back to 2.9.
   ═══════════════════════════════════════════════════════════ */
.orbit{
  /* min() caps the circle by width so a tall window cannot push its
     equator off screen; max() keeps both poles cropped. */
  --rb:min(max(54svh, 30vw), 42vw);
  --cy:50svh;
  --ring:rgba(72,58,180,.11);
  --ring-lit:rgba(108,92,224,.42);
  --hand:0;                          /* the shadow. See the note above. */
  position:absolute;inset:0;z-index:1;
  display:none;                      /* desktop only, switched on below */
  pointer-events:none;
}
/* rasterised once, then only recomposited while the hero fades. Without
   this the fade would repaint six shadowed chips on every frame. */
.scene.is-live.is-near .orbit{will-change:opacity}


.rings{
  position:absolute;inset:0;
  /* fades the hairlines out under the navbar and again at the fold,
     so they never end on a hard cut. no-repeat matters: a mask image
     tiles by default and would stamp the fade band down the page. */
  -webkit-mask-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.25) 7%,#000 17%,#000 80%,transparent 100%);
          mask-image:linear-gradient(180deg,transparent 0,rgba(0,0,0,.25) 7%,#000 17%,#000 80%,transparent 100%);
  -webkit-mask-repeat:no-repeat;
          mask-repeat:no-repeat;
}
.ring{
  position:absolute;left:50%;top:var(--cy);
  width:calc(var(--rb) * var(--k) * 2);
  height:calc(var(--rb) * var(--k) * 2);
  transform:translate(-50%,-50%);
  border:1px solid var(--ring);
  border-radius:50%;
}
/* the two lit segments. A conic gradient in the mask cuts an arc out
   of the ring with feathered ends, which a border-colour trick cannot
   do: a border side is always a hard mitred quadrant. If an engine
   drops the mask the ring simply stays whole in a lighter lavender,
   which is a degradation nobody would notice. */
.ring--lit{border-color:var(--ring-lit)}
.ring--lit-r{
  -webkit-mask-image:conic-gradient(from 0deg,transparent 46deg,#000 58deg,#000 94deg,transparent 106deg);
          mask-image:conic-gradient(from 0deg,transparent 46deg,#000 58deg,#000 94deg,transparent 106deg);
}
.ring--lit-l{
  -webkit-mask-image:conic-gradient(from 0deg,transparent 232deg,#000 244deg,#000 284deg,transparent 296deg);
          mask-image:conic-gradient(from 0deg,transparent 232deg,#000 244deg,#000 284deg,transparent 296deg);
}

/* ─── the compound chips ───
   Three elements, one job each, which is what keeps the reveal and
   the position from fighting over `transform`:
     .orb        solves the point on the ring
     .orb__rv    the shared load reveal (.rv, cleaned up by the JS)
     .orb__chip  the surface

   NO IDLE DRIFT. These used to run `orbDrift` on an infinite
   alternating loop, a 3px vertical float on a per-chip period
   (--t, 9s to 14s, staggered so the six never moved together).
   Removed on request: the chips read as calmer static objects, and
   the hero stops having a permanently running compositor job over
   the LCP element.

   That means three things elsewhere are now dead and are gone with
   it: the `.scene:not(.is-near)` pause rule, the reduced-motion
   `animation:none` override, and the inline `--t` on all six chips
   in templates/pages/home-guest.php. The `--c` on those chips is
   NOT dead, it still colours the vial icon.

   @keyframes orbDrift is gone with it. The closing card's chips
   (.corb__chip) were the last thing still using it and have since
   lost the drift too, so there is no float animation left on this
   page at all.                                                   */
.orb{
  position:absolute;
  left:calc(50% + var(--rb) * var(--k) * var(--cos));
  top:calc(var(--cy) + var(--rb) * var(--k) * var(--sin));
  transform:translate(-50%,-50%);
  display:none;                      /* needs a wider viewport than the rings */
}
.orb__chip{
  display:flex;align-items:center;gap:7px;
  padding:7px 13px 7px 9px;
  border-radius:var(--r-pill);
  border:1px solid var(--line);
  background:#fff;                   /* solid: see the note by .btn--ghost */
  /* tinted with the same indigo as the phone's drop shadow, so the two
     nearest floating objects on the page cast the same light */
  box-shadow:0 12px 28px -16px rgba(46,32,110,.38),0 2px 6px -3px rgba(17,24,39,.10);
  font-family:var(--font-ui);
  font-size:.78125rem;font-weight:600;letter-spacing:-.005em;line-height:1.35;
  color:var(--ink-2);
  white-space:nowrap;
}
.orb__vial{width:13px;height:16.7px;flex:none;color:var(--c)}

/* 23/20 is the point below which a height driven radius stops fitting
   across the width. Portrait-ish desktop windows get no ornament. */
@media (min-width:1000px) and (min-aspect-ratio:23/20){ .scene.is-live .orbit{display:block} }
@media (min-width:1180px) and (min-aspect-ratio:23/20){ .scene.is-live .orb{display:block} }

/* ─── hero copy ─── */
/* no flex gap on purpose. The two steps in this stack are not the same
   size and should not be: the subline is a continuation of the headline,
   the buttons are a new beat. One shared gap value flattens that into a
   list of three unrelated things. */
.scene__head{
  position:relative;z-index:3;
  text-align:center;
  padding:clamp(120px,11vw,168px) var(--pad) 0;
  display:flex;flex-direction:column;align-items:center;
}
.scene.is-live .scene__head{
  position:absolute;left:0;right:0;top:0;
  padding-top:calc(var(--nav-h) + var(--head-top));
  /* fades out over the first half of the move, so the phone is
     travelling into space that is already clear */
  opacity:clamp(0,calc((1 - var(--hand)) * 2),1);
  transform:translate3d(0,calc(var(--hand) * -40px),0);
}
/* ONE threshold for "the hero is gone", not two.
   This used to hang off .is-feature, which lands at hand > .55.
   The opacity above is driven by the EASED value, and
   easeInOut(.55) is .636, so (1 - .636) * 2 puts the block at 73%
   opacity at that exact moment: the primary CTA was plainly
   readable and already dead to the touch, for about 190px of
   scroll. .is-hero-gone lands where the fade has actually
   finished (2.7% opacity) and is set in the same branch that
   sets `inert`, so the pointer target, the tab order and the a11y
   tree can never disagree about whether the hero is still there. */
.scene.is-hero-gone .scene__head{pointer-events:none}

.scene h1{
  /* two caps. 9.6vw is the width rule from V3, 7.4svh is the height one:
     on a laptop with 700px of viewport a 90px headline plus a phone
     simply cannot both fit above the fold, and the headline is the thing
     that can afford to give. On a tall monitor it grows back. */
  font-size:min(clamp(2.25rem,9.6vw,5.2rem),7.4svh);
  line-height:1.03;
  letter-spacing:-.038em;
  font-weight:500;
  text-wrap:balance;
  max-width:900px;
}
/* ─── the small phone fix ───
   9.6vw is not a width rule, it is a COLUMN rule wearing a width rule's
   clothes, and the two stop being the same number at 400px.

   The column the headline actually gets is 100vw minus two gutters, and
   --pad is clamp(20px,5vw,40px). Above 400px that middle term is live,
   the gutters are 5vw each, the column is 0.9 * 100vw, and 9.6vw is the
   size at which "A free peptide tracker" fills it: 9.6 * 9.348 = 89.7,
   near enough 90% of the viewport. That is why the line has always
   landed within a pixel or two of the gutter on every phone. It was
   solved to.

   Below 400px the clamp bottoms out, the gutters freeze at 20px each,
   and the column starts shrinking faster than 9.6vw does. Two lines down
   the clamp's own floor of 2.25rem catches the font size and it stops
   shrinking at all, so from there the line only gets longer relative to
   the space. Measured against Instrument Sans at weight 500 with the
   -.038em tracking, "A free peptide tracker" needs 336px and gets:

       393px phone   353px column   fits, 1px to spare
       375px phone   335px column   1px SHORT
       360px phone   320px column   16px short
       320px phone   280px column   56px short

   360px is the reported width of most Samsung Galaxy phones, so this is
   not an edge case. What it looks like there is a three line headline,
   "A free peptide" / "tracker" / "with no paywall.", because the <br>
   is hard and text-wrap:balance can only rebalance the half it is given.
   Third line pushes everything under it down and eats the phone shot.

   So below 400px the size is solved from the column directly instead of
   from the viewport. 9.5 is the set width of the longest line in ems
   (9.348 measured) plus a little, so the line lands with about 5px of
   gutter left instead of 1px. At the 400px boundary this returns 37.9px
   against the old rule's 38.4px, which is the whole size of the change
   on any phone where the old rule already worked. */
@media (max-width:400px){
  .scene h1{font-size:min(calc((100vw - var(--pad) * 2) / 9.5),7.4svh)}
}
.scene__sub{
  margin-top:var(--sub-gap);
  max-width:540px;
  font-size:min(clamp(1.0625rem,1.6vw,1.1875rem),2.4svh);
  line-height:1.55;
  color:var(--ink-2);
  text-wrap:pretty;
}
.scene__actions{margin-top:var(--act-gap);display:flex;gap:12px;flex-wrap:wrap;justify-content:center;isolation:isolate}
/* 520px is not an arbitrary phone width, it is the width at which these
   two buttons stop fitting on one line. Above it the secondary button is
   free: it sits beside the primary and costs no vertical space at all,
   so it stays. Below it the layout stacks and the second button costs a
   whole row of the one screen the hero gets, which is the most expensive
   space on the page, spent on the weaker of two calls to action.
   display:none, not visibility or opacity: it has to leave the tab order
   and the a11y tree too, and #features is still reachable from the menu. */
@media (max-width:520px){
  /* 88% rather than 100%: with only one button left it is a single black
     bar, and a black bar that runs to within a hair of both gutters reads
     as a docked toolbar rather than as a thing you press. The percentage
     keeps the optical inset even on a 360px phone, the cap stops it
     turning into a slab on a 430px one. */
  .scene__actions{flex-direction:column;align-items:stretch;width:88%;max-width:310px;gap:10px}
  .scene__actions .btn{width:100%}
  .scene__actions .btn--ghost{display:none}
}

/* ═══ THE DEVICE ═══════════════════════════════════════════
   One element, two jobs. .device carries the scroll driven
   transform and nothing else. .device__shots carries the
   drop-shadow and the stacked screenshots.

   Splitting them matters: a filter on the element that is being
   scaled forces the browser to re-rasterise and re-blur the
   shadow every frame. With the filter one level down, that layer
   is rasterised once and the parent's transform is a pure GPU
   composite. This is the difference between 60fps and 30 on a
   mid range Android.
   ═══════════════════════════════════════════════════════════ */
.device{
  /* TOP LAYER OF THE SCENE.
     During the handoff the hero copy is still fading out while the phone
     is already travelling through the space it occupies, and on mobile
     the panel copy starts arriving before the phone has parked. Type
     painting across the artwork reads as a bug, type disappearing behind
     it reads as depth, so the device outranks the head, the panels and
     the rail and is always the thing in front.
     pointer-events:none because the device is pure artwork (role=img):
     sitting on top must never cost a tap on the copy or the rail that
     now sit underneath it. */
  position:relative;z-index:6;pointer-events:none;
  width:var(--dev-w);          /* declared on .scene, see the note there */
  aspect-ratio:786 / 1059;
  /* the no-JS / reduced-motion stack. Same token as the pinned version
     so the two states cannot drift apart as the numbers get tuned. */
  margin:var(--dev-gap) auto 0;
  /* the notch sits at 41.03% of the cutout's width (its dark pill runs
     x=262..383 in the 786px asset, centre 322.5). Centring the notch on
     the page therefore needs 50% - 41.03% = 8.97% of the art width. */
  transform:translateX(9%);
}


.scene.is-live .device{
  position:absolute;top:var(--dev-top);
  left:50%;margin:0 0 0 calc(var(--dev-w) * -.41);   /* -50% + 9% notch */
  transform-origin:50% 0;
  transform:translate3d(var(--tx,0px),var(--ty,0px),0) scale(var(--sc,1));
  backface-visibility:hidden;
}
/* promoted only while the scene is actually on screen. A permanent
   will-change on a 500px image is a permanent texture in VRAM. */
.scene.is-near .device{will-change:transform}

.device__shots{
  position:absolute;inset:0;
  /* the pool underneath dissolves on scroll, so this is what keeps the
     phone lit for the rest of the section. Slightly heavier than it was:
     on the old lilac field the artwork had tint to sit against, on paper
     it has to cast its own. The violet is deliberate, a neutral grey
     shadow on a white hero reads as a cutout pasted on. */
  filter:drop-shadow(0 30px 54px rgba(52,34,128,.24))
         drop-shadow(0 8px 18px rgba(17,24,39,.09));
}
.shot{
  position:absolute;inset:0;
  background-size:100% 100%;background-repeat:no-repeat;
  opacity:0;
  transition:opacity .6s var(--ease);
}
/* Stacking, not cross fading. Each shot is painted above the previous
   one and every shot up to the active index stays fully opaque. So a
   switch is always "fade the new one in over a solid one", never "fade
   one out while another fades in", which would dip the combined alpha
   to 75% mid transition and flash the background through the phone. */
.shot--1{z-index:1;opacity:1;background-image:var(--shot-a)}   /* home · the base plate, never fades */
.shot--2{z-index:2;background-image:var(--shot-b)}             /* levels */
.shot--3{z-index:3;background-image:var(--shot-c)}             /* health metrics */
.shot.is-on{opacity:1}

/* ═══ FEATURE PANELS ═══════════════════════════════════════
   Static fallback first: three cards in a row under the hero.
   Everything the pinned version does is layered on top of this
   under .is-live, so a browser that never runs the JS still gets
   a finished section rather than an empty screen.
   ═══════════════════════════════════════════════════════════ */
.panels{
  position:relative;z-index:3;
  display:grid;gap:20px;
  padding:clamp(56px,8vw,96px) var(--pad) clamp(64px,9vw,104px);
  max-width:var(--maxw);margin-inline:auto;
}
@media (min-width:840px){.panels{grid-template-columns:repeat(3,1fr)}}
.panel{
  border:1px solid var(--line);border-radius:var(--r-lg);
  background:#fff;padding:26px 24px 24px;
}

.scene.is-live .panels{
  position:absolute;inset:0;
  display:block;max-width:none;padding:0;
  pointer-events:none;
  /* the content column, rebuilt as two lengths instead of as padding.
     An absolutely positioned child is laid out against its parent's
     PADDING box, and the padding box is the border box minus the
     border, NOT minus the padding. So padding on .panels is invisible
     to .panel and left:0 lands on the viewport edge. Percentages in
     left / width do resolve against that same box, so the column has
     to be expressed as an offset. */
  --col-w:min(var(--maxw),calc(100% - var(--pad) * 2));
  --col-x:calc((100% - var(--col-w)) / 2);
}
.scene.is-live .panel{
  position:absolute;
  /* +5% of the column: hard against the gutter the copy reads as
     orphaned, this pulls it toward the device so the two are a pair */
  left:calc(var(--col-x) + var(--col-w) * .05);
  top:calc(50% + 22px);                          /* optically centred under the navbar */
  width:min(calc(var(--col-w) * .40),460px);
  border:0;background:none;padding:0;
  opacity:0;
  transform:translate3d(0,calc(-50% + 26px),0);
  transition:opacity .5s var(--ease),transform .7s var(--ease);
}
.scene.is-live .panel[data-state="above"]{transform:translate3d(0,calc(-50% - 26px),0)}
.scene.is-live .panel[data-state="on"]{opacity:1;transform:translate3d(0,-50%,0);pointer-events:auto}

/* ─── the boot guard ───
   .is-live does two jobs at once: it moves the panels into the
   overlay AND it hands them a transition. A transition declared in
   the same style change that alters the property does run, so the
   very first flip animates opacity 1 → 0 over half a second, and
   for that half second all three panels sit absolutely positioned
   on the same spot, visible, on top of each other.

   The JS therefore makes the flip inside a frame where this class
   is present, and drops it two frames later once the new state has
   been painted. Nothing animates into its starting position.

   The three classes give this 0,3,1 specificity, which beats every
   .scene.is-live .panel rule in the file including the ones inside
   the mobile media queries, so it does not depend on source order. */
.scene.is-live.is-booting .panel{transition:none}

.panel h2{
  font-size:clamp(1.5rem,3.1vw,2.5rem);
  line-height:1.12;letter-spacing:-.032em;font-weight:500;
  text-wrap:balance;
}
.panel p{
  /* 14px, not 12. There is no third element under this paragraph
     any more, so the gap under the headline is the only spacing
     the block has and it can afford to be the generous one. */
  margin-top:14px;
  font-size:clamp(.9375rem,1.4vw,1.125rem);
  line-height:1.6;color:var(--ink-2);text-wrap:pretty;
  max-width:44ch;
}
/* No keyword pills. Three of them under every panel put nine
   bordered chips inside a section that already carries a pinned
   phone, an orbit and a progress rail, and they were repeating in
   two words what the paragraph above had just said in a sentence.
   A headline and one paragraph is the whole panel now. */

/* ─── progress rail ───
   A pinned section that eats four screens of scroll has to say how
   long it is, or people think the page has frozen. Three ticks, the
   active one filling, each one a jump target.

   FILLING, not switching. This is the one element in the whole scene
   that answers the finger 1:1 at every scroll position, and that is
   its actual job. The phone and the copy deliberately hold still
   while you read them, so without something moving continuously a
   pinned stage reads as a page that has stopped responding. A tick
   that only flips at the boundary tells you where you are; a tick
   that fills tells you the scroll is doing something RIGHT NOW,
   which is the question people are actually asking. */
.rail{display:none}
.scene.is-live .rail{
  display:flex;gap:8px;z-index:4;
  position:absolute;
  pointer-events:auto;
  opacity:0;transition:opacity .45s var(--ease);
}
.scene.is-feature .rail{opacity:1}
.rail button{
  width:38px;height:3px;border-radius:2px;
  background:rgba(17,24,39,.13);
  position:relative;
  -webkit-tap-highlight-color:transparent;
  transition:background .3s;
}
.rail button::before{content:"";position:absolute;inset:-13px -3px}   /* tap target */
/* the fill. scaleX from the left edge rather than a width, so the
   only thing written per frame is a transform on a composited layer
   and nothing in here can ever trigger layout. NOT overflow:hidden
   on the button: that would clip ::before and shrink the 29px tap
   target back down to the 3px bar. It is not needed either, since
   scaling DOWN from inset:0 can never leave the box. */
.rail button::after{
  content:"";position:absolute;inset:0;
  border-radius:inherit;background:var(--ink);
  transform:scaleX(var(--p,0));transform-origin:0 50%;
}
.rail button:hover{background:rgba(17,24,39,.3)}

/* ─── DESKTOP GEOMETRY ─── */
@media (min-width:1000px){
  .scene.is-live .rail{
    --col-w:min(var(--maxw),calc(100% - var(--pad) * 2));
    --col-x:calc((100% - var(--col-w)) / 2);
    left:calc(var(--col-x) + var(--col-w) * .05);   /* same column as .panel */
    bottom:13svh;
  }
}

/* ─── MOBILE GEOMETRY ───
   No two column split to make, so the axis turns 90 degrees: the
   phone lifts into the top band and the copy takes the bottom.
   Same progress, same panels, same code path, only the numbers
   the JS solves for are different. */
@media (max-width:999px){
  /* shorter than the desktop .88. Four and a quarter screens of pinned
     scroll is a long time to hold a thumb down for, and a flick covers
     ground far faster than a wheel does, so the same pacing that reads
     as generous on a trackpad reads as a page that will not end. */
  .scene{--dwell:.76;--frame:112%}

  /* ═══ THE PHONE IS CENTRED ON ITS NOTCH ════════════════════
     The artwork is not symmetrical: the phone inside it sits at
     41.03% of the image width, not at 50%. .scene.is-live .device
     already compensates for that with margin-left:-.41 * --dev-w,
     so the UNSCALED phone is dead centre on the page.

     But the desktop transform-origin is 50% 0, the artwork's own
     centre, and mobile parks the phone at roughly half size. Scaling
     about a point 9% to the RIGHT of the notch drags the notch right
     with it, by 9% of the artwork width times (1 - scale). At the
     parked scale that is a few percent of the screen, which is
     exactly enough for the hero to look centred and the tour panels
     underneath it to look off.

     Scaling about the notch instead fixes the notch in place at any
     scale, so the phone reads as centred in both states. Desktop
     keeps 50% 0, where the phone is deliberately parked off-centre
     in the right column anyway and the origin was tuned by eye.

     public/js/landing.js reads this same .4103 when it solves the
     carousel's --frame. The two have to agree. */
  .scene.is-live .device{transform-origin:41.03% 0}

  .scene.is-live .panels{padding:0}
  .scene.is-live .panel{
    left:0;right:0;width:auto;
    /* Bottom-anchored, so the height is what decides where the copy
       sits: 3svh shorter drops its centre 1.5svh and closes the same
       distance to the rail underneath. The gap is meant to be there,
       the copy just should not be floating in the middle of it. */
    /* 35svh here and .65 for the band in landing.js are one number split
       across two files: together they are 100svh, which is what puts the
       phone's box bottom exactly on this block's top edge. Change one and
       change the other. */
    top:auto;bottom:0;height:35svh;
    padding:0 var(--pad) 28px;
    display:flex;flex-direction:column;justify-content:center;align-items:center;
    text-align:center;
    /* HORIZONTAL, not vertical. The phone above now travels sideways
       between panels, and copy that arrives on a different axis from
       the artwork reads as two animations that happen to be playing at
       the same time rather than as one thing turning to the next page.
       Still class driven and still CSS timed: the panel change is the
       one moment in the section where type is moving, and type that
       tracks the scrollwheel frame by frame cannot be read. */
    transform:translate3d(30px,0,0);
  }
  .scene.is-live .panel[data-state="above"]{transform:translate3d(-30px,0,0)}
  .scene.is-live .panel[data-state="on"]{transform:none}
  /* the desktop 3.1vw term runs away just under the breakpoint and
     eats the room the rail needs, so the stacked layout caps it */
  .panel h2{font-size:clamp(1.5rem,5.6vw,1.75rem)}
  .panel p{max-width:38ch}
  .scene.is-live .rail{left:0;right:0;justify-content:center;bottom:calc(20px + env(safe-area-inset-bottom))}
  /* wide enough to read as a progress bar rather than as three dashes.
     At 38px a filling tick is a detail you have to go looking for; the
     whole point of the fill is that it is caught out of the corner of
     the eye while you are reading the copy above it. */
  .rail button{width:min(23vw,84px);height:4px;border-radius:2.5px}
  .rail button::before{inset:-14px -3px}

  /* ═══ THE CAROUSEL ═════════════════════════════════════════
     Desktop parks the phone in the right column and cross fades
     the screenshot in place, which works there because the copy
     is moving into the empty half beside it and the eye has two
     places to be. Mobile has one column: the phone parks in the
     top band, the copy takes the bottom, and from the moment it
     parks NOTHING with any size to it moves again for two and a
     half screens. A cross fade between two shots of the same
     hand holding the same phone is not motion, it is a texture
     swap, and the page reads as frozen.

     So the three frames stop being a stack and become a strip:
     laid out side by side, one --frame apart, moved as one. Each
     panel change pulls the strip left by exactly one frame, so
     the phone you were looking at leaves to the left while the
     next one arrives from the right, which is the gesture every
     phone user already owns.

     --frame is solved in JS, not typed here, because the answer
     depends on the parked scale and on the fact that the phone
     is not centred on the page: it carries a 9% notch offset, so
     the two gutters need different clearances and the wider one
     has to win or a sliver of the previous hand sits at the left
     edge forever. The 112% above is only a sane value for the
     frame before measure() has run.

     Frame ONE starts dead centre. The handoff drops the phone
     straight down out of the hero into the top band, so there is
     no sideways travel to inherit; the strip is at offset 0 for
     the whole of it and only starts moving once the tour does.
     ═══════════════════════════════════════════════════════════ */
  .scene.is-live .device__shots{
    /* the filter moves down to the frames, see below */
    filter:none;
    transform:translate3d(calc(var(--slide,0) * var(--frame) * -1),0,0);
  }
  .scene.is-live .shot{
    /* the stack's opacity ladder is a desktop mechanism. Here the
       frames are separated in space instead, so every one of them is
       solid all the time and there is no combined alpha to dip. */
    opacity:1;transition:none;
    /* one shadow per frame rather than one for the strip. The strip is
       three frames wide, and a drop-shadow on the strip would ask the
       browser to rasterise and blur a layer roughly three viewports
       across on a device that has the least memory to spend on it. */
    filter:drop-shadow(0 26px 46px rgba(52,34,128,.22))
           drop-shadow(0 7px 16px rgba(17,24,39,.08));
  }
  .scene.is-live .shot--2{transform:translate3d(var(--frame),0,0)}
  .scene.is-live .shot--3{transform:translate3d(calc(var(--frame) * 2),0,0)}
}
@media (max-width:999px) and (max-height:720px){
  .scene.is-live .panel{height:37svh;padding-bottom:24px}
  .panel h2{font-size:1.375rem}
  .panel p{margin-top:12px}
  .scene.is-live .rail{bottom:calc(16px + env(safe-area-inset-bottom))}
}

/* ─── load reveal ────────────────────────────────────────── */
.rv{opacity:0;transform:translateY(16px);animation:rvIn .85s var(--ease) forwards;animation-delay:var(--d,0ms)}
@keyframes rvIn{to{opacity:1;transform:none}}

@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  .rv{animation:none;opacity:1;transform:none}
  .scene__wash{animation:none}
  *{transition-duration:.01ms !important}
}

/* ═══════════════════════════════════════════════════════════
   MORE  ·  the release after the pinned scene
   ───────────────────────────────────────────────────────────
   The scene eats four screens of pinned motion. Whatever comes
   next has to do two things at once: let the page breathe, and
   answer the one question the tour leaves behind, which is
   "fine, but does it do the thing I need". So this is a
   coverage block, not another sales block: nine named
   capabilities that scan in about five seconds, a tail of
   smaller ones, then the ask.

   Structure is a ledger, not cards. Cards would repeat the
   fallback .panel look and would weigh far more than a long
   tail of features deserves. Hairline row rules read as an
   index, which is what this actually is, and they cost one
   border each.

   The thread at the top is the only decoration in here, and it
   is doing a job: it carries the eye out of the pinned stage
   and into flat scrolling again, so the change of rhythm is
   announced rather than just happening.
   ═══════════════════════════════════════════════════════════ */
.more{
  position:relative;z-index:2;
  background:var(--bg);
  padding:clamp(34px,5vw,56px) 0 clamp(88px,11vw,140px);
}

/* ─── scroll reveal ───
   Guarded on .js so that with scripting off nothing is ever
   left invisible: the class is set in the first line of body.
   Three behaviours, one attribute:
     rise   fade + lift, one element
     stage  same, but staggered across the children
     draw   the thread, which grows instead of fading */
.js [data-in="rise"],
.js [data-in="stage"] > *{
  opacity:0;transform:translateY(14px);
  transition:opacity .65s var(--ease) var(--d,0ms),transform .75s var(--ease) var(--d,0ms);
}
.js [data-in="rise"].is-in,
.js [data-in="stage"].is-in > *{opacity:1;transform:none}
.js [data-in="stage"].is-in > *{transition-delay:calc(var(--i,0) * 55ms)}

/* ─── the thread ───
   3px, fading up into nothing so it reads as continuing out of
   the scene rather than as a rule that starts here. No terminal
   dot: the line ends on its own darkest point, which is enough
   of a full stop and one fewer element on the page. */
.more-thread{display:block;margin-inline:auto;width:3px}
.more-thread span{
  display:block;width:3px;border-radius:1.5px;
  height:clamp(96px,13vh,156px);
  /* the OLD landing page's line, carried over: a visible light
     grey edge at the top running to true black at the tip, so the
     line is present from its first pixel instead of fading in.
     Under the scrub the black end rides the moving tip, which
     draws like a pen stroke. */
  background:linear-gradient(to bottom,var(--line),#000);
  transform-origin:top center;
  transition:transform .95s var(--ease);
}
/* drawn state gated on .js for the same reason as the rest: with
   scripting off the thread is simply already there */
.js .more-thread span{transform:scaleY(0)}
.js .more-thread.is-in span{transform:scaleY(1)}
/* the drawn-lines block in landing.js can take the thread over and
   scrub it against the scroll position instead. It announces that
   with is-scroll, and the entrance transition has to go with it:
   smoothing per-frame writes through a .95s curve turns a scrubbed
   line into a rubber band that arrives a second after the reader. */
.more-thread.is-scroll span{transition:none}

/* ─── header ─── */
.more-head{text-align:center;max-width:680px;margin:clamp(26px,4vw,38px) auto 0}
.more-h{
  font-size:clamp(1.75rem,4.3vw,2.9rem);
  line-height:1.1;letter-spacing:-.035em;font-weight:500;
  text-wrap:balance;
}
.more-sub{
  margin:14px auto 0;max-width:50ch;
  font-size:clamp(1rem,1.5vw,1.125rem);
  line-height:1.6;color:var(--ink-2);text-wrap:pretty;
}

/* ─── the ledger ───
   column-gap only. The row rules break at the gaps, which is
   what separates the columns: a second set of vertical rules
   on top of that would be one accessory too many. */
.more-grid{
  display:grid;
  gap:0 clamp(24px,3.4vw,48px);
  margin-top:clamp(38px,5.5vw,60px);
}
/* Two columns, not three. Six items in two columns is three rules
   deep, which reads as a short list you can take in at a glance;
   nine in three columns read as a spec sheet. The cap keeps the
   rows from stretching to 1200px, where a 34ch paragraph would sit
   in the left third of a very wide rule. */
@media (min-width:700px){
  .more-grid{
    grid-template-columns:repeat(2,1fr);
    max-width:900px;margin-inline:auto;
  }
}

/* No icons. Six line-art glyphs down the left edge are the house
   style of every feature grid on every SaaS page, and they were the
   only thing in this section pretending to be a graphic. Without
   them the name and its description share one left edge with nothing
   to align to, and the hairline above each row is the whole
   structure, which is the same argument the FAQ below already makes. */
.more-item{
  border-top:1px solid var(--line);
  padding:22px 0 24px;
}
.more-item h3{font-size:1.0625rem;font-weight:600;letter-spacing:-.018em;line-height:1.3}
.more-item p{
  /* 38ch when there was an icon column eating 30px off the left.
     With the row starting at the rule the same measure left a
     quarter of every hairline standing over nothing, so it takes
     back roughly what the icon used to occupy. */
  margin-top:7px;max-width:41ch;
  font-size:.9375rem;line-height:1.55;color:var(--ink-2);text-wrap:pretty;
}

/* ─── the ask ───
   isolation, because .btn--primary parks its glow on z-index:-1
   and any section background would otherwise paint over it. */
.more-cta{
  margin-top:clamp(50px,7vw,78px);
  text-align:center;
  isolation:isolate;
}
.more-cta__line{
  font-size:clamp(1.25rem,2.3vw,1.6875rem);
  line-height:1.25;letter-spacing:-.03em;font-weight:500;
  text-wrap:balance;
}
.btn--lg{padding:17px 32px;font-size:1rem;border-radius:14px}
.more-cta .btn{margin-top:22px}
/* one item today, and still a list, because this is the slot the
   second and third proof line go in. If one lands, the separator
   is a `li+li::before{content:"·"}` rule; it is not carried here
   in advance, because a rule that can never match is a rule that
   gets edited on faith. */
.more-cta__meta{
  margin-top:18px;
  display:flex;flex-wrap:wrap;align-items:center;justify-content:center;gap:5px 12px;
  font-family:var(--font-ui);font-size:.8125rem;color:var(--ink-3);
}

@media (max-width:520px){
  .more-cta .btn{width:min(100%,320px)}
  /* one column, so the list is long: buy back a little of it */
  .more-item{padding:19px 0 21px}
}

/* ═══════════════════════════════════════════════════════════
   FAQ
   Two columns on desktop: the title block parks on the left and
   sticks while the questions scroll past it, so the section
   always announces itself no matter where you enter it. Below
   900px it stacks, title first, which is the only order that
   makes sense on a phone.

   The rows themselves are the reference style: no cards, no
   fills, no shadows. One hairline between each question and a
   chevron on the right. On a page that has already spent its
   decoration budget on the pinned scene, an FAQ that tries to
   be interesting is noise: this one is a list, and the only
   thing that moves is the chevron.

   The open/close is done with grid-template-rows 0fr → 1fr,
   which animates to the panel's natural height without any
   JS measuring, without a hardcoded max-height that clips long
   answers, and without a resize handler. Engines too old for
   it simply snap open, which is still a working accordion.

   The panel is kept out of the tab order and out of the
   accessibility tree while closed via visibility:hidden, with
   the visibility switch delayed until the row has finished
   collapsing so the text does not vanish mid animation.
   ═══════════════════════════════════════════════════════════ */
.faq{
  position:relative;z-index:2;
  background:var(--bg);
  padding:clamp(72px,9vw,118px) 0;
}
.faq__inner{
  display:grid;
  gap:clamp(30px,4.5vw,44px);
  max-width:1080px;margin-inline:auto;
}
@media (min-width:900px){
  .faq__inner{
    grid-template-columns:minmax(250px,330px) 1fr;
    gap:clamp(56px,7vw,104px);
    align-items:start;
  }
  /* sticks under the navbar. 34px rather than the 20px in
     html's scroll-padding-top, and the two are not meant to
     match: the scroll-padding is the minimum clearance that
     stops a jumped-to heading landing under the bar, while this
     is a parked block that sits there for the length of the
     section and wants the more generous gap. */
  .faq__aside{position:sticky;top:calc(var(--nav-h) + 34px)}
}

.faq h2{
  font-size:clamp(1.75rem,4.3vw,2.6rem);
  line-height:1.08;letter-spacing:-.035em;font-weight:500;
  text-wrap:balance;
}
.faq__note{
  margin-top:14px;max-width:36ch;
  font-size:.9375rem;line-height:1.6;color:var(--ink-2);text-wrap:pretty;
}
/* .faq__mail is styled with .lnk in the base layer. It used to carry
   its own copy of the same four declarations; see the note there. */

/* ─── the rows ─── */
.faq-item{border-top:1px solid var(--line)}
.faq-item:last-child{border-bottom:1px solid var(--line)}
.faq-item h3{font-size:inherit;font-weight:inherit;line-height:inherit}

.faq-q{
  width:100%;
  display:grid;grid-template-columns:1fr auto;align-items:start;
  column-gap:24px;
  padding:21px 2px 22px;                          /* ~64px row: comfortably over the 44px touch target */
  text-align:left;
  font-size:1.0625rem;font-weight:600;
  letter-spacing:-.018em;line-height:1.42;
  color:var(--ink);
  text-wrap:pretty;
  transition:color .2s var(--ease);
}
.faq-q:hover{color:#000}
.faq-q:hover .faq-q__ico{color:var(--ink)}
.faq-q:focus-visible{outline-offset:-2px}          /* the row is full bleed: an outset ring would clip on the hairline */

.faq-q__ico{
  width:19px;height:19px;flex:none;
  margin-top:3px;
  color:var(--ink-3);stroke-width:2;
  transition:transform .38s var(--ease),color .2s var(--ease);
}
.faq-item.is-open .faq-q__ico{transform:rotate(180deg);color:var(--ink)}

/* ─── the answer ─── */
.faq-a{
  display:grid;grid-template-rows:0fr;
  transition:grid-template-rows .42s var(--ease);
}
.faq-a__in{
  overflow:hidden;min-height:0;
  opacity:0;visibility:hidden;
  transition:opacity .2s var(--ease),visibility 0s linear .42s;
}
.faq-item.is-open .faq-a{grid-template-rows:1fr}
.faq-item.is-open .faq-a__in{
  opacity:1;visibility:visible;
  transition:opacity .32s var(--ease) .1s,visibility 0s;
}
.faq-a p{
  padding:2px 44px 26px 0;                         /* right pad clears the chevron column */
  max-width:64ch;
  font-size:.9688rem;line-height:1.65;
  color:var(--ink-2);text-wrap:pretty;
}
.faq-a p + p{padding-top:0;margin-top:-14px}       /* second paragraph: keep the 12px rhythm, not 26+2 */
.faq-a a{
  color:var(--ink);font-weight:500;
  box-shadow:inset 0 -1px 0 var(--line);
  transition:box-shadow .2s var(--ease),color .2s var(--ease);
}
.faq-a a:hover{color:var(--lav-ink);box-shadow:inset 0 -1px 0 var(--lav)}

@media (max-width:600px){
  .faq-q{font-size:1rem;column-gap:16px;padding:19px 0 20px}
  .faq-a p{padding-right:26px;padding-bottom:23px}
}
@media (prefers-reduced-motion:reduce){
  .faq-a,.faq-a__in,.faq-q__ico{transition:none}
}

/* ═══════════════════════════════════════════════════════════
   FINAL CTA
   Two modes, one element.

   Phone: the card runs edge to edge with no radius and no
   border. At 350px of usable width, 40px of side margin is 11%
   of the screen spent on showing that a box is a box, and the
   colour change already says everything a border would.

   Desktop: the card pulls in to 1080px and gets its radius and
   its hairline back. There the inset is what makes it read as
   an object placed at the end of the page rather than as one
   more full width section, and at that size the margin costs
   nothing.

   Grey card on white, which is the reference inverted. It has
   to be that way round: on a phone the card is full bleed with
   no border, so the only thing separating it from the page is
   its fill. A white card on white would simply vanish.

   isolation sits on the card because the card owns the
   background. .btn--primary parks its glow on z-index:-1 and a
   negative child paints above the background of whichever
   ancestor owns the stacking context, so if that ancestor were
   further up the tree the glow would slide behind the grey.
   ═══════════════════════════════════════════════════════════ */
/* position + z-index for the same reason every other section
   below the scene carries them: the pinned hero is a tall,
   isolated, transformed stage, and a section that is not
   positioned paints below every one that is. Last in the DOM is
   not the same as last in the paint order. */
.close{position:relative;z-index:2;padding:clamp(54px,7vw,86px) 0}

.close__card{
  position:relative;
  isolation:isolate;
  /* the rings are wider and much taller than the card on purpose,
     so that their poles crop and what is left reads as arcs. This
     is what does the cropping. It went missing when the card was
     stripped back to a flat band, which is why the arcs ran off
     into the page. */
  overflow:hidden;
  padding:clamp(44px,5.8vw,70px) var(--pad);
  text-align:center;
  /* two pools, not a wash. The hero lights its band from two
     sources, lavender on the right and warm on the left, and this
     is the same pair moved onto a diagonal so it never crosses the
     middle where the copy sits. Alphas are a third of what a hero
     scale gradient would take: at this size the peak lands about
     13 levels off the base, which is felt rather than seen. Both
     are positioned in percentages, so they ride the card down to
     the phone without a breakpoint. */
  background:
    radial-gradient(52% 78% at 88% 5%, rgba(108,92,224,.10), transparent 68%),
    radial-gradient(46% 72% at 9% 97%, rgba(217,130,43,.07), transparent 66%),
    #F4F5F7;
}
@media (min-width:700px){
  .close{padding-inline:var(--pad)}
  .close__card{
    /* the navbar's content edges, not a number of its own. .nav-inner
       is --maxw wide with --pad inside it, so its logo and its CTA sit
       exactly this far apart. Solving for it rather than typing 1120px
       means the two stay aligned if either token ever moves. */
    max-width:calc(var(--maxw) - var(--pad) * 2);margin-inline:auto;
    border:1px solid var(--line);
    border-radius:clamp(20px,2.6vw,28px);
    padding-inline:clamp(40px,6vw,72px);
  }
}

/* the measure, not the card. 640px puts the headline on two
   lines at the desktop size, which is the shape the whole
   block is built around: one line reads as a caption, three
   reads as a paragraph. */
.close__inner{max-width:640px;margin-inline:auto}

.close__mark{
  width:48px;height:48px;margin-inline:auto;
  border-radius:14px;
  background:var(--ink);color:#fff;
  display:grid;place-items:center;
  box-shadow:0 10px 22px -12px rgba(17,24,39,.55);
}
.close__mark svg{width:25px;height:25px}

/* text-wrap:balance, and a hard <br> in the markup on top of it.
   The break is authored rather than left to the wrap because
   measured from 360px to 1440px it is the only thing that holds
   this headline at two lines everywhere: without it the 520 to
   700 band collapses to one, since there the type is already at
   its clamp minimum while the measure is still nearly 600px wide.
   Two lines is the shape the block is built around, per the note
   on .close__inner above. */
.close h2{
  margin-top:clamp(18px,2.4vw,26px);
  font-size:clamp(1.625rem,4.4vw,2.75rem);
  line-height:1.1;letter-spacing:-.035em;font-weight:500;
  text-wrap:balance;
}

.close__sub{
  margin:12px auto 0;max-width:40ch;
  font-size:clamp(.9375rem,1.4vw,1.0625rem);
  /* --ink-2 is tuned for white and measures 4.43:1 on this
     grey, a hair under AA. Two steps darker clears 5.0 and is
     not visibly different. */
  line-height:1.6;color:#626A78;text-wrap:pretty;
}

.close__actions{
  margin-top:clamp(24px,2.9vw,30px);
  display:flex;gap:12px;flex-wrap:wrap;justify-content:center;
}

/* ─── the orbit, second appearance ─────────────────────────
   The hero's rings and chips, re-cut for a box instead of a
   viewport. Same vocabulary on purpose: the page opens on this
   ornament and closes on it, and in between it is never used.

   THE FRAME is a square as wide as the card, centred on it and
   cropped by the card's overflow. Square matters: inside it a
   percentage on `left` and a percentage on `top` resolve to the
   same number of pixels, which is the whole trick. It lets the
   rings be sized and the chips be placed from one set of
   unitless numbers, with no container queries and no JS, and it
   makes the system scale with the card automatically.

   RING SIZES are k * cardWidth as a DIAMETER, so k:1 is exactly
   as wide as the card and its left and right extremes land on
   the card's edges. k:.68 and k:.84 nest inside it. All three
   are taller than the card, so the poles crop and what is left
   reads as near vertical arcs sweeping through the gutters,
   which is the hero's read.

   WHERE THEY LIVE: .close__inner caps the copy at 640px, so on
   a 1120px card there is a 240px gutter each side and the arcs
   and chips sit in it. Below 1000px that gutter is too thin for
   the arcs, below 1180px too thin for the chips, so each switches
   off at the width the hero uses for the same reason.

   The vial glyph is the vial symbol in .svg-defs, the first
   block in the document. It used to live inside the hero's
   .orbit, which was a hidden dependency in both directions:
   these chips render from 1200px up, while .orbit is
   display:none below 1000px AND whenever .scene is not .is-live,
   which is every reduced-motion visitor at any width. Document
   level, it cannot be switched off by a media query belonging to
   a different section.
   ─────────────────────────────────────────────────────────── */
.close__orbit{
  position:absolute;z-index:-2;pointer-events:none;
  left:0;top:50%;
  width:100%;aspect-ratio:1;
  transform:translateY(-50%);
  /* a touch stronger than the hero's .11 / .42, because these sit
     on #F4F5F7 rather than near white and lose some separation */
  --cring:rgba(72,58,180,.13);
  --cring-lit:rgba(108,92,224,.34);
  display:none;
}
.cring{
  position:absolute;left:50%;top:50%;
  width:calc(var(--k) * 100%);
  aspect-ratio:1;
  transform:translate(-50%,-50%);      /* also the no-JS resting state */
  border:1px solid var(--cring);
  border-radius:50%;
}
/* The two lit segments are CHILDREN of the ring they belong to,
   not siblings sitting on top of it. inset:-1px pins the child's
   border box exactly onto the parent's, so the two 1px strokes
   coincide, and because the child inherits the parent's transform
   it can never drift out of register with it.

   That is the whole reason for the nesting. As siblings all three
   scaled up from .94 on their own delays, so mid entrance they
   were briefly at three different sizes and read as three separate
   hairlines where there should have been one. Nested, the segments
   only ever animate opacity. */
.cring__lit{
  position:absolute;inset:-1px;
  border:1px solid var(--cring-lit);
  border-radius:50%;
}

/* the centring is done with margins, not with a transform, so
   that the reveal below can own `transform` outright without
   having to restate the offset */
.corb{
  position:absolute;
  left:calc(50% + var(--k) * var(--cos) * 50%);
  top:calc(50% + var(--k) * var(--sin) * 50%);
  margin:-18px 0 0 -18px;
  display:none;
}
.corb__chip{
  width:36px;height:36px;border-radius:50%;
  display:grid;place-items:center;
  border:1px solid var(--line);
  background:#fff;
  /* the hero chips' shadow, unchanged: same indigo, so every
     floating object on the page is lit from the same place */
  box-shadow:0 12px 28px -16px rgba(46,32,110,.38),0 2px 6px -3px rgba(17,24,39,.10);
  /* NO IDLE DRIFT. Same call, and the same reasoning, as the hero's
     .orb__chip: these six used to run `orbDrift`, a 3px vertical float
     on a per-chip period, and they read better as objects that are
     simply placed on the ring. They still arrive with the entrance
     below; what is gone is the loop that never stopped afterwards.
     With this the last `infinite` animation on the page is the hero
     wash, so nothing down here holds a compositor job open while
     somebody sits on the footer. */
}
.corb__chip svg{width:14px;height:18px;color:var(--c)}

/* ─── the entrance ───
   Its own data-in value, so it picks up the shared observer and
   the shared .is-in without inheriting rise/stage, whose 14px
   lift would make the rings slide rather than settle. Rings
   scale up from just inside their final size, chips pop from
   80%, both ordered by --d: three rings, the two lit arcs, then
   the six chips. Under a second end to end. */
.js [data-in="orbit"] .cring{
  opacity:0;transform:translate(-50%,-50%) scale(.94);
  transition:opacity .85s var(--ease) var(--d,0ms),transform 1.05s var(--ease) var(--d,0ms);
}
.js [data-in="orbit"].is-in .cring{opacity:1;transform:translate(-50%,-50%) scale(1)}
.js [data-in="orbit"] .cring__lit{opacity:0;transition:opacity .6s var(--ease) var(--d,0ms)}
.js [data-in="orbit"].is-in .cring__lit{opacity:1}
.js [data-in="orbit"] .corb__rv{
  opacity:0;transform:scale(.8);
  transition:opacity .5s var(--ease) var(--d,0ms),transform .62s var(--ease) var(--d,0ms);
}
.js [data-in="orbit"].is-in .corb__rv{opacity:1;transform:scale(1)}

@media (min-width:1000px){.close__orbit{display:block}}
/* 1200px is not a taste call, it is the width at which the card
   stops growing: it caps at --maxw minus the padding, so from here
   up the geometry is frozen and the chips sit where they were
   solved for. One notch narrower and the k:.68 pair creeps 3px
   into the copy column. */
@media (min-width:1200px){.corb{display:block}}
@media (prefers-reduced-motion:reduce){
  .js [data-in="orbit"] .cring,
  .js [data-in="orbit"] .cring__lit,
  .js [data-in="orbit"] .corb__rv{transition:none}
}
/* 520px, the same number the hero uses, and for the same
   reason: it is the width at which the two buttons stop
   fitting on one line. Above it the secondary is free, it sits
   beside the primary and costs no vertical space. Below it the
   row would stack and the second button would cost a full
   button of height at the one moment the page wants a single
   obvious action. */
@media (max-width:520px){
  .close__actions{flex-direction:column;align-items:center}
  .close__actions .btn{width:min(100%,320px)}
  .close__alt{display:none}
  .close__mark{width:44px;height:44px;border-radius:13px}
  .close__mark svg{width:23px;height:23px}
}

/* ═══════════════════════════════════════════════════════════
   WHY IT IS FREE
   ───────────────────────────────────────────────────────────
   This block answers exactly one sentence. The More section
   ends on "every one of these is free", and the next thought
   in the reader's head is "so what is the catch". Nowhere
   else on the page is allowed to answer that: an answer that
   only turns up in the FAQ reads as an admission.

   TEXTURE. The two sections either side of this one are
   hairline ledgers, the coverage list above and the FAQ
   below. A third set of rules here would flatten the whole
   lower half of the page into one grey stripe. So this is the
   chapter that has surface: a tinted band with one white
   object sitting on it. The band's two pools are the closing
   card's gradient at about two thirds strength, so it reads
   as the same light rather than as a new idea.

   THE RECEIPT is the section. The objection is about money,
   so the answer is the artifact money leaves behind: an
   itemised bill where every line is 0.00. It lands in about
   two seconds on a phone, which is the whole point, because
   the prose beside it is the part that actually has to be
   read and it only gets read if the reader is not already
   bracing for a price.

   The torn bottom edge is a mask. That forces the shadow to
   be a drop-shadow on a wrapper: a box-shadow on the masked
   element gets masked away with it, and an inset one would
   leave hairline stubs on the tips of the teeth. Same trick
   the device uses in the hero, same reason.

   ORDER. Receipt first in the DOM, prose second. On a phone
   that is the order you want anyway, payoff then
   explanation, and on desktop it falls into two columns with
   no reordering and no source-order lie for screen readers.
   ═══════════════════════════════════════════════════════════ */
.why{
  position:relative;z-index:2;
  padding:clamp(64px,8.5vw,108px) 0 clamp(70px,9.5vw,120px);
  /* no rules at the edges. The band is full bleed at every width,
     and the closing card already argues this case: when a block
     runs edge to edge the change of colour says everything a
     border would, and the border only costs a seam. */
  background:
    radial-gradient(56% 50% at 84% 0%, rgba(108,92,224,.065), transparent 70%),
    radial-gradient(50% 46% at 4% 100%, rgba(217,130,43,.055), transparent 68%),
    #F7F7FA;
}

/* ─── header ─── */
.why-head{text-align:center;max-width:640px;margin-inline:auto}
.why-head h2{
  font-size:clamp(1.75rem,4.3vw,2.6rem);
  line-height:1.08;letter-spacing:-.035em;font-weight:500;
  text-wrap:balance;
}
.why-sub{
  margin:14px auto 0;max-width:46ch;
  font-size:clamp(1rem,1.5vw,1.125rem);
  line-height:1.6;color:var(--ink-2);text-wrap:pretty;
}

/* ─── the pair ───
   400px is the widest the receipt can be before the dot
   leaders get long enough to read as a table of contents
   rather than as a bill. The prose takes whatever is left. */
.why-grid{
  max-width:1000px;
  margin:clamp(38px,5vw,58px) auto 0;
  display:grid;gap:clamp(34px,5vw,52px);
}
@media (min-width:920px){
  .why-grid{
    grid-template-columns:minmax(0,400px) 1fr;
    gap:clamp(44px,5.5vw,76px);
    align-items:start;
  }
}

/* ─── the receipt ─── */
.why-receipt{
  /* follows the masked alpha of the child, so the shadow is
     torn along with the paper instead of squaring it off */
  filter:drop-shadow(0 20px 36px rgba(52,34,128,.11))
         drop-shadow(0 3px 8px rgba(17,24,39,.05));
}
.why-paper{
  --tooth:10px;
  background:#fff;
  border-radius:var(--r-lg) var(--r-lg) 0 0;   /* the bottom corners are eaten by the teeth */
  padding:clamp(22px,3.6vw,28px) clamp(20px,3.4vw,26px) calc(var(--tooth) + 20px);
  /* two layers: a solid plate everywhere except the last
     --tooth pixels, then one row of downward triangles.
     The triangle is a 90 degree wedge fired from the bottom
     centre of each tile, which is cheaper and crisper than
     four linear-gradients doing the same job. Engines without
     mask support simply get a straight edge, which is fine. */
  -webkit-mask:
    linear-gradient(#000 0 0) top left/100% calc(100% - var(--tooth)) no-repeat,
    conic-gradient(from -45deg at 50% 100%, #000 90deg, #0000 90deg)
      bottom left/calc(var(--tooth) * 2) var(--tooth) repeat-x;
  mask:
    linear-gradient(#000 0 0) top left/100% calc(100% - var(--tooth)) no-repeat,
    conic-gradient(from -45deg at 50% 100%, #000 90deg, #0000 90deg)
      bottom left/calc(var(--tooth) * 2) var(--tooth) repeat-x;
}

.why-paper__head{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  padding-bottom:13px;border-bottom:1px solid var(--line);
  font-family:var(--font-mono);
  font-size:.6875rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-3);
}
.why-paper__head span:first-child{color:var(--ink-2);font-weight:500}

/* ─── the lines ───
   The labels are quiet and the amounts are not, which is the
   opposite of a real bill and entirely on purpose: the column
   of identical noughts is the argument, so it gets the ink. */
.why-lines{margin-top:4px}
.why-line{
  display:flex;align-items:baseline;gap:8px;
  padding:9px 0;
  font-family:var(--font-mono);
  font-size:clamp(.75rem,3vw,.8125rem);
  line-height:1.4;
  color:var(--ink-2);
}
/* an empty flex item takes its bottom margin edge as its
   baseline, so a border-bottom on it lands exactly on the
   text baseline either side with nothing to tune */
.why-line i{
  flex:1 1 auto;min-width:12px;
  border-bottom:1px dotted #D5D8DE;
}
.why-line b{
  flex:none;font-weight:500;color:var(--ink);
  font-variant-numeric:tabular-nums;
}
/* the one line people are really asking about */
.why-line--em{color:var(--ink)}

.why-total{
  margin-top:8px;padding-top:15px;
  border-top:1px dashed #D5D8DE;
  display:flex;align-items:baseline;gap:12px;
  font-family:var(--font-mono);
}
.why-total span{
  font-size:.6875rem;letter-spacing:.14em;text-transform:uppercase;
  color:var(--ink-3);
}
.why-total b{
  margin-left:auto;
  font-size:clamp(1.375rem,5.4vw,1.625rem);
  font-weight:500;letter-spacing:-.02em;color:var(--ink);
  font-variant-numeric:tabular-nums;
}
.why-total__note{
  margin-top:6px;text-align:right;
  font-family:var(--font-ui);font-size:.75rem;line-height:1.5;
  color:var(--ink-2);
}

/* ─── the prose ───
   Two reasons on one rail: what pays today, what pays soon.
   The rail is the More thread again at conversation scale,
   same ink, faded out at both ends so it reads as a segment
   of time rather than as a border. The dots sit ON it: they
   are opaque, so the rail simply passes behind them and no
   halo is needed, which matters because the band behind this
   column is tinted and a bg-coloured ring would read as a
   hole punched in it.

   No hairlines between the reasons: the ledger above and the
   FAQ below both use them, and this column earns its
   separation from spacing and the rail.

   The label is the section's second voice, not a caption, so it
   is set as a smaller sibling of the h2 rather than as a mono
   eyebrow. That also leaves mono to the receipt alone, which is
   what makes the paper on the left read as an artifact. */
.why-label{
  font-size:clamp(1.1875rem,2.4vw,1.4375rem);
  font-weight:500;letter-spacing:-.028em;line-height:1.2;
  color:var(--ink);
}
.why-reasons{
  position:relative;
  margin-top:clamp(22px,2.8vw,28px);
  padding-left:27px;
  display:grid;gap:clamp(26px,3.4vw,34px);
}
/* the rail. 4.5 centres 2px under an 11px dot. Solid from its
   first pixel: the top edge sits under the Today dot, so the
   line reads as growing straight out of it, and only the far
   end fades, as the open end of the timeline. --railp is the
   drawn fraction, written per scroll frame by the drawn-lines
   block in landing.js. The fallback is 1, so with scripting
   off, with reduced motion, or before the script has run, the
   rail is simply already there. Scaling squeezes the gradient
   with it, which is the point: the moving tip is always the
   fade-out end, so it draws like ink running down. */
.why-reasons::before{
  content:"";position:absolute;left:4.5px;top:12px;bottom:14px;
  width:2px;border-radius:1px;
  background:linear-gradient(to bottom,rgba(17,24,39,.16) 78%,rgba(17,24,39,0));
  transform:scaleY(var(--railp,1));
  transform-origin:top center;
}
.why-reason{position:relative}
/* on the rail, centred on the Today/Soon line beside it: the
   label's 11px/1.6 line box is 17.6px tall, and (17.6 - 11) / 2
   rounds to the 3px here */
.why-reason__dot{
  position:absolute;left:-27px;top:3px;
  width:11px;height:11px;border-radius:50%;
  background:var(--c);
}
.why-reason__when{
  display:block;margin-bottom:5px;
  font-family:var(--font-ui);
  font-size:.6875rem;font-weight:600;letter-spacing:.12em;text-transform:uppercase;
  color:var(--ink-3);
}
.why-reason h4{
  font-size:1.0625rem;font-weight:600;
  letter-spacing:-.018em;line-height:1.35;
}
.why-reason p{
  margin-top:7px;max-width:54ch;
  font-size:.9375rem;line-height:1.6;color:var(--ink-2);text-wrap:pretty;
}

@media (max-width:520px){
  .why-paper{--tooth:9px}
  .why-line{padding:8px 0}
}

/* ═══════════════════════════════════════════════════════════
   PROOF
   ───────────────────────────────────────────────────────────
   The section above removes the objection about price. What
   is left is the one about substance: fine, it is free, but
   is anyone using it. So: five figures and nothing else. No
   quotes, because a quote is not checkable and these are.

   ═══ THE ARC ═══
   Every card sits on ONE circle, and the whole geometry is
   two numbers rather than five sets of coordinates.

   The trick is transform-origin. Park it at a point R below
   a card's own centre and rotate: the card swings along a
   circle of radius R about that point. Give all five cards
   the same starting position and the same origin, and five
   different angles put them on the same arc, each already
   tilted by exactly its own angular position. No per card
   left/top, no trigonometry in the stylesheet, and the whole
   composition scales off a single length, --cw.

   R is 4.15 card widths and the angles step by 18 degrees,
   both read off the reference. Two consequences follow from
   that pair and both were solved before they were typed:

     WIDTH   the fan measures 6.458 * --cw. That is what caps
             --cw at 13.5vw: at 900px, the narrowest this
             layout runs, 6.458 * 121px is 785px inside an
             820px column. It never reaches for the gutter.

     GAPS    the chord between neighbouring centres is 1.298
             * --cw, so there is 0.298 of a card width of air
             between every pair, at every viewport, without a
             gap property being involved at all. They cannot
             touch, because the circle will not let them.

   Fan height is 2.272 * --cw, which is the lowest point of
   the outermost card, and the big figure is pulled up into
   the bowl by 0.52 of a card width. That clears the middle
   pair's lowest corner by about 20px at full size. If the
   angles change, that number wants re-solving.

   ═══ BELOW 900px ═══
   An arc needs width to be an arc. At 390px it would be a
   flat line with the cards on top of each other, so the
   layout switches to a flex row of three with a real gap and
   a 7 degree tilt, and the outermost pair is dropped. Those
   two are the least load bearing on purpose: what they say
   is already said in the section above this one.

   That row is SOLVED against the viewport rather than sized
   by hand, see the note on .proof-arc. The two numbers it
   is solved from are the air between two cards and the air
   at the screen edge, because those are the two things that
   are visible; the card width is the leftover, not the
   input. Doing it the other way round is what left a 393px
   phone carrying 98px cards inside 52px of dead margin.

   ═══ COLOUR ═══
   Ink, green, lavender, amber, ink. The reference runs black
   at both ends of its arc and that symmetry is worth taking.
   All four are on this page already, but the green and the
   amber are the mint and peach tokens darkened: as published
   both sit near 3:1 against white, which is fine behind a
   5px chip in the hero orbit and not fine behind an 11px
   label. The orbit keeps the bright ones.

   No fills over the fills, no hover. A card here is a number
   on a coloured rectangle and it does not need help.
   ═══════════════════════════════════════════════════════════ */
.proof{
  position:relative;z-index:2;
  background:var(--bg);
  padding:clamp(72px,9.5vw,120px) 0 clamp(76px,10vw,126px);
}

.proof-head{text-align:center;max-width:660px;margin-inline:auto}
.proof-head h2{
  font-size:clamp(1.75rem,4.3vw,2.6rem);
  line-height:1.08;letter-spacing:-.035em;font-weight:500;
  text-wrap:balance;
}
.proof-sub{
  margin:14px auto 0;max-width:48ch;
  font-size:clamp(1rem,1.5vw,1.125rem);
  line-height:1.6;color:var(--ink-2);text-wrap:pretty;
}

/* ─── the composition ───
   --cw is the one length everything else is expressed in, so
   the card, the type inside it and the arc it rides can only
   ever scale together. */
.proof-arc{
  /* ─── the phone row, solved rather than guessed ─────────
     The part a plain width guess keeps getting wrong is the
     tilt. A rotated box is wider than the box: at 7 degrees
     with this card's 29/38 it measures w*cos7 + h*sin7, so
     1.152w, and it is rotated about its own centre, which
     means each outer card pokes .076 of a card width past
     its own layout edge on BOTH sides. Four of those in a
     row of three. So the row is not 3cw + 2 gaps, it is:

       100vw = 3cw + 4(.076 cw) + 2 gaps + 2 edges
             = 3.304 cw + 2(--fan-gap) + 2(--fan-edge)

     which rearranges into --cw below. The two numbers that
     stay hand set are the only two anyone can actually SEE:
     the air between two cards, and the air left at the
     screen edge. The card width is what falls out of them.

     Turn it round, set the width and let the margin be the
     leftover, and that leftover is invisible until it is
     enormous, which is exactly what happened: 98px cards on
     a 393px phone with 26px of nothing down each side.

     Both are floors, not targets. The clamp's 78px min wins
     on a very narrow phone and the 160px max wins on a
     tablet, and in both cases the surplus goes back into the
     edge, which is the harmless place to put it.

     100vw, not the container's width, because a % inside a
     custom property is meaningless once --cw is read as a
     font-size further down. On a desktop window under 900px
     with a classic scrollbar that runs ~15px long: it spends
     the edge, never the gap, and cannot overflow, because
     body is already overflow-x:clip. */
  --fan-edge:10px;    /* screen edge -> tilted corner */
  --fan-gap:7px;      /* card        -> card, as the eye sees it */
  --cw:clamp(78px,calc((100vw - 2 * var(--fan-edge) - 2 * var(--fan-gap)) / 3.304),160px);
  margin-top:clamp(44px,6vw,68px);
}

.proof-fan{
  display:flex;justify-content:center;align-items:flex-start;
  /* the LAYOUT gap is the visual gap plus the ONE overhang that
     lands between an outer card and the middle one. The other
     overhang of that pair points at the screen edge and is
     already paid for by --fan-edge. */
  gap:calc(var(--fan-gap) + var(--cw) * .076);
  /* the row is wider than .wrap's content box now, on purpose, so
     it is handed the page padding back instead of overflowing
     into it. Reset at 900px, where the arc takes over. */
  margin-inline:calc(var(--pad) * -1);
}
.proof-card{
  position:relative;flex:none;
  width:var(--cw);
  aspect-ratio:29/38;
  padding:calc(var(--cw) * .115);
  display:flex;flex-direction:column;justify-content:flex-end;
  border-radius:calc(var(--cw) * .12);
  color:#fff;background:var(--c);
  box-shadow:0 16px 30px -14px var(--sh),0 3px 8px -4px rgba(17,24,39,.16);
  /* hidden, for the watermark and only the watermark. The pulse
     line runs off both sides of its card on purpose and this is
     what crops it at the rounded edge. Nothing else in the card
     ever leaves the box, and the shadow is the card's own so it
     is not clipped by the card's own overflow. */
  overflow:hidden;
  /* ONE function list, in both layouts and in both states. That
     is the whole reason the opening is smooth: matching lists
     interpolate function against function, mismatched ones fall
     back to decomposing a matrix, and a matrix does not know
     that this card is swinging on a circle. */
  transform:translate(var(--x,0px),var(--y,0px)) rotate(var(--r,0deg));
  transition:transform .9s cubic-bezier(.34,.75,.26,1) var(--dl,0ms);
}
/* ─── the hanger slot ─────────────────────────────────────
   The euro slot every gift card on a rack is punched with: a
   round peg hole and a wide slot under it, cut as one shape.
   Two elements here, and the numbers are not eyeballed, they
   are solved so the union is the real die cut:

     the circle is .10 across and the slot is .054 tall, so
     the circle stands .046 proud of the slot's top edge and
     its lowest point lands EXACTLY on the slot's bottom edge.
     Tangent, not overlapping past it. That is what gives the
     two little concave shoulders where they meet, and those
     shoulders are the whole tell: without them the top half
     reads as a separate blob sitting on a bar.

   Change either height and the other one follows, or the
   circle starts poking out of the bottom of the slot.

   It is a FILL, not a cut. A real mask would clip the card's
   box-shadow away with it, the same trap the receipt walks
   into, so this is painted in the section's own background
   colour instead. That is the one dependency: change .proof
   away from var(--bg) and these have to follow.

   Sized off --cw like everything else, so the slot on a 78px
   phone card and on a 166px desktop card are one object at
   two sizes rather than two guesses. The card's type is
   bottom aligned, so nothing ever meets it.
   ─────────────────────────────────────────────────────── */
.proof-card::before,
.proof-card::after{
  content:"";position:absolute;left:50%;
  background:var(--bg);
  pointer-events:none;
  /* above the watermark, below nothing. A real die cut is punched
     through the print, so where a mark passes under the slot the
     slot wins, exactly as it would on the rack. */
  z-index:2;
}
.proof-card::before{                      /* the peg hole */
  width:calc(var(--cw) * .10);
  height:calc(var(--cw) * .10);
  margin-left:calc(var(--cw) * -.05);
  top:calc(var(--cw) * .095);
  border-radius:50%;
}
.proof-card::after{                       /* the slot under it */
  width:calc(var(--cw) * .325);
  height:calc(var(--cw) * .054);
  margin-left:calc(var(--cw) * -.1625);
  top:calc(var(--cw) * .141);             /* .095 + .046, so the two are tangent */
  border-radius:var(--r-pill);
}

/* the outer pair is the desktop arc's business only */
.proof-card:nth-child(1),
.proof-card:nth-child(5){display:none;--dl:60ms}
.proof-card:nth-child(2){--y:calc(var(--cw) * .14);--r:-7deg;z-index:2}
.proof-card:nth-child(3){z-index:3}
.proof-card:nth-child(4){--y:calc(var(--cw) * .14);--r:7deg;z-index:2}

/* ─── the row, which is where they start ──────────────────
   Read this rule as the ONLY thing the animation is. There is
   no keyframe and no script that touches a style: the cards
   are laid out flat and untilted while the arc is waiting, a
   class lands on the wrapper, three custom properties change,
   and the transition on transform does the rest.

   Gated on .js on purpose. With scripting off, or with reduced
   motion, none of this applies and the cards are simply already
   fanned. Nothing is ever left in a state a visitor can get
   stuck looking at.

   COST: five elements, transform only, once. No layout, no
   paint, no filter being recomputed. The box-shadow travels
   inside each card's own layer and is never re-blurred, because
   nothing here scales. This is the cheapest kind of animation
   there is, which is why it can afford to be the big one. */
.js .proof-arc:not(.is-in) .proof-card{--x:0px;--y:0px;--r:0deg}
/* the hint is added a frame before the class and dropped when
   the motion is over, so nothing keeps a layer it is not using */
.proof-arc.is-armed .proof-card{will-change:transform}

/* ─── the watermark ─────────────────────────────────────────
   The print on the gift card: one mark per card, drawn in the
   card's own colour a step lighter. The step is not a second
   set of tokens, it is white at 9% over the fill, which lands
   on "same colour, lighter" for all four fills at once.

   THE OPACITY SITS ON THE SPAN, NOT ON THE SHAPES. A mark is
   several subpaths and some of them touch; opacity per shape
   would double up into darker seams wherever they do. On the
   group the mark is composited first and faded once, so it
   reads as one object no matter how it is built.

   Layer order inside a card, bottom to top: fill, watermark
   (z1), slot pseudos (z2), type (z3). The slot above the mark
   is deliberate, see the note on the pseudos. The type above
   everything is what keeps the 9% honest: the label was
   checked against the raw fill, and the mark can only ever
   darken-by-lightening what is behind it by that same 9%.

   Geometry rides the card box in percentages, so the mark on
   a 109px phone card and a 166px desktop card is one
   composition at two sizes. --wl/--wt/--ww are set inline per
   card in the markup, next to the --a/--c/--sh that card
   already carries. */
.proof-wm{
  position:absolute;z-index:1;
  left:var(--wl,0);top:var(--wt,0);width:var(--ww,80%);
  color:#fff;opacity:.09;
  pointer-events:none;
}
.proof-wm svg{display:block;width:100%;height:100%}

.proof-card__n{
  position:relative;z-index:3;
  font-size:calc(var(--cw) * .205);
  font-weight:500;letter-spacing:-.04em;line-height:1;
  white-space:nowrap;
}
.proof-card__k{
  position:relative;z-index:3;
  margin-top:calc(var(--cw) * .05);
  font-family:var(--font-ui);
  /* floored, because 0.078 of a 78px card is 6px and no label
     survives that. Below the floor the card simply carries a
     proportionally larger label, which is the right way round */
  font-size:max(10px,calc(var(--cw) * .078));
  font-weight:500;line-height:1.3;
  color:rgba(255,255,255,.82);
}

/* ─── the figure in the bowl ───
   An arc without something at its centre is a decoration
   looking for a job. This is the number the arc is built
   around, which is why it is not on a card. */
.proof-bowl{
  margin-top:clamp(30px,5vw,42px);
  text-align:center;
}
.proof-bowl b{
  display:block;
  font-size:max(2.25rem,calc(var(--cw) * .36));
  font-weight:500;letter-spacing:-.045em;line-height:1;
}
/* tabular figures, because this one is counted up on the way in
   and proportional digits make a rolling number twitch sideways
   on every frame. The comma is narrower than a figure either way,
   so the string still grows out of the centre while the digits
   themselves hold still. */
.proof-bowl__n{font-variant-numeric:tabular-nums}
/* child combinator, not a descendant one. The figure above carries
   its own span for the digits that get counted, and a descendant
   selector here would hand that span the caption's size and face. */
.proof-bowl > span{
  display:block;margin-top:10px;
  font-family:var(--font-ui);font-size:.875rem;font-weight:500;
  color:var(--ink-2);
}

/* ═══ the arc proper ═══ */
@media (min-width:900px){
  .proof-arc{--cw:min(13.5vw,166px);--step:calc(var(--cw) + clamp(10px,1.1vw,18px))}

  .proof-fan{
    display:block;position:relative;
    /* the phone row's breakout goes away with the phone row. The
       arc is absolutely positioned off this box's centre and does
       not want a wider box to be centred in. */
    margin-inline:0;
    height:calc(var(--cw) * 2.272);
  }
  .proof-card{
    position:absolute;left:50%;top:0;
    margin-left:calc(var(--cw) / -2);
    /* 4.805 = half a card height (0.6552) + R (4.15), both in
       card widths, so the pivot lands R below the card centre */
    transform-origin:50% calc(var(--cw) * 4.805);
    --r:var(--a);
  }
  .proof-card:nth-child(1),
  .proof-card:nth-child(5){display:flex;z-index:1}
  /* undo the phone layout's tilt and drop for the middle pair */
  .proof-card:nth-child(2),
  .proof-card:nth-child(4){--y:0px;--r:var(--a);z-index:2}

  /* the row spacing, and the only number in the opening that is
     not already fixed by the circle */
  .js .proof-arc:not(.is-in) .proof-card:nth-child(1){--x:calc(var(--step) * -2)}
  .js .proof-arc:not(.is-in) .proof-card:nth-child(2){--x:calc(var(--step) * -1)}
  .js .proof-arc:not(.is-in) .proof-card:nth-child(4){--x:var(--step)}
  .js .proof-arc:not(.is-in) .proof-card:nth-child(5){--x:calc(var(--step) * 2)}

  .proof-bowl{margin-top:calc(var(--cw) * -.52)}
}

/* ═══════════════════════════════════════════════════════════
   PLACEHOLDER for the sections still to come.
   ═══════════════════════════════════════════════════════════ */
.todo{padding:clamp(80px,11vw,120px) var(--pad);text-align:center}
.todo p{font-family:var(--font-mono);font-size:.7rem;letter-spacing:.16em;text-transform:uppercase;color:var(--ink-3)}
