/* GoalPace website — V2 "In Motion".
   Design system ported from the app (Theme.swift), with the warmer App Store
   panel background, thin device frames, and motion photography as the spine.
   Light-first, adaptive to the visitor's system dark-mode preference. */

/* ---- Satoshi (self-hosted). Regular=400, Bold=700, Black=900. ---- */
/* Paths are relative to THIS stylesheet (which lives in /assets/), so they are
   "fonts/…" not "assets/fonts/…". The latter resolved to /assets/assets/fonts/
   and 404'd; fonts still rendered only because the HTML <link rel=preload> paths
   (relative to the document root) happened to be correct. */
@font-face { font-family:"Satoshi"; font-weight:400; font-style:normal; font-display:swap; src:url("fonts/Satoshi-Regular.woff2") format("woff2"); }
@font-face { font-family:"Satoshi"; font-weight:700; font-style:normal; font-display:swap; src:url("fonts/Satoshi-Bold.woff2") format("woff2"); }
@font-face { font-family:"Satoshi"; font-weight:900; font-style:normal; font-display:swap; src:url("fonts/Satoshi-Black.woff2") format("woff2"); }

/* ---- Tokens ---- */
:root {
  --bg: #EAEAE5;            /* warm App Store panel greige (was #F8F8F7) */
  --surface: #FFFFFF;
  --ink: #121314;
  /* Secondary is DARKER than the app's #727275 because the warmer bg drops that
     value to ~4.0:1; #5F5F65 restores WCAG AA (~5:1) on #EAEAE5. */
  --ink-secondary: #5F5F65;
  --accent: #FFC90E;        /* goal gold, identical both modes */
  --on-accent: #121314;
  --cta-ink: #121314;
  /* A filled CTA pill sitting on the PAGE surface (the scrolled nav), as
     opposed to --cta-ink, which is the ink of the white pill on photography.
     It has to invert with the theme for the same reason .btn-store does: the
     value was hardcoded to #121314, so in dark mode the nav's Download button
     was a #121314 pill on the #0E0E0F page — a 1.03:1 pill, invisible, and the
     one control the fixed nav exists to show. The rule to keep is that a filled
     pill is always the maximum-contrast inversion of what is behind it. */
  --cta-fill: #121314;
  --cta-fill-ink: #FFFFFF;
  --pill-bg: rgba(0,0,0,0.05);
  --line: rgba(18,19,20,0.10);
  --gold-grad: linear-gradient(180deg,#FFEB6B 0%,#FFC90E 55%,#EB9900 100%);
  --radius: 24px;
  --dark-bg: #08080A;       /* photo-section fallback, behind the motion images */
  /* The solid dark band (privacy) is a SEPARATE token from --dark-bg on purpose.
     In light mode the two are identical and the band is the page's dark
     counterpoint (16.6:1 against the greige). In dark mode that relationship
     collapses: page #0E0E0F against band #060608 is a 1.05:1 step, so the band
     stops reading as a band and tabs/science/privacy fuse into one slab. Note
     that WCAG contrast is the wrong instrument for this judgement — its +0.05
     denominator floor puts EVERY near-black pair at ~1.1:1 no matter how far
     apart they look — so the fix is perceptual: lift the band into a raised
     surface and give it explicit hairline edges (see .privacy). */
  --band-bg: #08080A;
  --dark-ink: #F0F0ED;
  --dark-sec: #9A9A9C;
  --maxw: 1200px;
  --gutter: clamp(20px, 5vw, 56px);
  --font: "Satoshi", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0E0E0F; --surface: #1A1A1C; --ink: #F0F0ED; --ink-secondary: #9A9A9E;
    --pill-bg: rgba(255,255,255,0.10); --line: rgba(255,255,255,0.12);
    --dark-bg: #060608;
    --cta-fill: #F0F0ED; --cta-fill-ink: #121314;
    /* Lifted ABOVE the page, not sunk below it: below near-black there is no
       room left to go darker, so the band has to rise to stay a band. This is
       --surface's dark value, reused deliberately rather than a new one-off grey
       — it gives the band the same elevation language as a card. Measures +5.9 L*
       against the page where the old #060608 sat 2.3 L* BELOW it. */
    --band-bg: #1A1A1C;
  }
}

/* ---- Reset / base ---- */
*,*::before,*::after { box-sizing:border-box; }
/* scroll-padding-top was unset ("auto"), so the #showcase anchor from "See how it
   works" landed with its first 65px underneath the fixed nav. */
html { -webkit-text-size-adjust:100%; scroll-behavior:smooth; scroll-padding-top:84px; }
@media (prefers-reduced-motion: reduce){ html{ scroll-behavior:auto; } }
body {
  margin:0; font-family:var(--font); font-weight:400; font-size:17px; line-height:1.55;
  color:var(--ink); background:var(--bg); overflow-x:hidden;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
}
img { max-width:100%; height:auto; display:block; }
a { color:inherit; }
.tnum { font-variant-numeric:tabular-nums; }
.skip { position:absolute; left:-9999px; top:0; z-index:200; background:var(--ink); color:var(--bg); padding:10px 16px; border-radius:0 0 10px 0; }
.skip:focus { left:0; }
:where(a,button):focus-visible { outline:2px solid currentColor; outline-offset:3px; border-radius:6px; }

.wrap { max-width:var(--maxw); margin-inline:auto; padding-inline:var(--gutter); }
section { position:relative; }

/* ---- Type ---- */
h1,h2,h3 { margin:0; font-weight:900; letter-spacing:-0.025em; line-height:0.98; }
/* Sized so the headline sets in TWO lines, which took measuring rather than
   guessing. The hero column is 62% of the wrap's content box, not of the
   viewport — 675px at any width from 1200px up, not the 780px the max-width
   suggests. At 108px the narrowest possible two-line split ("you trained for.",
   745px) does not fit, so it fell to three lines with an orphaned "for." at the
   end. 8vw capped at 96px keeps the longest line inside the column at every
   width from 360px up, so the headline sets as "Hit the time / you trained for."
   Two lines at 96px hits harder than three at 108px. balance stays as insurance
   for the narrowest phones, where three lines is unavoidable. */
h1 { font-size:clamp(48px, 8vw, 96px); text-wrap:balance; }
h2 { font-size:clamp(34px, 5.6vw, 68px); }
h3 { font-size:clamp(22px, 2.6vw, 30px); letter-spacing:-0.02em; }
p { margin:0; }
.lead { font-size:clamp(19px, 2.2vw, 24px); line-height:1.45; font-weight:400; }
.sub  { color:var(--ink-secondary); }
.eyebrow { font-size:12px; font-weight:700; letter-spacing:0.16em; text-transform:uppercase; color:var(--ink-secondary); }

/* ---- Brand mark (inline glyph, no box; inherits currentColor) ---- */
.brand { display:inline-flex; align-items:center; gap:10px; font-weight:900; font-size:21px; letter-spacing:-0.02em; text-decoration:none; color:inherit; }
.brand-glyph { height:26px; width:auto; display:block; flex:none; }

/* ---- Nav (transparent over hero, solid after) ---- */
/* padding-top clears the status bar / Dynamic Island when a page opts into
   viewport-fit=cover (only index does, for the full-bleed dark hero). env()
   resolves to 0 on pages without cover, so this is harmless there. */
.nav { position:fixed; inset:0 0 auto 0; z-index:100; color:#fff; padding-top:env(safe-area-inset-top); transition:background .3s ease, color .3s ease, border-color .3s ease; border-bottom:1px solid transparent; }
.nav .wrap { display:flex; align-items:center; justify-content:space-between; height:64px; }
.nav a { text-decoration:none; }
/* Download is a real button, always. Ghost white pill over the photo hero,
   filled dark pill once the nav goes solid. Never a bare text link. */
.nav .nav-cta { font-weight:700; font-size:14.5px; letter-spacing:-0.01em; padding:9px 20px; border-radius:9999px; border:1.5px solid rgba(255,255,255,.55); color:#fff; transition:background .2s ease, color .2s ease, border-color .2s ease, transform .18s ease; }
.nav .nav-cta:hover { background:rgba(255,255,255,.14); transform:translateY(-1px); }
/* No bottom hairline: the blur + tint separate it; a 1px line read as a dark seam. */
.nav.solid { background:color-mix(in srgb, var(--bg) 82%, transparent); backdrop-filter:saturate(180%) blur(20px); -webkit-backdrop-filter:saturate(180%) blur(20px); color:var(--ink); border-color:transparent; }
.nav.solid .nav-cta { background:var(--cta-fill); color:var(--cta-fill-ink); border-color:transparent; }
.nav.solid .nav-cta:hover { background:var(--cta-fill); opacity:.9; transform:translateY(-1px); }

/* ---- Buttons ---- */
/* Both .btn-store instances live on dark motion photography (hero and close), so
   the button is WHITE on dark, not dark on dark. It used to be --cta-ink over
   those backdrops, which measured 1.10:1 in the hero and 1.04:1 in the close
   against the darkest adjacent pixels — the page's single most important element
   was effectively invisible, and it broke the app's own documented floor
   ("interactive icons need 3:1", CLAUDE.md). White measures ~18.6:1 there, and it
   is also Apple's own convention for the App Store badge on dark artwork.
   If a CTA is ever placed on a light (greige) section it must invert back to
   --cta-ink on white; do not reuse this rule there. */
.btn-store {
  display:inline-flex; align-items:center; gap:11px; background:#fff; color:var(--cta-ink);
  text-decoration:none; padding:15px 26px; border-radius:9999px; border:0;
  box-shadow:0 10px 30px -12px rgba(0,0,0,.55);
  transition:transform .18s ease, box-shadow .18s ease;
}
.btn-store:hover { transform:translateY(-1px); box-shadow:0 16px 38px -12px rgba(0,0,0,.6); }
.btn-store svg { width:22px; height:22px; fill:var(--cta-ink); flex:none; }
.btn-store small { display:block; font-weight:400; font-size:11px; line-height:1.1; opacity:.62; }
.btn-store b { display:block; font-weight:700; font-size:17px; line-height:1.15; margin-top:1px; }
/* Reassurance under a CTA. Answers "what does it cost / will it run on my phone"
   at the moment of decision, where it removes a reason to leave and look. */
.cta-note { font-size:14px; color:rgba(255,255,255,.62); text-wrap:balance; }
.link-underline { font-weight:700; text-decoration:none; border-bottom:2px solid var(--accent); padding-bottom:2px; transition:opacity .2s ease, border-color .2s ease; }
.link-underline:hover { opacity:.72; }

/* ---- Realistic device mockup ---- The elevation shadow is BAKED into the
   PNG's alpha (semi-transparent black pixels), not applied with a CSS filter.
   Reason: filter:drop-shadow (and, on the shared layer, box-shadow) promotes a
   large image to a GPU render surface that Chrome rasterizes in tiles; when the
   bitmap is also heavily downscaled, the tile seams show as a faint horizontal
   line that MOVES as the layout width changes. A plain <img> with no filter is
   downscaled by the normal high-quality image path and never seams. The images
   are exported at ~2.4x their display size (not 3x+) to keep that downscale
   gentle. A baked semi-transparent shadow also composites correctly over both
   the light and dark section backgrounds, unlike a fixed-colour box-shadow. */
.mock { width:100%; display:block; }

/* ---- HERO ---- */
/* overflow:hidden lives on .hero-media, NOT on .hero. On .hero it clipped the
   pace card by a viewport-dependent amount (measured 0px at 1440x900, 114px at
   1440x700, 40px at 390x844) — and on mobile it cancelled the -40px bleed the
   card's own margin asks for exactly, so the intended overlap into the showcase
   never rendered anywhere. The media is absolutely positioned and object-fit
   cover, so it cannot overflow on its own; clipping it there is sufficient.
   z-index keeps the bleeding card above the following section's background. */
.hero { position:relative; z-index:2; min-height:92vh; display:flex; align-items:center; background:var(--dark-bg); }
.hero-media { position:absolute; inset:0; z-index:0; overflow:hidden; }
.hero-media img { width:100%; height:100%; object-fit:cover; object-position:72% 40%; filter:grayscale(100%) contrast(1.05); }
.hero-media::after { content:""; position:absolute; inset:0; background:
    linear-gradient(90deg, rgba(6,6,8,.86) 0%, rgba(6,6,8,.6) 40%, rgba(6,6,8,.35) 70%, rgba(6,6,8,.55) 100%),
    linear-gradient(0deg, rgba(6,6,8,.75) 0%, rgba(6,6,8,0) 42%); }
.hero .wrap { position:relative; z-index:2; width:100%; padding-top:calc(96px + env(safe-area-inset-top)); padding-bottom:8vh; }
.hero-copy { max-width:min(62%, 780px); color:#fff; }
.hero h1 { color:#fff; }
.hero .lead { color:rgba(255,255,255,.82); margin-top:26px; max-width:26ch; }
.hero-actions { margin-top:40px; display:flex; align-items:center; gap:24px; flex-wrap:wrap; }
/* balance: at phone widths this runs to two lines, and greedy wrapping left
   "account." alone on the second. */
.hero-note { margin-top:20px; font-size:14px; color:rgba(255,255,255,.62); text-wrap:balance; max-width:44ch; }
.hero .link-underline { color:#fff; }

/* Hero phone — the pace screen (the 4:59 crown jewel) as a single rounded
   screen card, fully visible, sitting in the runner's negative space and
   bleeding gently down toward the dial card in the showcase below. */
/* Width is capped by viewport HEIGHT as well as width. The card is a 0.46 aspect
   portrait screen, so a purely width-driven size overruns a short desktop window
   (at 1440x700 it ran 114px past the hero). calc(42vh - 62px) is that constraint
   solved for width: 108px top offset + height + 24px clearance <= 92vh. */
.hero-cards { position:absolute; z-index:2; right:max(3vw, calc((100vw - var(--maxw))/2 + var(--gutter))); top:108px; width:min(29vw, 330px, calc(42vh - 62px)); pointer-events:none; }
.card-float { display:block; width:100%; border-radius:36px; box-shadow:0 60px 110px -34px rgba(0,0,0,.72), 0 10px 28px rgba(0,0,0,.4); outline:1px solid rgba(255,255,255,.08); }

/* ---- SHOWCASE ("One number in") — dial card + copy, the input side ---- */
/* Bottom padding was clamp(56px,8vw,104px) and .tabs' top was clamp(48px,7vw,96px),
   which summed to a measured 200px of empty greige between the dial card and the
   "THE APP" eyebrow on desktop — the two sections read as unrelated rather than
   as input followed by output. Halved to ~120px; they are still clearly separate
   sections, just no longer strangers. */
/* z-index is the fix for the hard horizontal cut in the dial card's shadow, and
   it took measuring the pixels to find rather than guessing at the shadow values.
   The card's shadow reaches 94px below it (44 offset + 90 blur - 40 spread) but
   .showcase only has 64px of bottom padding left, so the last 30px overflowed into
   .tabs — and .tabs is `position:relative` with an opaque background, later in DOM
   order, so it painted straight over it. Measured at the card's centre column: the
   shadow ramps 1 level per ~3 rows down to (225,225,220) at y=890, then jumps to
   the page colour (234,234,229) in a single row at y=891, which is exactly
   .showcase's bottom edge and .tabs' top edge. A 9-level step in one row is the
   cut. Raising .showcase above .tabs lets the tail finish. Note what does NOT fix
   it: tuning the shadow (the cut just moves), and adding bottom padding (it works
   but re-opens the dead gap this section was tightened to close). .hero is already
   z-index 2 for the same class of reason, so this stays consistent with it. */
.showcase { background:var(--bg); padding-block:clamp(96px,15vw,184px) clamp(40px,5vw,64px); position:relative; z-index:1; }
.showcase .wrap { max-width:1080px; display:grid; grid-template-columns:0.78fr 1fr; gap:clamp(28px,5vw,72px); align-items:center; }
/* The shadow is cast by this wrapper box, NOT the <img> inside it — a box-shadow
   on the downscaled bitmap tiles on the GPU and seams (horizontal cut across the
   shadow). The img just fills the box with matching rounded corners. */
/* The aspect ratio is inherited from the bitmap this replaced, so swapping to
   markup did not move anything below it on the page. Everything inside is sized
   in cqw against the card's own inline size, which is the only way one set of
   numbers survives both the 330px desktop render and the 240px mobile one — the
   card is a scale model, so its type has to scale with it, not with the viewport. */
.showcase .dial-card {
  justify-self:center; width:min(90%, 330px); aspect-ratio:680/925;
  container-type:inline-size;
  border-radius:38px;
  /* No `outline`. It paints OUTSIDE the border box, so it drew a dark grey line
     immediately outside the white glass hairline and cancelled it — the hairline
     reads as light catching an edge, and a dark line one pixel further out reads as
     a stroke around a sticker. The card's own background (#FBFBFA against the
     #EAEAE5 page) plus the cast shadow give it all the silhouette it needs. */
  background:linear-gradient(170deg, #FBFBFA 0%, #F1F1EF 48%, #E7E7E4 100%);
  /* The inset white ring is the app's glass hairline. Every Card in the app carries
     a light top edge from .glassSurface, and without it the replica read as a flat
     rectangle next to three real screenshots that have one. It is brightest along
     the top edge, because that is where a glass surface catches the light, and the
     dark `outline` stays for the silhouette against the greige. */
  box-shadow:
    0 44px 90px -40px rgba(0,0,0,.42),
    0 8px 24px rgba(0,0,0,.14),
    inset 0 0 0 1px rgba(255,255,255,.55),
    inset 0 1px 0 rgba(255,255,255,.95);
  color:#121314; text-align:center; overflow:hidden;
}
/* The card is a light UI surface in BOTH themes, exactly as the three mockups
   beside it are. Recolouring it for dark mode would be showing the visitor a
   screen the app does not open on (light is the app's default appearance). */
/* Padding lives here, never on .dial-card — see the note in index.html. */
.showcase .dial-body { height:100%; display:flex; flex-direction:column; padding:6.5cqw; }
.showcase .dial-face { position:relative; flex:1; display:flex; flex-direction:column; align-items:center; justify-content:flex-start; padding-top:6cqw; }
.showcase .dial-eyebrow { margin:0; font-size:3.5cqw; font-weight:700; letter-spacing:.14em; text-transform:uppercase; color:#6A6A6E; }
.showcase .dial-race { margin:1.8cqw 0 0; font-size:5.4cqw; font-weight:700; letter-spacing:-0.01em; }
.showcase .dial-time { margin:1cqw 0 0; display:flex; align-items:baseline; gap:.8cqw; line-height:.95; }
/* tabular-nums is load-bearing, not a nicety: the scroll scrub swaps this string
   every frame, and proportional digits would make the whole numeral twitch
   sideways on every minute change. */
.showcase .dial-time b { font-size:23cqw; font-weight:900; letter-spacing:-0.04em; }
.showcase .dial-time span { font-size:7cqw; font-weight:700; }
/* The scrub rail. Ticks are UNIFORM in width and opacity, and the fade comes from
   a mask on the container — which is how the app draws it (checked against a
   simulator capture: constant tick length, opacity ramping from faint at the ends
   to solid in the middle). The first version varied each tick's width via
   nth-child, which looked close when static and was structurally wrong: the
   emphasis was baked into the ticks, so translating them dragged the highlight
   along and the rail could not travel. With the ramp in a mask the ticks slide
   under a fade that stays put, which is what a real dial does. The strip is taller
   than the window it shows through, so there is always travel in both directions. */
/* Two elements, not one, and that split is the whole point: the MASK must not move.
   Putting the mask and the transform on the same element made the fade travel with
   the ticks, which is visually identical to the ticks not moving at all. */
/* Inset rather than spanning the full face. Measured against the app: its ticks
   cover ~55% of the card's height and start about a third of the way down, level
   with the numeral, not up at the eyebrow. Running the rail edge to edge made the
   card read as a ruler. */
.showcase .dial-rail {
  position:absolute; right:0; top:24%; bottom:12%; width:5cqw; overflow:hidden; pointer-events:none;
  -webkit-mask-image:linear-gradient(180deg, transparent 0%, #000 28%, #000 72%, transparent 100%);
          mask-image:linear-gradient(180deg, transparent 0%, #000 28%, #000 72%, transparent 100%);
}
.showcase .dial-ticks {
  position:absolute; inset:0; display:flex; flex-direction:column;
  align-items:flex-end; justify-content:center; gap:6.4cqw;
  transform:translateY(var(--dial-scrub, 0px));
}
.showcase .dial-ticks i { display:block; flex:none; width:5cqw; height:1.2cqw; border-radius:9999px; background:#121314; opacity:.72; }
.showcase .dial-modes { display:flex; align-items:center; gap:.4cqw; padding:1.2cqw; border-radius:9999px; background:rgba(18,19,20,.055); font-size:4.1cqw; font-weight:700; }
.showcase .dial-modes span { flex:1; padding:2.3cqw .4cqw; border-radius:9999px; color:#4E4E52; white-space:nowrap; }
.showcase .dial-modes .on { background:var(--accent); color:var(--on-accent); box-shadow:0 1px 3px rgba(0,0,0,.16); }
.showcase .dial-done { margin:2.8cqw 0 0; padding:4.2cqw 0; border-radius:9999px; background:#121314; color:#fff; font-size:5.4cqw; font-weight:900; letter-spacing:-0.01em; }
.showcase h2 { margin-bottom:22px; }
.showcase-copy .eyebrow { display:block; margin-bottom:20px; }
.showcase .kicker { font-size:clamp(18px,2vw,22px); line-height:1.5; color:var(--ink-secondary); max-width:30ch; }
.showcase .kicker strong { color:var(--ink); font-weight:700; }

/* ---- THREE TABS cluster ---- */
.tabs { background:var(--bg); padding-block:clamp(36px,4vw,56px) clamp(56px,8vw,104px); }
.tabs .head { max-width:760px; margin-bottom:clamp(28px,4vw,52px); }
.tabs .head .eyebrow { display:block; margin-bottom:16px; }
.tabs .head-sub { margin-top:20px; font-size:clamp(17px,1.9vw,21px); color:var(--ink-secondary); line-height:1.5; max-width:40ch; }
.cluster { display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(16px,2.5vw,40px); align-items:start; }
.tab { text-align:left; }
.tab .mock { max-width:360px; margin:0 auto; }
/* The middle column used to carry transform:translateY(-40px) as a desktop
   flourish. Removed, for two reasons. (1) It dragged the caption up with it, so
   the three labels sat at 2889 / 2849 / 2889 — three parallel captions on
   different baselines read as different hierarchy levels rather than as a set.
   (2) Elevating one of three reads as a claim that that one is primary, and
   Training is not; it is the same category error as drawing mutually exclusive
   race distances as a connected ladder (see CLAUDE.md). It was also already
   switched off below 900px, so it only ever existed on wide desktop. */
/* Negative top margin tucks the caption up into the phone's baked shadow padding
   so the label clearly belongs to the phone ABOVE it, not the one below. The gap
   to the next phone (cluster gap) is kept ~3x larger — see the mobile block. */
.tab .label { margin-top:-14px; font-weight:900; font-size:clamp(21px,2.4vw,26px); letter-spacing:-0.02em; }
.tab .desc { margin-top:10px; font-size:clamp(15px,1.5vw,17px); color:var(--ink-secondary); line-height:1.5; max-width:32ch; }

/* ---- MOTION BAND (full-bleed photo break between features and science) ---- */
/* This section is DARKENED WITH A TONE CURVE, not veiled with a translucent
   overlay, and the difference is the whole point. The photograph is a mid-key
   frame (mean luminance 112/255 — light pavement), so white type needs the field
   pushed dark. A rgba black overlay does that by compressing every value toward
   black, which kills local contrast: the previous radial scrim (.42 centre to .8
   edge) took a legible runner and rendered a flat dark texture in which nothing
   could be made out, at a cost of 524KB. brightness() scales luminance down while
   contrast() re-expands the local differences, so the legs stay legible as legs
   against the pavement AND the field is dark enough for type. The remaining
   gradient is deliberately light — it only shapes attention toward the centre. */
.band { position:relative; min-height:clamp(440px,62vh,660px); display:flex; align-items:center; justify-content:center; text-align:center; overflow:hidden; background:var(--dark-bg); }
.band-media { position:absolute; inset:0; z-index:0; }
.band-media img { width:100%; height:100%; object-fit:cover; object-position:50% 50%; filter:grayscale(100%) brightness(.55) contrast(1.18); }
.band-media::after { content:""; position:absolute; inset:0; background:
    radial-gradient(120% 100% at 50% 50%, rgba(6,6,8,0) 0%, rgba(6,6,8,.42) 100%),
    linear-gradient(0deg, rgba(6,6,8,.28) 0%, rgba(6,6,8,0) 55%); }
.band .wrap { position:relative; z-index:2; }
/* Slightly smaller than the other poster headlines (was clamp(38px,7vw,92px)).
   With the photograph visible this section is a photographic beat with a caption
   rather than a third full-scale type poster in a row of type posters, which is
   what it was designed to be before the scrim buried the image. */
.band h2 { color:#fff; font-size:clamp(34px,6vw,76px); max-width:15ch; margin-inline:auto; text-wrap:balance; text-shadow:0 2px 30px rgba(6,6,8,.5); }

/* ---- SCIENCE (statement, not fine print) ---- */
.science { background:var(--bg); padding-block:clamp(56px,8vw,104px); }
.science h2 { max-width:15ch; }
.science .credits { margin-top:clamp(40px,5vw,64px); display:grid; grid-template-columns:repeat(3,1fr); gap:clamp(24px,4vw,56px); }
.science .credit { border-top:2px solid var(--ink); padding-top:18px; }
.science .credit .k { font-weight:900; font-size:clamp(18px,2vw,22px); letter-spacing:-0.015em; }
.science .credit .v { margin-top:8px; font-size:clamp(15px,1.5vw,17px); color:var(--ink-secondary); line-height:1.45; }

/* ---- PRIVACY (dark band) ---- */
/* --band-bg, not --dark-bg: see the token comment. The hairlines are what make
   this read as a band in dark mode, where no tonal step is large enough on its
   own; in light mode they sit invisibly inside a 16.6:1 edge. */
.privacy { background:var(--band-bg); color:var(--dark-ink); padding-block:clamp(80px,12vw,150px); border-block:1px solid transparent; }
@media (prefers-color-scheme: dark) { .privacy { border-block-color:var(--line); } }
.privacy h2 { max-width:16ch; }
.privacy .lead { color:var(--dark-sec); margin-top:24px; max-width:46ch; }
.privacy .link-underline { color:var(--dark-ink); display:inline-block; margin-top:36px; }

/* ---- CLOSE (motion bookend) ---- */
.close { position:relative; min-height:74vh; display:flex; align-items:center; justify-content:center; text-align:center; overflow:hidden; background:var(--dark-bg); }
.close-media { position:absolute; inset:0; z-index:0; }
/* Same tone-curve treatment as the band, and needed more badly: this is the
   brightest source frame of the three (mean 188/255), so with only a scrim it
   rendered mid-grey while every other dark section is near-black — in dark mode
   it measured as the LIGHTEST region on the page, which lifts the tonal arc at
   the exact point the page should land hardest. */
/* Brightness DOWN, contrast well UP, which is the opposite of the instinct. The
   earlier .5/1.05 landed the section at the right luminance but read as dark noise
   rather than as a photograph: you could not tell it was a runner. The lever that
   fixes that is contrast, not exposure — 1.34 re-separates the torso, the shoulder
   and the arm's motion blur from the field while brightness .46 keeps white type
   at full legibility. Rejected on measured evidence, not taste:
     - Inverting the section to high-key (the source is a bright hazy frame, mean
       188/255, so using it near its native exposure is tempting) puts dark type
       straight onto the dark torso. The figure is centred and the source is
       portrait, so object-position cannot move it sideways under a cover fit —
       there is no crop that clears the headline. "Start" and "fin" were swallowed.
     - Cropping to the legs (74%) makes the frame ambiguous: strong texture, no
       identifiable runner.
   Keep this curve in step with .band-media's: same technique, gentler numbers,
   because this subject is soft where the band's is sharp. */
.close-media img { width:100%; height:100%; object-fit:cover; object-position:50% 34%; filter:grayscale(100%) brightness(.46) contrast(1.34); }
/* A light scrim on TOP of the tone curve, not instead of it. The curve does the
   work of keeping the photograph readable; this only seats the top and bottom
   edges against the sections either side. */
.close-media::after { content:""; position:absolute; inset:0; background:linear-gradient(0deg, rgba(6,6,8,.45), rgba(6,6,8,.25)); }
.close .wrap { position:relative; z-index:2; }
.close h2 { color:#fff; font-size:clamp(44px,8vw,104px); text-wrap:balance; }
.close .hero-actions { justify-content:center; margin-top:36px; }
.close .cta-note { margin-top:22px; }

/* ---- Footer ---- */
.site-footer { background:var(--bg); border-top:1px solid var(--line); padding-block:44px; font-size:14px; color:var(--ink-secondary); }
.site-footer .wrap { display:flex; flex-wrap:wrap; gap:18px 40px; align-items:center; justify-content:space-between; }
.site-footer nav { display:flex; gap:26px; flex-wrap:wrap; }
.site-footer a { text-decoration:none; color:var(--ink-secondary); transition:color .2s ease; }
.site-footer a:hover { color:var(--ink); }
.footer-brand { color:var(--ink); }

/* ---- Long-form (support / privacy pages) ---- */
.page-pad { padding-top:calc(78px + env(safe-area-inset-top)); }
.doc { max-width:720px; }
.doc .eyebrow { display:block; margin-bottom:14px; }
.doc h1 { font-size:clamp(34px,6vw,54px); line-height:1.02; margin-bottom:12px; }
.doc .updated { color:var(--ink-secondary); font-size:15px; margin-bottom:40px; }
.doc h2 { font-size:clamp(22px,3vw,28px); margin-top:48px; margin-bottom:14px; line-height:1.1; }
.doc p,.doc li { color:var(--ink); line-height:1.65; font-size:17px; }
.doc p + p { margin-top:16px; }
.doc ul { margin:14px 0; padding-left:22px; }
.doc li { margin-bottom:8px; }
.doc a { color:var(--ink); text-decoration:underline; text-decoration-color:var(--accent); text-underline-offset:3px; }
.doc .lede { font-size:20px; line-height:1.6; margin-bottom:8px; }
/* Must match .doc exactly. At 760px against .doc's 720px, both auto-centred, the
   FAQ list started 20px to the left of the intro paragraph above it — a visible
   ragged left edge down the page for no reason. */
.faq { max-width:720px; }
.faq details { border-top:1px solid var(--line); padding:22px 0; }
.faq details:last-of-type { border-bottom:1px solid var(--line); }
.faq summary { cursor:pointer; list-style:none; font-weight:700; font-size:19px; letter-spacing:-0.01em; display:flex; justify-content:space-between; align-items:center; gap:20px; }
.faq summary::-webkit-details-marker { display:none; }
.faq summary::after { content:"+"; font-weight:400; font-size:26px; color:var(--ink-secondary); transition:transform .28s cubic-bezier(.22,.61,.36,1); }
.faq details[open] summary::after { transform:rotate(45deg); }
.faq details p { margin-top:14px; color:var(--ink-secondary); font-size:16px; line-height:1.6; }

/* ---- The answer opening and closing ----
   A native <details> cannot animate either direction: it lays out at full height
   on the frame it opens, and it deletes its content on the frame it closes. The
   CSS-only version here could therefore only fade the paragraph in ON TOP OF that
   instant jump — which is what read as janky, because the fade says "arriving"
   over 300ms while the rule below it has already snapped the answer's full height
   (91px, measured) down the page — and it had nothing at all to offer on close.

   support.html's script owns the open attribute instead and transitions a measured
   height. Two consequences for these rules:
     - The open state is a CLASS, not [open]. While a close animates, the element
       is still [open] (it has to be, or the content vanishes mid-animation), so
       anything keyed on [open] would flip a step early. The + mark is the visible
       case, hence the override below.
     - Everything is gated behind .js-faq, which the script puts on <html> only
       when it is about to run. No JS, a parse error, or prefers-reduced-motion all
       fall through to the native disclosure plus the old fade, which is correct on
       its own and is why that rule is kept rather than deleted. */
html:not(.js-faq) .faq details[open] p { animation:faqIn .3s cubic-bezier(.22,.61,.36,1) both; }
@keyframes faqIn { from { opacity:0; transform:translateY(-5px); } }
@media (prefers-reduced-motion: reduce) { html:not(.js-faq) .faq details[open] p { animation:none; } }

.js-faq .faq-body {
  height:0; overflow:hidden; opacity:0;
  /* Collapsing is quicker than opening, and the text fades out well ahead of the
     clip so nothing is still legible as the edge sweeps over it (measured: 5%
     opacity at 127ms, with 60px of the answer still on screen). No translateY:
     the clip is already the movement, and a second one under it is the doubled
     motion that made the old fade feel loose.

     A gradient mask to soften the clip edge was tried and dropped: the fade zone
     sits over the answer's last line once the box reaches full height, so removing
     the mask at the end of the transition pops that line to full opacity, and
     keeping it means permanently fading the last line of every open answer. The
     opacity ramp above does the same job with no end state to unwind. */
  transition:height .26s cubic-bezier(.4,0,1,1), opacity .14s linear;
}
.js-faq .faq details.is-open .faq-body {
  opacity:1;
  transition:height .32s cubic-bezier(.22,.61,.36,1), opacity .22s ease-out .06s;
}
.js-faq .faq details[open]:not(.is-open) summary::after { transform:none; }

/* ---- Scroll reveal ----
   One gesture, used once per content block: a 18px rise out of transparent as the
   block enters. Deliberately not applied to the hero — the hero must be fully
   painted on the first frame, and fading in the headline and the primary CTA
   would trade conversion for decoration. Reveals therefore start at the showcase.
   Gated behind .js-reveal on <html>, which the script adds only when it is about
   to run: without JS, or before it executes, nothing is hidden and the page is
   complete. That ordering is the whole safety story, so do not move the opacity
   rule out from under .js-reveal. */
.js-reveal .reveal { opacity:0; transform:translateY(18px); will-change:opacity, transform; }
.js-reveal .reveal.in {
  opacity:1; transform:none;
  transition:opacity .65s cubic-bezier(.22,.61,.36,1), transform .65s cubic-bezier(.22,.61,.36,1);
  transition-delay:var(--reveal-delay, 0ms);
}
.js-reveal .reveal.in { will-change:auto; }
@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal, .js-reveal .reveal.in { opacity:1; transform:none; transition:none; }
}

/* ---- Responsive ---- */
/* Tablet / phone-landscape band (721–900px). This range is still wide enough
   for the two-column hero and showcase, so we DON'T collapse them here (that
   happens at 720). We only undo the two desktop flourishes that break at this
   width: the top-pinned hero card (leaves a dead band below it) and the middle
   tab's upward stagger (collides with the section heading). */
@media (max-width: 900px){
  /* 92vh is too tall once the layout is this narrow: at 820x1180 it produced a
     1085px hero with a measured 320px of empty dark below the copy block, which
     reads as a missing section rather than as breathing room. Capping the pixel
     height keeps the proportion sane on tall tablets without touching desktop. */
  .hero { min-height:min(84vh, 800px); }
  .hero-copy { max-width:66%; }
  /* Center the pace card vertically so it sits beside the centered copy rather
     than pinning to the top and orphaning a dark band beneath it. Height-capped
     for the same reason as the desktop rule: at short heights a 33vw card is
     taller than the hero it is centred in. */
  .hero-cards { width:min(33vw, calc(40vh - 40px)); right:2vw; top:50%; transform:translateY(-50%); }
  .tabs .head { margin-bottom:clamp(36px,5vw,56px); }
}
/* Phone landscape only (short viewport): the vertically-centered hero card
   rides up out of the top because it's taller than the short hero. Anchor it
   near the top there instead. Height-gated so it can't affect portrait tablets
   (tall) or desktop. Very low-priority surface. */
@media (max-width: 900px) and (max-height: 640px){
  .hero-cards { top:88px; transform:none; }
}
@media (max-width: 720px){
  body { font-size:16px; }
  .hero { min-height:auto; display:block; }
  .hero .wrap { padding-top:84px; padding-bottom:0; }
  .hero-copy { max-width:100%; }
  .hero-cards { position:relative; right:auto; top:auto; bottom:auto; transform:none; width:min(62vw,250px); margin:32px auto -40px; }
  /* Much lighter here, and it is not a taste call. The desktop shadow (72% black,
     110px blur) was calibrated against the dark hero photograph, where that weight
     is nearly invisible. On phones the card stops floating on the photo and bleeds
     40px onto the light greige, so the identical shadow lands as a large dark cloud
     over the top of the next section. Same shadow, different backdrop, so it needs
     different numbers. The bleed itself stays — it is what ties the two sections
     together — this only stops it dimming the section it bleeds into. */
  .card-float { box-shadow:0 30px 58px -28px rgba(0,0,0,.40), 0 6px 16px rgba(0,0,0,.20); }
  .showcase { padding-top:88px; }
  .showcase .wrap { grid-template-columns:1fr; gap:clamp(28px,6vw,48px); }
  .showcase .dial-card { width:min(64vw,240px); justify-self:center; }
  .cluster { grid-template-columns:1fr; gap:60px; }  /* ~3x the phone-to-caption gap */
  .tab { text-align:center; }
  .tab .mock { max-width:330px; }
  /* Portrait band crops to a vertical slice; bias left so the trailing shoe stays in frame. */
  .band-media img { object-position:22% 50%; }
  .band-media::after { background:
      radial-gradient(120% 90% at 50% 42%, rgba(6,6,8,0) 0%, rgba(6,6,8,.34) 100%),
      linear-gradient(0deg, rgba(6,6,8,.2) 0%, rgba(6,6,8,0) 55%); }
  .tab .desc { margin-inline:auto; }
  .science .credits { grid-template-columns:1fr; gap:0; }
  .science .credit { border-top:none; padding-top:0; }
  .science .credit + .credit { margin-top:26px; }
}
