/* ---------- Tokens ---------- */
:root {
  --color-bg: #000000;
  --color-fg: #ffffff;
  --color-fg-muted: rgba(255, 255, 255, 0.68);
  /* 0.45 measured 4.43:1 on #000, just under the 4.5 AA floor for small
     text. 0.475 measures 4.82:1. One token, three fixed uses: the App
     Store line, the copyright, and the FatSecret credit. */
  --color-fg-faint: rgba(255, 255, 255, 0.475);
  --color-accent: #35c2fd;
  --color-accent-soft: rgba(53, 194, 253, 0.14);
  --color-border: rgba(255, 255, 255, 0.12);
  --color-border-soft: rgba(255, 255, 255, 0.07);
  --color-surface: rgba(255, 255, 255, 0.04);
  --color-surface-2: rgba(255, 255, 255, 0.06);

  --font-display: 'Archivo', 'Arial Narrow', sans-serif;
  --font-body: 'Manrope', -apple-system, BlinkMacSystemFont, sans-serif;

  --container: 1200px;
  --gutter: clamp(20px, 5vw, 64px);

  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in: cubic-bezier(0.7, 0, 0.84, 0);
  --ease-back: cubic-bezier(0.34, 1.56, 0.64, 1);
  --dur-fast: 200ms;
  --dur-hover: 250ms;
  --dur-med: 500ms;
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--color-bg);
  color: var(--color-fg);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
img { max-width: 100%; display: block; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; margin: 0; padding: 0; }
h1, h2, h3, p { margin: 0; }
button { font: inherit; }

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0,0,0,0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  background: var(--color-accent);
  color: #001018;
  padding: 10px 16px;
  border-radius: 8px;
  font-weight: 600;
  z-index: 200;
  transition: top var(--dur-fast) var(--ease-out);
}
.skip-link:focus {
  top: 12px;
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
  border-radius: 4px;
}

section { position: relative; }
section[id] { scroll-margin-top: 96px; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 44px;
  padding: 0 24px;
  border-radius: 999px;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.95rem;
  border: none;
  cursor: pointer;
  transition: transform var(--dur-fast) var(--ease-out), background var(--dur-fast) var(--ease-out);
}
.btn-accent {
  background: var(--color-accent);
  color: #001018;
}
.btn-accent:hover:not(:disabled) {
  background: #57cdfd;
}
.btn-accent:active:not(:disabled) {
  transform: scale(0.97);
}
/* Nothing to download yet. Hold the brand colour and wash it back rather than
   dropping to grey: a faded accent reads as "not yet", a grey slab reads as
   broken. The disabled attribute does the real work, keeping these out of the
   tab order instead of merely looking inert. */
.btn:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

/* ---------- Press feedback ----------
   Acknowledge the press itself, not the release: waiting for the click to show
   anything reads as dead. Until now the only :active rule in this file was
   .btn-accent's, and it excludes :disabled, so with every Download button
   disabled nothing on the page responded to being pressed at all.

   Deliberately not covering .extra-card: those cards have no click behaviour,
   and a pressed state on something inert promises an affordance that is not
   there. The disabled buttons stay inert for the same reason. */
.step-card:active:not(.is-active) {
  background: var(--color-surface-2);
  border-color: var(--color-fg-muted);
}
.nav-links a:active { opacity: 0.55; }
.nav-toggle:active { background: rgba(255, 255, 255, 0.14); }
.btn-small {
  min-height: 38px;
  padding: 0 18px;
  font-size: 0.85rem;
}

/* ---------- Nav ---------- */
.nav {
  position: fixed;
  top: 16px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 100;
  width: min(920px, calc(100% - 32px));
  animation: fade-in 1000ms ease 1500ms both;
}
.nav-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
  padding: 10px 10px 10px 20px;
  background: rgba(10, 10, 12, 0.6);
  backdrop-filter: blur(16px);
  -webkit-backdrop-filter: blur(16px);
  border: 1px solid var(--color-border-soft);
  border-radius: 999px;
}
.nav-logo img { height: 22px; width: auto; }
.nav-links {
  position: relative;
  display: flex;
  align-items: center;
  gap: 28px;
}
/* These sit on a translucent pill that passes over the phone screenshots, so
   flat grey is the wrong call: text over a material needs more contrast than it
   would on a solid background. The small positive tracking earns its place
   twice, since 14px text wants slightly looser letterspacing anyway. */
.nav-links a {
  position: relative;
  font-size: 0.9rem;
  font-weight: 600;
  color: rgba(255, 255, 255, 0.78);
  letter-spacing: 0.01em;
  padding: 6px 2px;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav-links a:hover { color: var(--color-fg); }
.nav-links a.is-active { color: var(--color-fg); }
/* Fixed 100px reference width, scaled by script.js, rather than animating the
   width itself: width is a layout property and would run layout on every one
   of the 400ms of frames. transform and opacity stay on the compositor. */
.nav-indicator {
  position: absolute;
  left: 0;
  bottom: -6px;
  height: 2px;
  width: 100px;
  transform-origin: 0 50%;
  border-radius: 999px;
  background: var(--color-accent);
  opacity: 0;
  transform: translateX(0) scaleX(0);
  transition: transform 400ms var(--ease-out), opacity var(--dur-fast) var(--ease-out);
}
.nav-indicator.is-active { opacity: 1; }

/* Hamburger. display:none until 900px, so desktop never sees it and the
   flex gap never reserves space for it. */
.nav-toggle {
  display: none;
  order: 3;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  border: 1px solid var(--color-border-soft);
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out);
}
.nav-toggle:hover { background: rgba(255, 255, 255, 0.06); }
.nav-toggle-box {
  position: relative;
  display: block;
  width: 16px;
  height: 10px;
}
.nav-toggle-bar {
  position: absolute;
  left: 0;
  width: 100%;
  height: 1.5px;
  border-radius: 999px;
  background: var(--color-fg);
  transition: transform 280ms var(--ease-out), opacity 160ms var(--ease-out);
}
.nav-toggle-bar:nth-child(1) { top: 0; }
.nav-toggle-bar:nth-child(2) { bottom: 0; }
/* Bars converge on the centre line, then cross into an X. */
.nav[data-nav-open] .nav-toggle-bar:nth-child(1) { transform: translateY(4.25px) rotate(45deg); }
.nav[data-nav-open] .nav-toggle-bar:nth-child(2) { transform: translateY(-4.25px) rotate(-45deg); }

/* ---------- Layout helpers ---------- */
.hero-inner, .compare-inner, .footer-inner {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.eyebrow {
  font-size: 1rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin-bottom: 16px;
}

.section-headline {
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 110%;
  font-size: clamp(1.9rem, 3.6vw, 2.9rem);
  line-height: 1.12;
  /* Tracking is size-specific. Letters read further apart as they grow, so the
     larger a tier runs the tighter it needs to be. This tier tops out at 46px. */
  letter-spacing: -0.016em;
  max-width: 18ch;
}

.section-sub {
  margin-top: 18px;
  font-size: 1.05rem;
  line-height: 1.6;
  color: var(--color-fg-muted);
  max-width: 46ch;
}

/* ---------- Reveal animation ---------- */
[data-reveal] {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity var(--dur-med) var(--ease-out), transform var(--dur-med) var(--ease-out);
}
[data-reveal].is-visible {
  opacity: 1;
  transform: translateY(0);
}
[data-reveal-group] > [data-reveal]:nth-child(1) { transition-delay: 0ms; }
[data-reveal-group] > [data-reveal]:nth-child(2) { transition-delay: 80ms; }
[data-reveal-group] > [data-reveal]:nth-child(3) { transition-delay: 160ms; }
[data-reveal-group] > [data-reveal]:nth-child(4) { transition-delay: 240ms; }


/* ---------- Hero ----------
   Pin mechanic: the logo lives in a zero-height sticky box pinned at 50vh,
   while the phone scrolls past it in normal flow. The phone starts centred
   on the logo and rises away, revealing the wordmark underneath. */
.hero {
  position: relative;
  /* The pinned frame of the next section carries its own 96px top padding, so
     this only needs to top that up to a sensible gap. */
  padding: 0 0 24px;
  /* Reference ratio: phone height is 67% of viewport height. */
  --hero-phone-h: clamp(430px, 67vh, 680px);
  /* Rendered width of the wordmark, and half its height. The logo art is
     6.99:1, and it hangs half its height below the zero-height pin box,
     so the copy below has to reserve that overhang or it collides. */
  --brand-w: min(calc(100vw - (2 * var(--gutter))), 1080px);
  --brand-half-h: calc(var(--brand-w) / 13.98);
}

.hero-stage {
  position: relative;
  height: 135vh;
}

.hero-pin {
  position: sticky;
  top: 50vh;
  height: 0;
  z-index: 1;
}
.brand-lockup {
  position: absolute;
  left: 0;
  right: 0;
  /* The cursor parallax drives --parallax-x rather than overwriting transform,
     because this -50% is what centres the wordmark on the zero-height pin. Set
     transform wholesale from JS and the wordmark drops half its height. */
  transform: translate(var(--parallax-x, 0px), -50%);
  perspective: 1000px;
  display: flex;
  justify-content: center;
  padding: 0 var(--gutter);
}
.brand-flip {
  width: 100%;
  display: flex;
  justify-content: center;
  transform-style: preserve-3d;
}
.brand-scale {
  position: relative;
  width: var(--brand-w);
  display: flex;
  justify-content: center;
}
.brand-logo { width: 100%; height: auto; }
.brand-logo-glow {
  position: absolute;
  inset: 0;
  width: 100%;
  height: auto;
  filter: blur(90px);
  opacity: 0.35;
  pointer-events: none;
}

/* The phone occupies the first viewport-height of the stage and centres
   inside it, so it sits dead-centre on load and scrolls up from there. */
.hero-phone {
  position: relative;
  z-index: 2;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}
.phone-figure { position: relative; }
.hero-phone-shot {
  display: block;
  height: var(--hero-phone-h);
  width: auto;
}
.phone-shadow {
  position: absolute;
  inset: 12% -14%;
  background: #000;
  filter: blur(64px);
  z-index: -1;
  pointer-events: none;
}

/* The wordmark is the headline, so what sits under it stays quiet: one line
   and one button. */
.hero-copy {
  text-align: center;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The --brand-half-h term is not optional: the wordmark sits in a zero-height
     sticky box and hangs half its height below it, so without this the line
     collides with the logo. The constant on top is the visible gap. */
  padding-top: calc(var(--brand-half-h) + 32px);
}
.hero-lede {
  margin: 0 0 30px;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: clamp(1.25rem, 2vw, 1.7rem);
  line-height: 1.35;
  letter-spacing: -0.01em;
  color: var(--color-fg);
}
.coming-soon {
  margin-top: 14px;
  font-size: 0.8rem;
  color: var(--color-fg-faint);
}

/* ---------- Hero proof row (currently not rendered) ----------
   The markup was pulled out of index.html; these rules and the marquee code in
   script.js are kept intact so the strip can be dropped back in as-is. Both are
   inert while no .proof-row exists in the document. */
.proof-row {
  margin-top: 76px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;
  width: 100%;
}
/* Full-bleed out of the 1200px column, clipping the track. The mask feathers
   both ends so marks dissolve into the black instead of popping at a hard
   edge. */
.proof-marquee {
  width: 100vw;
  margin-left: calc(50% - 50vw);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(to right, transparent, #000 12%, #000 88%, transparent);
}
/* Travels exactly half its own width. Because script.js fills the track with
   an even number of identical copies, the halfway point is pixel-identical to
   the start, so the loop has no seam. */
.proof-track {
  display: flex;
  align-items: center;
  width: max-content;
  animation: marquee-scroll 40s linear infinite;
}
@keyframes marquee-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
.proof-mark {
  flex: none;
  display: flex;
  justify-content: center;
  padding: 0 34px;
  color: var(--color-fg-faint);
}
.proof-mark svg {
  width: 28px;
  height: 28px;
}
.proof-caption {
  margin: 0;
  font-size: 1rem;
  font-weight: 500;
  color: var(--color-fg-muted);
}

/* Reference reveal: 25px rise over 1000ms with a 100ms hold, staggered
   left to right across the row. */
[data-reveal-soft] {
  opacity: 0;
  transform: translateY(25px);
  transition: opacity 1000ms ease 100ms, transform 1000ms ease 100ms;
}
[data-reveal-soft].is-visible {
  opacity: 1;
  transform: translateY(0);
}

/* ---------- Hero load choreography ----------
   Sequence: the wordmark flips up from a 60deg X-rotation, its glow blooms in,
   then the phone rises from +120px as the wordmark settles from 1.1 to 1,
   and finally the phone shadow and nav fade in. */
@keyframes brand-flip-in {
  from { opacity: 0; transform: rotateX(60deg); }
  to   { opacity: 1; transform: rotateX(0deg); }
}
@keyframes brand-settle {
  from { transform: scale(1.1); }
  to   { transform: scale(1); }
}
@keyframes brand-glow-in {
  from { opacity: 0; }
  to   { opacity: 0.35; }
}
@keyframes phone-rise {
  0%   { opacity: 0; transform: translateY(120px); }
  50%  { opacity: 1; }
  100% { opacity: 1; transform: translateY(0); }
}
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

.brand-flip      { animation: brand-flip-in 1100ms ease 0ms both; }
.brand-scale     { animation: brand-settle 1100ms ease 1200ms both; }
.brand-logo-glow { animation: brand-glow-in 1800ms ease 400ms both; }
.hero-phone      { animation: phone-rise 1200ms ease 1200ms both; }
.phone-shadow    { animation: fade-in 1000ms ease 1500ms both; }

/* Landing on an anchor skips the entrance, so nothing is mid-animation
   (and the nav is not invisible) when the page opens partway down. */
.deep-link .brand-flip,
.deep-link .brand-scale,
.deep-link .brand-logo-glow,
.deep-link .hero-phone,
.deep-link .phone-shadow,
.deep-link .nav { animation: none; }

/* ---------- Feature sections (scroll-driven steppers) ----------
   The track is taller than the frame pinned inside it. That surplus is the
   scroll budget, split into one band per step: --step-span each. script.js
   reads the same numbers to decide which step is active and where a card
   click should scroll to, so the two can never disagree. */
.feature-section {
  padding: 0;
  --step-span: 70vh;
  --step-count: 3;
}
.stepper-track {
  position: relative;
  height: calc(100vh + (var(--step-count) * var(--step-span)));
}
.stepper-inner {
  position: sticky;
  top: 0;
  height: 100vh;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 60px;
  align-items: center;
  /* Clears the fixed nav (which ends at 76px) without shifting the optical
     centre too far down. */
  padding: 92px var(--gutter) 28px;
  max-width: var(--container);
  margin: 0 auto;
}
.feature-text-right .feature-copy { order: 2; }
.feature-text-right .feature-visual { order: 1; }
.feature-visual { display: flex; justify-content: center; }

/* Shots are stacked on top of each other, so the stage has to reserve the
   phone's footprint itself. Ratio is the cut-out art's own 600x1236. */
.step-stage {
  position: relative;
  width: clamp(210px, 21vw, 280px);
  aspect-ratio: 600 / 1236;
}
.step-shot {
  position: absolute;
  inset: 0;
  opacity: 0;
  pointer-events: none;
  transition: opacity 420ms var(--ease-out), transform 560ms var(--ease-out);
}
/* The captures are real devices cut out of their background, so the art
   carries its own frame. drop-shadow follows that silhouette, unlike the
   hero's blurred box, which needed a separate element. */
.step-shot-img {
  display: block;
  width: 100%;
  height: auto;
  filter: drop-shadow(0 30px 60px rgba(0, 0, 0, 0.55));
}
/* Direction of travel, set by script.js. Scrolling down, the outgoing shot
   exits upward and the next one rises from below; scrolling up reverses it. */
.step-stage[data-dir='down'] .step-shot { transform: translateY(48px); }
.step-stage[data-dir='down'] .step-shot.is-leaving { transform: translateY(-48px); }
.step-stage[data-dir='up'] .step-shot { transform: translateY(-48px); }
.step-stage[data-dir='up'] .step-shot.is-leaving { transform: translateY(48px); }
.step-stage .step-shot.is-active {
  opacity: 1;
  transform: translateY(0);
}

.step-list {
  margin-top: 26px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}
/* The card's whole appearance is one number, --lift, running 0 to 1. script.js
   writes it per scroll frame so the state hands over continuously instead of
   flipping at a band boundary; before this, a card held identical values for
   the full 560px of its band and then changed instantly.

   Marked by material, not by a coloured stroke: a 1px accent line drawn all the
   way around a large rounded card is a lot of saturated colour for a state
   change, and it reads as loud rather than considered. Elevation cannot come
   from a drop shadow either, since over pure black a dark shadow is invisible.
   It comes from surface brightness plus a 1px inner highlight along the top
   edge, the light-catching cue that makes a surface read as a real material.

   At --lift 0 and 1 these evaluate to exactly the previous inactive and active
   values, so only the path between them is new. 0.55 at rest rather than lower:
   dimmer than that pushed the body copy to roughly 0.31 effective white, which
   reads as switched off rather than as a step you have not reached yet. */
.step-card {
  --lift: 0;
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  padding: 16px 22px;
  border: 1px solid;
  border-radius: 16px;
  opacity: calc(0.55 + (0.45 * var(--lift)));
  background: rgba(255, 255, 255, calc(0.04 + (0.035 * var(--lift))));
  border-color: rgba(255, 255, 255, calc(0.12 + (0.1 * var(--lift))));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, calc(0.09 * var(--lift)));
  /* Only filter transitions. The four properties above are driven per scroll
     frame, and easing them would smear the feedback behind the input rather
     than tracking it. No transform either: inside a pinned frame a hover lift
     reads as jitter. */
  transition: filter var(--dur-hover) var(--ease-out);
}
/* Hover moved off opacity, which --lift now owns. brightness is compositor
   friendly, independent of the calc chain, and can keep its own easing. */
.step-card:hover { filter: brightness(1.35); }
/* Keyboard affordance, not decoration: this one keeps the accent. It only ever
   appears when tabbing, which is exactly where a loud colour earns its place. */
.step-card:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 3px;
}
/* The fallback when script.js has not written a value yet, or at all: the card
   marked active in the markup still reads as lifted. An inline --lift from the
   scroll handler overrides this. */
.step-card.is-active {
  --lift: 1;
  cursor: default;
}
/* Sits under the step list, so it lands on whichever side that column is on:
   left in Nutrition, right in Training. */
.feature-cta { margin-top: 24px; }
.extras-cta { margin-top: 48px; }

/* Step dots are a mobile affordance only: desktop shows all three cards at
   once, so it already tells you how many steps there are. */
.step-dots { display: none; }
.step-card h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.05rem;
  margin-bottom: 8px;
}
.step-card p {
  color: var(--color-fg-muted);
  font-size: 0.95rem;
  line-height: 1.55;
}

/* The pinned column is ~700px tall. On a short window it would run out of
   frame, so tighten it rather than let it clip. */
/* The Nutrition column is the tall one: a three-line headline, three cards and
   now a button. It needs ~880px of window to sit in the frame unaided, so step
   it down below that rather than let it run past the bottom edge. */
@media (max-height: 880px) {
  .feature-section .section-headline,
  .extras-heading .section-headline { font-size: clamp(1.6rem, 2.8vw, 2.25rem); }
  .feature-section .section-sub { font-size: 0.98rem; margin-top: 14px; }
  .feature-section .eyebrow { font-size: 0.9rem; margin-bottom: 10px; }
  .step-card { padding: 13px 18px; }
  .step-card p { font-size: 0.9rem; line-height: 1.5; }
  .step-list { gap: 12px; margin-top: 20px; }
  .feature-cta { margin-top: 18px; }
  .stepper-inner { padding: 84px var(--gutter) 20px; }
}
@media (max-height: 720px) {
  .feature-section .section-headline,
  .extras-heading .section-headline { font-size: clamp(1.45rem, 2.4vw, 1.9rem); }
  .step-card { padding: 10px 16px; }
  .step-card p { font-size: 0.86rem; }
  .step-list { gap: 9px; margin-top: 16px; }
  .feature-cta { margin-top: 14px; }
}

/* ---------- Extra feature cards ----------
   The lit border: the card is one pixel larger than its content and painted in
   a lighter colour, so a 1px rim shows around the edge. A big blurred white
   blob sits between the card background and the (opaque) content and tracks the
   cursor, which means it can only ever be seen through that rim. The border
   lights up wherever the pointer is. script.js drives the tracking. */
.extras {
  padding: 120px 0;
  --card-rim: rgba(255, 255, 255, 0.16);
  --card-bg: #0b0c0e;
}
.extras-inner {
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter);
}
/* Left-aligned, matching Nutrition and Training. This section holds feature
   content, so it should wear the feature-section layout rather than the centred
   one Compare uses. Centred here made it the only interruption in a run of
   three feature sections, which is most of why it read as out of place. */
.extras-heading {
  margin-bottom: 72px;
}
.extras-heading .section-headline {
  max-width: 22ch;
}
/* Below 900px the feature sections centre their headline (see the stepper
   block further down), so Assist follows, or the fix for desktop just moves the
   mismatch onto phones. */
@media (max-width: 900px) {
  .extras-heading { text-align: center; }
  .extras-heading .section-headline,
  .extras-heading .section-sub { margin-left: auto; margin-right: auto; }
}
.extras-grid {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 30px;
}
/* The 5/3 then 3/5 rhythm is most of why this reads as designed rather than
   as a plain grid of equal boxes. */
.extra-wrapper:nth-child(1),
.extra-wrapper:nth-child(4) { grid-column: span 5; }
.extra-wrapper:nth-child(2),
.extra-wrapper:nth-child(3) { grid-column: span 3; }
.extra-wrapper {
  display: flex;
  perspective: 1000px;
}
.extra-card {
  position: relative;
  z-index: 5;
  display: flex;
  width: 100%;
  overflow: hidden;
  border-radius: 26px;
  background: var(--card-rim);
  transform-style: preserve-3d;
}
.extra-card-inner {
  position: relative;
  z-index: 5;
  display: flex;
  flex: 1;
  flex-direction: column;
  justify-content: space-between;
  /* This 1px is the rim the glow shines through. */
  margin: 1px;
  border-radius: 25px;
  background: var(--card-bg);
}

/* Blurred blob, centred in a full-size wrapper. script.js translates the
   wrapper by up to +/-50% of the card, so the blob tracks the cursor 1:1. */
.extra-glow {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
}
.extra-glow span {
  width: 200px;
  height: 200px;
  background: #fff;
  filter: blur(70px);
}
/* Second blob, in front of the content. Hidden until hover, then a soft sheen
   across the card face. */
.extra-glow--front { z-index: 6; }
.extra-glow--front span {
  opacity: 0;
  transition: opacity 500ms var(--ease-out);
}
.extra-card:hover .extra-glow--front span { opacity: 0.1; }

.extra-media {
  position: relative;
  height: 260px;
  overflow: hidden;
  border-radius: 25px 25px 0 0;
  /* Black so the letterboxing below is invisible: every screenshot in here is
     shot against the app's own pure-black background, so padding and picture
     are the same colour and the seam never shows. */
  background: var(--color-bg);
}
/* contain, not cover.
   cover crops whatever does not fit, and the slot changes shape between
   breakpoints: 2.53 on a wide card, 1.47 on a narrow one, 1.27 once the grid
   collapses on mobile. A wide screenshot dropped into the mobile slot lost 50%
   of its width, which sliced the set table in half. contain guarantees the
   whole screenshot survives at every size, and the cost is padding that cannot
   be seen because it is black on black. */
.extra-media-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* Left-aligned to match .step-card and .compare-col. These were the only
   centred cards on the page. */
.extra-text {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  row-gap: 6px;
  padding: 36px 28px 40px;
  text-align: left;
}
.extra-text h3 {
  font-family: var(--font-display);
  font-weight: 800;
  /* Was clamp(1.3rem, 2vw, 1.75rem), which rendered 25.6px at 1280: 52% larger
     than the .step-card headings and 71% of a whole section headline, so these
     competed with the section title instead of sitting under it. This lands at
     ~20px, matching .compare-col-title, so every card heading on the page is
     one size. */
  font-size: clamp(1.05rem, 1.55vw, 1.3rem);
  line-height: 1.3;
  color: var(--color-fg);
}
.extra-text p {
  font-size: 1rem;
  line-height: 1.6;
  color: var(--color-fg-muted);
  max-width: 34ch;
}

/* ---------- Extra card wireframes ----------
   Placeholders only. Each card's artwork is swapped by replacing the .wireframe
   element with an <img class="extra-media-img">. */
.wireframe {
  height: 100%;
  padding: 28px 30px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  background: rgba(255, 255, 255, 0.03);
}
.wf-bar {
  height: 12px;
  border-radius: 6px;
  background: rgba(255, 255, 255, 0.1);
  flex: none;
}
.wf-bar--head { width: 42%; background: rgba(255, 255, 255, 0.16); }
.wf-bar--short { width: 60%; }
.wf-bar--tiny { width: 34%; }
.wf-figure {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 2.6rem;
  line-height: 1;
  color: rgba(255, 255, 255, 0.22);
}
.wf-track {
  height: 12px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.wf-track span {
  display: block;
  width: var(--fill);
  height: 100%;
  background: var(--color-accent);
  opacity: 0.55;
}
.wf-cols {
  flex: 1;
  display: flex;
  align-items: flex-end;
  gap: 10px;
}
.wf-cols span {
  flex: 1;
  height: var(--h);
  border-radius: 5px 5px 0 0;
  background: var(--color-accent);
  opacity: 0.5;
}
.wf-ring { align-items: center; }
.wf-dial {
  width: 92px;
  height: 92px;
  border-radius: 50%;
  background: conic-gradient(var(--color-accent) 0deg 250deg, rgba(255, 255, 255, 0.08) 250deg 360deg);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.6;
  flex: none;
}
.wf-dial span {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--card-bg);
}
.wf-ring .wf-bar { align-self: center; }
.wf-list { gap: 12px; }
.wf-row {
  display: flex;
  align-items: center;
  gap: 12px;
}
.wf-row .wf-bar { flex: 1; }
.wf-chip {
  width: 26px;
  height: 26px;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.1);
  flex: none;
}
.wf-chip--on { background: var(--color-accent); opacity: 0.55; }

/* ---------- Compare ---------- */
.compare { padding: 120px 0; }
.compare-heading { text-align: center; margin: 0 auto 56px; }
.compare-heading .section-headline { max-width: 24ch; margin: 0 auto; }
.compare-table {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
  max-width: 820px;
  margin: 0 auto;
  /* Each column is only as tall as its own content. Premium lists 13 features
     and Free lists 5, so the default stretch left 351px of dead space inside
     the Free card, which read as a rendering fault rather than a short list.
     This also matches the in-app Premium screen, where the free column simply
     ends where it ends. */
  align-items: start;
}
.compare-col {
  border: 1px solid var(--color-border);
  border-radius: 20px;
  padding: 32px 28px;
  position: relative;
}
.compare-free { background: var(--color-surface); }
/* Same material treatment as the active step card, for the same reason: an
   accent stroke around the whole column was the loudest thing on the page. The
   "Recommended" badge already names this column, so the surface only has to
   lift it, not shout. */
.compare-premium {
  background: rgba(255, 255, 255, 0.075);
  border-color: rgba(255, 255, 255, 0.22);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09);
}
.compare-badge {
  position: absolute;
  top: -13px;
  left: 28px;
  background: var(--color-accent);
  color: #001018;
  font-size: 0.72rem;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  padding: 5px 12px;
  border-radius: 999px;
}
.compare-col-title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.3rem;
  /* Was 22px. The price sits directly beneath now and carries the gap down to
     the list instead. */
  margin-bottom: 10px;
}
.compare-list { display: flex; flex-direction: column; gap: 14px; margin-bottom: 26px; }
.compare-list li {
  font-size: 0.95rem;
  color: var(--color-fg-faint);
  padding-left: 26px;
  position: relative;
}
.compare-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: 6px;
  width: 14px;
  height: 14px;
  border-radius: 4px;
  border: 1px solid var(--color-border);
}
.compare-list li.is-included {
  color: var(--color-fg);
}
.compare-list li.is-included::before {
  background: var(--color-accent);
  border-color: var(--color-accent);
}
.compare-col-footer {
  font-size: 0.85rem;
  color: var(--color-fg-faint);
  font-weight: 600;
}
/* The single call to action, centred under both columns. Not full width like
   the old in-column button: on its own under an 820px table a full-bleed pill
   reads as a banner rather than a button. */
.compare-cta-row { margin-top: 48px; text-align: center; }

/* ---------- Final CTA ---------- */


/* ---------- Footer ---------- */
.footer { border-top: 1px solid var(--color-border-soft); padding: 48px 0; }
.footer-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 20px;
}
.footer-logo { height: 32px; width: auto; opacity: 0.8; }
.footer-links { display: flex; gap: 24px; }
.footer-links a { font-size: 0.85rem; color: var(--color-fg-muted); }
.footer-links a:hover { color: var(--color-fg); }
.footer-copy { font-size: 0.8rem; color: var(--color-fg-faint); }
.footer-support { font-size: 0.85rem; color: var(--color-fg-muted); }
/* Underlined for the same reason as the attribution link below: inside a line of
   body text an <a> with inherited colour and no underline is invisible as a link. */
.footer-support a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color var(--dur-hover) var(--ease-out);
}
.footer-support a:hover { color: var(--color-fg); }

/* FatSecret's required Platform API attribution. flex-basis 100% forces it onto
   a line of its own inside the space-between footer row, so it centres cleanly
   instead of being pushed to one end by whatever wraps beside it.

   Sits below the copyright and is quieter than the legal links, because it is a
   data credit rather than something a visitor is meant to go and read. It is
   still a real link with a real underline: the attribution requirement is for a
   working link, so do not strip the underline or drop the colour to invisible. */
.footer-attribution {
  flex: 0 0 100%;
  margin-top: 4px;
  text-align: center;
  font-size: 0.78rem;
  color: var(--color-fg-faint);
}
.footer-attribution a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color var(--dur-hover) var(--ease-out);
}
.footer-attribution a:hover { color: var(--color-fg); }

/* The four legal links are deliberately flat: same element, same size, same
   colour, no grouping and nothing collapsible. They carry the same weight as
   the site links above them rather than being demoted to fine print. Wrapping
   is allowed so they never truncate on a narrow screen. */
.footer-legal {
  display: flex;
  flex-wrap: wrap;
  gap: 0 24px;
}
.footer-legal a {
  font-size: 0.85rem;
  color: var(--color-fg-muted);
  /* 12px either side of a ~20px line box clears the 44px tap target these
     want on a phone, where they get opened constantly. */
  padding: 12px 0;
}
.footer-legal a:hover { color: var(--color-fg); }
.footer-legal a:active { opacity: 0.55; }

/* ---------- Legal documents ----------
   Text-forward pages for the privacy, health data, terms and SMS documents.
   Deliberately plain: these are read end to end, often on a phone, and by app
   store and carrier reviewers who need the content to load with nothing in the
   way. They share the site's tokens and fonts but none of its motion, and the
   pages load no JavaScript at all. */
.legal-page {
  /* Overrides the home page's scroll-driven layout assumptions. */
  min-height: 100%;
}
.legal-header {
  padding: 28px var(--gutter);
  border-bottom: 1px solid var(--color-border-soft);
}
.legal-home { display: inline-flex; opacity: 0.9; }
.legal-home:hover { opacity: 1; }

.legal-doc {
  /* Manrope ligates "(c)" into a copyright glyph. Harmless in marketing copy,
     wrong in a contract: section 17.2 lists conditions (a), (b) and (c), and
     clause (c) was rendering as ©. Measured at 20px, "(c)" collapses from 26.5px
     to 18.1px when this is left at normal. Scoped here so the marketing page
     keeps its default typography. */
  font-variant-ligatures: no-common-ligatures;
  /* 68ch lands inside the 65 to 75 character measure that long prose wants. */
  max-width: 68ch;
  margin: 0 auto;
  padding: 56px var(--gutter) 88px;
}
.legal-doc h1 {
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: 110%;
  font-size: clamp(1.9rem, 4.4vw, 2.6rem);
  line-height: 1.15;
  /* Tighter than body copy, matching the size-specific tracking used sitewide. */
  letter-spacing: -0.018em;
}
.legal-updated {
  margin-top: 12px;
  font-size: 0.9rem;
  color: var(--color-fg-faint);
}
.legal-doc h2 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.3rem;
  line-height: 1.3;
  letter-spacing: -0.01em;
  margin-top: 48px;
  /* Keeps a deep-linked heading clear of the top edge instead of flush to it. */
  scroll-margin-top: 24px;
}
.legal-doc p {
  margin-top: 16px;
  font-size: 1.02rem;
  /* Generous: these are read in long stretches, not scanned. */
  line-height: 1.75;
  color: var(--color-fg-muted);
}
.legal-doc a { color: var(--color-accent); text-decoration: underline; }

/* Numbered sub-clauses (4.1, 4.2 and so on). Quieter than h2 and set in the
   body face, so a run of them reads as structure inside a section rather than
   as a second stack of section headings. */
.legal-doc h3 {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 1.02rem;
  line-height: 1.4;
  margin-top: 32px;
  color: var(--color-fg);
  scroll-margin-top: 24px;
}

/* The global reset strips list markers sitewide. These documents are the one
   place that genuinely needs them: the prohibited-use and indemnity clauses are
   enumerated lists, and running them together as prose changes how they read. */
.legal-doc ul.legal-list {
  margin-top: 16px;
  padding-left: 22px;
  list-style: disc;
}
.legal-doc ul.legal-list li {
  margin-top: 8px;
  font-size: 1.02rem;
  line-height: 1.75;
  color: var(--color-fg-muted);
}

/* Tables. The privacy policy enumerates what is collected, which legal basis
   applies and which processor receives what, and those are genuinely tabular:
   flattening them into prose loses the row-to-row comparison that makes them
   readable. The wrapper scrolls rather than the page, so a three-column table
   never forces the whole document sideways on a phone. */
.legal-table-scroll {
  margin-top: 20px;
  overflow-x: auto;
  /* Stops the flex/grid min-content floor from widening the whole column. */
  max-width: 100%;
}
.legal-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.94rem;
  line-height: 1.6;
  /* Narrow enough to fit a phone, wide enough that the wrapper scrolls instead
     of the cells crushing to one word per line. */
  min-width: 420px;
}
.legal-table th {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-align: left;
  color: var(--color-fg-muted);
  padding: 0 18px 10px 0;
  border-bottom: 1px solid var(--color-border);
  white-space: nowrap;
}
.legal-table td {
  padding: 12px 18px 12px 0;
  border-bottom: 1px solid var(--color-border-soft);
  color: var(--color-fg-muted);
  vertical-align: top;
}
.legal-table td:first-child { color: var(--color-fg); }
.legal-table tr:last-child td { border-bottom: 0; }

/* The account-deletion steps are a sequence, so they are numbered rather than
   bulleted. Order carries meaning there: the grace period sits between being
   signed out and the data actually going. */
.legal-doc ol.legal-list-numbered {
  margin-top: 16px;
  padding-left: 22px;
  list-style: decimal;
}
.legal-doc ol.legal-list-numbered li {
  margin-top: 8px;
  font-size: 1.02rem;
  line-height: 1.75;
  color: var(--color-fg-muted);
}

/* The arbitration and class-action notice. Conspicuousness is the entire point
   of this block, so it is boxed and carries full-strength body colour rather
   than the muted tone used for ordinary paragraphs. */
.legal-notice {
  margin-top: 28px;
  padding: 20px 24px;
  border: 1px solid var(--color-border);
  border-radius: 16px;
  background: var(--color-surface);
}
.legal-notice p {
  margin-top: 0;
  font-size: 0.95rem;
  color: var(--color-fg);
}

.legal-toc {
  margin-top: 40px;
  padding: 24px 28px;
  border: 1px solid var(--color-border);
  border-radius: 16px;
  background: var(--color-surface);
}
.legal-toc-title {
  font-family: var(--font-body);
  font-size: 0.78rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-fg-faint);
  /* Resets the h2 rules above; this is a label, not a document section. */
  margin-top: 0;
}
.legal-toc ol {
  margin-top: 14px;
  /* The section headings carry their own numbers ("1. Who we are"), and so do
     the link labels, so the list must not add a second set. Leaving the default
     decimal marker on rendered every entry as "1. 1. Who we are". */
  list-style: none;
  padding-left: 0;
  color: var(--color-fg-faint);
}
.legal-toc li { margin-top: 2px; }
.legal-toc a {
  display: inline-block;
  /* 11px either side of a ~23px line box clears a 44px tap target. */
  padding: 11px 0;
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--color-fg-muted);
  text-decoration: none;
}
.legal-toc a:hover { color: var(--color-fg); text-decoration: underline; }
.legal-toc a:active { opacity: 0.55; }

@media (max-width: 640px) {
  .legal-doc { padding: 36px var(--gutter) 64px; }
  .legal-toc { padding: 20px 20px; }
  .legal-doc h2 { margin-top: 40px; }
}

/* ---------- Responsive ---------- */
@media (max-width: 1024px) {
  /* Two equal columns; the 5/3 rhythm needs more width than this to read. */
  .extra-wrapper:nth-child(n) { grid-column: span 4; }
  .extras-grid { gap: 24px; }
}

@media (max-width: 900px) {
  /* Stacked instead of side by side: mockup on top, one card underneath, and
     you swipe between steps. A mockup plus all three cards plus the headline is
     ~1100px of content in an 844px screen, which is why only one card shows.

     .feature-copy and .feature-visual go display:contents so their children
     become items of one flex column and `order` can arrange them across the two
     wrappers. No markup change, so desktop is untouched.

     The track collapses to the frame's own height, which makes travel 0 in
     script.js, and that is what switches the stepper from scroll-driven to
     swipe-driven. */
  .stepper-track { height: auto; }
  .stepper-inner {
    position: static;
    display: flex;
    flex-direction: column;
    align-items: center;
    /* svh, not dvh: dvh tracks the URL bar and would resize the frame as you
       scroll. vh first as the fallback for engines without svh. */
    min-height: 100vh;
    min-height: 100svh;
    gap: 0;
    padding: 84px var(--gutter) 24px;
    /* Vertical panning stays the browser's job, horizontal is ours. Lets every
       touch listener in script.js stay passive. */
    touch-action: pan-y;
    /* Contains the peeking neighbours. `clip`, not `hidden`: hidden forces the
       used value of overflow-y to auto, which would turn the section into a
       nested scroll container. */
    overflow-x: clip;
  }
  .feature-copy,
  .feature-visual { display: contents; }

  /* Every flattened item needs an explicit order; unset would default to 0. */
  .feature-copy > .eyebrow { order: 1; flex: 0 0 auto; }
  .feature-copy > .section-headline { order: 2; flex: 0 0 auto; text-align: center; }
  .feature-visual > .step-stage { order: 3; }
  .feature-copy > .step-list { order: 4; flex: 0 0 auto; }
  .feature-copy > .step-dots { order: 5; flex: 0 0 auto; }
  /* No room for either inside the pinned frame. The download call is still
     reachable from the sticky nav button, which is on screen at every scroll
     position, plus the Features section and the one under the Compare table. */
  .feature-copy > .section-sub,
  .feature-copy > .feature-cta { display: none; }

  /* The mockup absorbs whatever height is left after the fixed items, so one
     rule fits 360 through 768 and survives any URL-bar state. aspect-ratio
     turns that flexed height back into a width. */
  .step-stage {
    flex: 1 1 auto;
    /* A floor, not 0. As the only flexible item the mockup absorbed the entire
       cost of a larger text setting: at 150% root it fell to 118px wide, small
       enough that the screenshot stopped being readable. Below this the frame
       grows past the viewport and the section scrolls instead. */
    min-height: 300px;
    height: auto;
    width: auto;
    max-width: 68vw;
    margin: 18px 0;
  }
  .step-shot-img {
    height: 100%;
    width: auto;
    margin: 0 auto;
    filter: drop-shadow(0 20px 40px rgba(0, 0, 0, 0.55));
  }

  /* Cards become a stack in a single grid cell, so the container self-sizes to
     the tallest one with no hardcoded height, and they cross-fade in place. */
  .step-list {
    display: grid;
    width: 100%;
    margin-top: 0;
    /* Only one card shows, so there is nothing to choose between. */
    pointer-events: none;
  }
  .step-list > li { grid-area: 1 / 1; }
  /* Position is owned by the spring in script.js, which writes an inline
     transform every frame. CSS must not transition transform here or it would
     interpolate between the spring's own frames and fight it; opacity is still
     CSS's job. This is also what makes the motion interruptible: there is no
     fixed-duration animation to wait out. */
  /* Overrides the desktop --lift calcs outright. box-shadow is listed
     explicitly because --lift is written inline and survives a resize: without
     this, crossing down from desktop mid-scroll would leave a phantom top
     highlight on the mobile card. */
  .step-card,
  .step-card.is-active {
    opacity: 0;
    cursor: default;
    border-color: var(--color-border);
    background: var(--color-surface);
    box-shadow: none;
    transition: opacity 420ms var(--ease-out);
  }
  .step-card.is-active { opacity: 1; }
  /* Neighbours stay visible: the mockup pitch is short enough to leave a sliver
     of the next and previous phone in the gutters, which is both the affordance
     that the section swipes and what makes the outgoing one read as pushed
     rather than faded. At the first and last slide one gutter is empty, which
     is the signal that there is nothing further that way. */
  .step-shot {
    opacity: 0.4;
    transition: opacity 420ms var(--ease-out);
  }
  .step-stage .step-shot.is-active { opacity: 1; }

  .step-dots {
    display: flex;
    justify-content: center;
    gap: 4px;
    margin-top: 20px;
  }
  /* The Extras carousel starts at 760, not here: between the two the four
     cards are still a two-up grid and there is nothing to page through. */
  .extras-dots { display: none; }
  /* The dot is 6px but the button is 32px, so the tap target clears the 44px
     guidance once the 12px gap either side is counted. */
  .step-dot {
    width: 32px;
    height: 32px;
    padding: 13px;
    border: 0;
    background: none;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .step-dot::after {
    content: '';
    display: block;
    width: 6px;
    height: 6px;
    border-radius: 999px;
    background: var(--color-border);
    transition: background var(--dur-hover) var(--ease-out), transform var(--dur-hover) var(--ease-out);
  }
  .step-dot.is-active::after {
    background: var(--color-accent);
    transform: scale(1.35);
  }
  /* Ties with .is-active on specificity, so it has to come after it to win. */
  .step-dot:active::after {
    background: var(--color-accent);
    transform: scale(1.7);
  }
  .step-dot:focus-visible {
    outline: 2px solid var(--color-accent);
    outline-offset: -4px;
    border-radius: 999px;
  }

  /* The frame is pinned again, so the max-height clamps that shrink type to fit
     still earn their keep. These only tighten the headline for narrow widths. */
  .feature-section .section-headline { font-size: clamp(1.5rem, 5.2vw, 2rem); }
  .feature-section .eyebrow { font-size: 0.9rem; margin-bottom: 10px; }
  .step-card { padding: 14px 18px; }
  .step-card p { font-size: 0.92rem; line-height: 1.5; }

  .compare-table { grid-template-columns: 1fr; }

  /* Nav: the primary list turns into a panel under the pill, and the pill goes
     burger / mark / download. Grid rather than flex so the mark is optically
     centred whatever the two side items measure. .nav-links is absolutely
     positioned by then, so it never claims a column. */
  .nav-inner {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 10px;
    padding: 10px;
  }
  .nav-toggle { display: inline-flex; order: 1; justify-self: start; }
  .nav-logo { order: 2; justify-self: center; }
  /* The two 1fr columns are equal, so the button has to fit the narrower of
     them or it wraps to two lines. Tightened here rather than left to wrap,
     and nowrap as the backstop: if it ever does outgrow its column the column
     widens and the mark drifts a little off centre, which beats a broken
     two-line button. */
  .nav-cta {
    order: 3;
    justify-self: end;
    white-space: nowrap;
    padding: 0 13px;
    font-size: 0.78rem;
  }
  .nav-indicator { display: none; }
  .nav-links {
    position: absolute;
    top: calc(100% + 10px);
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: 10px;
    /* Heavier than the pill's 0.6: the pill sits over black, but this panel
       drops over the hero phone and has to stay readable across it. */
    background: rgba(10, 10, 12, 0.97);
    backdrop-filter: blur(24px);
    -webkit-backdrop-filter: blur(24px);
    border: 1px solid var(--color-border-soft);
    border-radius: 22px;
    opacity: 0;
    visibility: hidden;
    /* Grows out of the hamburger that opened it rather than out of its own
       centre. The origin is only meaningful because of the scale: a panel that
       purely translates has nothing for an origin to anchor. */
    transform-origin: 30px top;
    transform: translateY(-8px) scale(0.96);
    /* visibility is discrete, so transitioning it would flip halfway through
       and focus() on open would land on a still-hidden panel. Hold it to the
       end when closing, switch it instantly when opening. */
    transition: opacity 240ms var(--ease-out), transform 240ms var(--ease-out), visibility 0s linear 240ms;
  }
  .nav[data-nav-open] .nav-links {
    opacity: 1;
    visibility: visible;
    transform: translateY(0) scale(1);
    transition: opacity 240ms var(--ease-out), transform 240ms var(--ease-out), visibility 0s linear 0s;
  }
  .nav-links a {
    font-size: 1rem;
    padding: 13px 14px;
    border-radius: 14px;
  }
  .nav-links a.is-active {
    color: var(--color-fg);
    background: rgba(255, 255, 255, 0.06);
  }
}

@media (max-width: 760px) {
  .extras { padding: 80px 0; }
  .extras-heading { margin-bottom: 48px; }
  .extra-media { height: 210px; }
  .extra-text { padding: 28px 22px 32px; }

  /* The four cards become one swipe carousel, the same gesture as the feature
     sections above. The grid collapses to a single cell so the wrappers stack,
     and script.js translates them from there. */
  .extras-grid {
    grid-template-columns: 1fr;
    gap: 0;
    /* Contains the peeking neighbours. `clip`, not `hidden`: hidden forces the
       used value of overflow-y to auto, which would make this a nested scroll
       container. */
    overflow-x: clip;
    /* Vertical panning stays the browser's job, horizontal is ours, which is
       what lets the touch listeners stay passive. */
    touch-action: pan-y;
  }
  .extra-wrapper:nth-child(n) {
    grid-area: 1 / 1;
    /* Narrower than the frame, and that margin is what leaves a sliver of the
       next card showing. The pitch in script.js is this width plus EXTRAS_GAP. */
    width: 77%;
    justify-self: center;
    /* Transform is the spring's, not CSS's. See the note in the stepper block. */
    transition: opacity 420ms var(--ease-out);
  }
  /* script.js marks these revealed on mobile (parked off-screen they would
     never intersect, so they would never reveal), which leaves the dimming of
     the neighbours to these rules. */
  .extra-wrapper[data-reveal],
  .extra-wrapper[data-reveal].is-visible { opacity: 0.35; }
  .extra-wrapper[data-reveal].is-active { opacity: 1; }

  .extras-dots { display: flex; }
}

@media (max-width: 640px) {
  .proof-row { margin-top: 60px; }
  .proof-mark { padding: 0 24px; }
  .proof-caption { font-size: 0.9rem; }
  .hero { --hero-phone-h: clamp(340px, 56vh, 500px); }
  .hero-stage { height: 125vh; }
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
  [data-reveal], [data-reveal-soft] { opacity: 1; transform: none; }
}

/* ---------- Reduced transparency ----------
   Reduced motion is only one of three signals, and this page leans on
   backdrop-filter for exactly the surfaces this one exists for. Frost them
   solid rather than blurring what is behind. */
@media (prefers-reduced-transparency: reduce) {
  .nav-inner,
  .nav-links {
    background: #0a0a0c;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* ---------- Increased contrast ----------
   Near-solid surfaces with a defined edge, and muted text brought up to full
   foreground so nothing relies on a dimmed grey to be readable. */
@media (prefers-contrast: more) {
  .nav-inner,
  .nav-links,
  .step-card,
  .extra-card-inner,
  .compare-col {
    background: #000;
    border-color: var(--color-fg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .section-sub,
  .step-card p,
  .extra-text p,
  .nav-links a {
    color: var(--color-fg);
  }
}

/* ---------- Consent gate ----------
   Styles for consent.js. Inert until a tracker is registered there, so on a
   site with nothing to gate none of this ever paints.

   Sits at z-index 150: above the nav at 100, below the skip link at 200, so
   the first thing a keyboard user reaches is still "Skip to content" rather
   than a cookie bar.

   No new colours and no gradient. The banner is the same material as the nav
   pill and the step cards, because a consent notice that looks bolted on from
   a vendor is one people learn to dismiss without reading. */
.consent {
  position: fixed;
  z-index: 150;
  inset: auto 0 0 0;
  pointer-events: none;
}
.consent[data-consent-state="idle"] .consent-banner { display: none; }

.consent-banner {
  pointer-events: auto;
  width: min(760px, calc(100% - 32px));
  margin: 0 auto 16px;
  padding: 20px 24px 18px;
  background: var(--color-surface-2);
  border: 1px solid var(--color-border);
  border-radius: 18px;
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09);
  animation: consent-rise 600ms var(--ease-out) 400ms both;
}

/* Travels a short distance from just below the fold. Long enough to be seen
   arriving, short enough not to read as an interruption. */
@keyframes consent-rise {
  from { opacity: 0; transform: translateY(24px); }
  to   { opacity: 1; transform: translateY(0); }
}

.consent-eyebrow {
  font-size: 0.75rem;
  font-weight: 800;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-accent);
  margin: 0 0 8px;
}
.consent-copy {
  margin: 0 0 16px;
  font-size: 0.95rem;
  line-height: 1.6;
  color: var(--color-fg-muted);
  max-width: 62ch;
}

.consent-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px;
}
/* Accept and Reject are deliberately identical. If you find yourself reaching
   for a quieter variant on one of them, that is the exact dark pattern the
   CNIL fines are about. Leave them matched.

   min-width is doing real work: without it the two buttons hug their labels
   and land 4px apart, because "Accept all" and "Reject all" are not the same
   width in Manrope. Same class and same colour was already true; this makes
   the rendered boxes match too, so there is nothing left to argue about. */
.consent-btn {
  flex: 0 0 auto;
  min-width: 104px;
}

.consent-link {
  background: none;
  border: none;
  padding: 8px 4px;
  min-height: 44px;
  font-family: var(--font-body);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--color-fg-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
  transition: color var(--dur-hover) var(--ease-out);
}
.consent-link:hover { color: var(--color-fg); }

.consent-policy {
  margin: 12px 0 0;
  font-size: 0.82rem;
}
.consent-policy a {
  color: var(--color-fg-faint);
  text-decoration: underline;
  text-underline-offset: 3px;
  transition: color var(--dur-hover) var(--ease-out);
}
.consent-policy a:hover { color: var(--color-fg); }

/* ---------- Preferences panel ----------
   A real modal, unlike the banner: the user opened it on purpose, so trapping
   focus inside it is correct here and would be wrong there. */
.consent-scrim {
  position: fixed;
  inset: 0;
  z-index: 160;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  pointer-events: auto;
  background: rgba(0, 0, 0, 0.72);
  backdrop-filter: blur(8px);
  -webkit-backdrop-filter: blur(8px);
  animation: fade-in 250ms var(--ease-out) both;
}
.consent-scrim[hidden] { display: none; }

.consent-panel {
  position: relative;
  width: min(560px, 100%);
  max-height: min(80vh, 720px);
  overflow-y: auto;
  padding: 32px;
  background: #0a0a0a;
  border: 1px solid var(--color-border);
  border-radius: 20px;
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.09);
  animation: consent-rise 350ms var(--ease-out) both;
}
.consent-panel-title {
  font-family: var(--font-display);
  font-stretch: 110%;
  font-size: 1.5rem;
  font-weight: 700;
  margin: 0 0 20px;
  padding-right: 40px;
}

.consent-list {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.consent-row {
  padding: 16px 0;
  border-top: 1px solid var(--color-border-soft);
}
.consent-row-head {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 32px;
  cursor: pointer;
}
.consent-row-name {
  font-weight: 700;
  font-size: 1rem;
}
.consent-row-fixed {
  margin-left: auto;
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-fg-faint);
}
.consent-row-body {
  margin: 8px 0 0 32px;
  font-size: 0.88rem;
  line-height: 1.6;
  color: var(--color-fg-muted);
}

.consent-check {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  accent-color: var(--color-accent);
  cursor: pointer;
}

/* The vendor list is rendered straight from the tracker registry in
   consent.js, so what is disclosed here cannot drift away from what actually
   gets injected. That coupling is the point. */
.consent-vendors {
  list-style: none;
  margin: 12px 0 0 32px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.consent-vendors li {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
  font-size: 0.8rem;
  line-height: 1.5;
}
.consent-vendor-name { font-weight: 700; }
.consent-vendor-purpose { color: var(--color-fg-faint); }

.consent-panel-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 24px;
  padding-top: 20px;
  border-top: 1px solid var(--color-border-soft);
}

.consent-close {
  position: absolute;
  top: 20px;
  right: 20px;
  width: 36px;
  height: 36px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid var(--color-border);
  border-radius: 999px;
  color: var(--color-fg-muted);
  font-size: 1.25rem;
  line-height: 1;
  cursor: pointer;
  transition: color var(--dur-hover) var(--ease-out),
              border-color var(--dur-hover) var(--ease-out);
}
.consent-close:hover {
  color: var(--color-fg);
  border-color: var(--color-fg-muted);
}

@media (max-width: 560px) {
  .consent-banner { padding: 18px 20px 16px; border-radius: 16px; }
  .consent-actions { gap: 10px; }
  /* Both decision buttons go full width and stay the same width as each
     other. Equal prominence has to survive the breakpoint too. */
  .consent-btn { flex: 1 1 100%; }
  .consent-link { width: 100%; text-align: center; }
  .consent-panel { padding: 24px 20px; }
  .consent-panel-actions .consent-btn { flex: 1 1 100%; }
}

/* Matches the existing reduced-transparency handling: solid surfaces, no blur.
   The global reduced-motion block already flattens the entrance animations. */
@media (prefers-reduced-transparency: reduce) {
  .consent-banner {
    background: #0a0a0a;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .consent-scrim {
    background: rgba(0, 0, 0, 0.94);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

@media (prefers-contrast: more) {
  .consent-banner,
  .consent-panel {
    background: #000;
    border-color: var(--color-fg);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
  .consent-copy,
  .consent-row-body,
  .consent-vendor-purpose,
  .consent-link { color: var(--color-fg); }
}
