/* sections.css — one block per section type, plus its variants.
 * Section types match the templates in scripts/templates.mjs. When a layout
 * idea doesn't fit, add a variant here — never a freeform positioning canvas.
 * No literal color/size values: tokens.css only.
 */

/* ==========================================================
   hero — variants: video-full, compact
   Built to the animation commission's deliverables so the animator's file
   drops in unchanged: 1920x1080, silent, seamless, object-fit cover,
   action center-framed, poster shown while loading and under reduced motion.
   ========================================================== */

.hero {
  position: relative;
  isolation: isolate;
  display: flex;
  align-items: flex-end;
  min-height: 78svh;
  padding-block: var(--space-8) var(--space-7);
  overflow: hidden;
  color: var(--color-hero-ink);
}

/* Compact heroes are top-aligned, so unlike the full-bleed one they have to
   pay for the fixed header themselves.

   The inner pages read on light ground, so the compact hero carries its own
   soft sage gradient and dark ink rather than the full-bleed hero's cream on
   film. The poster image stays out of it: a photographic band under a
   gradient band is one band too many. */
.hero--compact {
  min-height: 44svh;
  padding-block: calc(var(--header-h) + var(--space-6)) var(--space-6);
  background: transparent;
  color: var(--color-ink);
}

.hero--compact .hero__media,
.hero--compact .hero__scrim,
.hero--compact .hero__placeholder-note {
  display: none;
}


.hero__media,
.hero__media > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: -2;
}

.hero__media img,
.hero__media video {
  object-fit: cover;
  /* Center-safe: on narrow crops the action stays in frame. */
  object-position: center center;
}

/* Scrim keeps the headline at AA contrast over any frame of the loop.
   Weighted to the bottom, where the copy sits. */
.hero__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(
    to top,
    rgb(0 0 0 / 0.62) 0%,
    rgb(0 0 0 / 0.38) 42%,
    rgb(0 0 0 / 0.12) 78%,
    rgb(0 0 0 / 0.05) 100%
  );
}

.hero__content {
  position: relative;
}

.hero h1 {
  max-width: 16ch;
}

.hero .lede {
  color: var(--color-hero-ink);
  opacity: 0.92;
  max-width: 46ch;
  margin-top: var(--space-4);
}

.hero .btn-row {
  margin-top: var(--space-6);
}

.hero .eyebrow {
  color: var(--color-accent-soft);
}

/* These must come after the .hero rules above: same specificity, so source
   order decides, and the compact hero reads on light ground. */
.hero--compact .lede {
  color: var(--color-ink-soft);
  opacity: 1;
}

.hero--compact .eyebrow {
  color: var(--color-accent);
}

.hero--compact h1 {
  color: var(--color-ink);
}

/* Placeholder drift — a slow gradient wander that stands in for the
   commissioned loop. It is deliberately abstract: never stock footage, never
   anything that could be mistaken for the real animation. It paints OVER the
   poster (it comes after it in the markup) so the hero has some life in it;
   at this opacity it modulates the poster rather than replacing it. Delete
   .hero__drift from the markup the day the animator's files land. */
.hero__drift {
  opacity: 0.55;
  background:
    radial-gradient(
      42rem 30rem at 62% 58%,
      color-mix(in srgb, var(--color-accent-soft) 34%, transparent),
      transparent 70%
    ),
    radial-gradient(
      36rem 26rem at 22% 24%,
      color-mix(in srgb, var(--color-accent) 30%, transparent),
      transparent 72%
    );
  animation: hero-drift 34s ease-in-out infinite alternate;
}

@keyframes hero-drift {
  from {
    transform: scale(1.04) translate3d(-1.5%, -1%, 0);
  }
  to {
    transform: scale(1.12) translate3d(1.5%, 1.5%, 0);
  }
}

/* A loud, honest badge while the hero is a placeholder. Removed with the
   placeholder itself — see assets/manifest.json. */
.hero__placeholder-note {
  position: absolute;
  /* Clear of the fixed header, which otherwise sits on top of it and takes
     the nav with it. */
  top: calc(var(--header-h) + var(--space-3));
  right: var(--space-4);
  max-width: calc(100% - var(--space-6));
  z-index: 1;
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-ink-dark);
  background: var(--color-accent-soft);
  border-radius: var(--radius);
  padding: var(--space-2) var(--space-3);
}

/* A static two-column opening: copy first, campaign image second. The hero
   is its own snap area, so a short phone can scroll through the whole image. */
.campaign-hero {
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.campaign-hero.is-screen > .wrap {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  align-items: center;
  align-content: center;
  padding-block: calc(var(--header-h) + var(--space-6)) var(--space-6);
  padding-inline: var(--space-5);
  max-width: var(--layout-campaign-maxw);
}

.campaign-hero__copy,
.campaign-hero__visual {
  min-width: 0;
}

.campaign-hero h1 {
  max-width: 14ch;
  margin-top: var(--space-4);
  font-size: clamp(2.5rem, 4.8vw, 4.25rem);
  line-height: 1.06;
}

.campaign-hero .lede {
  max-width: 42ch;
  margin-top: var(--space-5);
}

.campaign-hero .btn-row {
  margin-top: var(--space-6);
}

.campaign-hero__visual img {
  width: 100%;
  height: auto;
  border-radius: var(--radius);
}

.media-caption {
  margin-top: var(--space-3);
  font-size: 0.8125rem;
  color: var(--color-ink-soft);
}

[data-dark] .media-caption {
  color: var(--color-hero-label);
}

@media (min-width: 60rem) {
  .campaign-hero.is-screen > .wrap {
    grid-template-columns: var(--layout-campaign-columns);
    gap: var(--space-5);
    padding-inline: calc(var(--space-7) * 2) var(--space-5);
    padding-block: calc(var(--header-h) + var(--space-7)) var(--space-7);
  }
}

/* ==========================================================
   film — variants: pinned
   The home page's opening act, in one section: a black title card, the film
   itself, then the green gradient with the Get started CTA.

   It is ONE section on purpose. The hand-offs have to be seamless, and the
   only way to guarantee that is for all the states to be cross-fading layers
   inside a single sticky stage — not sections meeting at edges that can show
   a seam.

   Scroll drives all of it. site.js writes the progress values onto this
   element: --curtain-o and --open-o/--open-y for the opening hand-off, then
   --grad-o and --close-o for the closing one.

   The first scroll input clears the title card and lifts the curtain off the
   film in one move, so the animation is what you are left looking at. From
   there the scroll drives the film's own timeline — it is scrubbed, not
   played — and the green only rises once it has run. The two copy blocks
   share a grid cell, so their ranges must never overlap — site.js enforces
   that rather than relying on the cue numbers being chosen well.

   The placeholder until the commissioned loop lands is black, edge to edge.
   ========================================================== */

/* DEFAULT state — no JavaScript, or reduced motion: an ordinary full-bleed
   gradient section with both pieces of copy stacked. Everything the page has
   to say is readable and every control works.

   site.js opts INTO the cinematic version by adding .film--cinematic. Written
   this way round on purpose: if scripting fails, the page degrades to a
   normal section rather than to 440vh of black nothing. */
.film {
  position: relative;
  background: var(--color-film);
}

.film__stage {
  position: relative;
  overflow: hidden;
  display: grid;
  place-items: center;
  min-height: 70svh;
  padding-block: calc(var(--space-8) + var(--header-h)) var(--space-8);
  background: var(--gradient-deep);
}

.film__media,
.film__media img,
.film__media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
}

/* Portrait phones crop a 16:9 film to about a third of its width, which cuts
   the ad in half exactly when it matters. So the film is fitted to the width
   and shown 1.3x (Jack's pick, 2026-10-02): the largest framing that keeps
   everything whole — the panel through the AdVio and Palm & Vine shots, and
   the end logo, which spans 0.156-0.844 of the film's width.

   Phones play the phone cut, hero-film-portrait.mp4: that 1.3x window, cut
   from the master with the framing built in per shot. Centred throughout,
   except the car rear and the panel close-up after it, which are held on the
   BumpStick (Jack: centre it "for that sequence"). The change lands exactly
   on the hard cut from the opening into the car rear — not on the cut into
   the close-up: the animator framed those two shots to match, and changing
   framing between them made that cut jump. It changes back to centred on the
   hard cut out of Palm & Vine into the take-off, which happens to keep the
   BumpStick in nearly the same spot on screen, so nothing ever moves. One
   fixed position could not do this: the BumpStick sits right of the film's
   centre through that sequence, and holding the whole film over there cuts
   the end logo. site.js marks the phone cut with .film__story--phone-cut once
   it is playing.

   Until then — and for the full film, in a browser that ignores media on
   <source> — the rule below frames the 16:9 film itself: 1.3x, 2.5% of the
   screen left of centre, the best one fixed position can do for both the
   panel and the logo. The stage clips the overflow.

   No runtime camera work: three versions were built and cut on 2026-10-02 (a
   path that slid and re-zoomed to follow the action, an eased slide, and
   switches hidden in dips to black that cut the car-rear shot short).

   The band's top and bottom fade into the stage's black instead of ending on
   a hard edge — the film's backdrop is a dark grey, so the fitted strip read
   as a box sitting on black. The mask is drawn in the video's unzoomed box,
   so it grows with the zoom. --film-half is half the fitted film's height
   (the media box's width × 9/16 ÷ 2), hence the container units. */
@media (max-aspect-ratio: 1 / 1) {
  .film--cinematic .film__media {
    container-type: inline-size;
  }

  .film--cinematic .film__media video {
    --film-half: 28.125cqw;
    --film-fade: 10cqw;
    object-fit: contain;
    transform: translateX(-2.5%) scale(1.3);
    -webkit-mask-image: linear-gradient(
      to bottom,
      transparent calc(50% - var(--film-half)),
      black calc(50% - var(--film-half) + var(--film-fade)),
      black calc(50% + var(--film-half) - var(--film-fade)),
      transparent calc(50% + var(--film-half))
    );
    mask-image: linear-gradient(
      to bottom,
      transparent calc(50% - var(--film-half)),
      black calc(50% - var(--film-half) + var(--film-fade)),
      black calc(50% + var(--film-half) - var(--film-fade)),
      transparent calc(50% + var(--film-half))
    );
  }

  /* The phone cut is already the 1.3x window, so it is shown as it is: no
     zoom, no nudge. Its frame is 1476x1080, so half its fitted height is
     36.585cqw, and the fade is the 13cqw the zoomed full film ends up with. */
  .film--cinematic .film__media video.film__story--phone-cut {
    --film-half: 36.585cqw;
    --film-fade: 13cqw;
    transform: none;
  }
}

/* The film recycles by hand: when it ends on the AdVio card's black, site.js
   adds film--recycle, rewinds, and removes it again — so the fade out is
   quick insurance over an already-black frame, and the fade back in is the
   gentle rise Jack asked for. */
.film__video {
  transition: opacity 0.9s ease;
}

.film--recycle .film__video {
  opacity: 0;
  transition-duration: 0.35s;
}

/* The scrim. Sits over the film and under the gradient.

   It used to be flat --color-film at full strength, which meant the film was
   completely hidden until the first scroll — the title card was a black card,
   and the animation played behind it unseen. The headline and the thing it is
   introducing were never on screen together.

   Now it darkens the film instead of hiding it: heaviest at the top and foot
   where the type sits, lightest through the middle where the animation is.
   The headline reads over the film from the first frame, and scrolling lifts
   the scrim off rather than uncovering something hidden. Weighted for
   contrast, not for taste — the film loops through some bright frames, and
   the copy has to hold AA over all of them. */
.film__curtain {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-film) 72%, transparent) 0%,
    color-mix(in srgb, var(--color-film) 48%, transparent) 44%,
    color-mix(in srgb, var(--color-film) 62%, transparent) 72%,
    color-mix(in srgb, var(--color-film) 80%, transparent) 100%
  );
}

/* Viewport-anchored, not box-anchored. Every deep-gradient surface on the
   page paints the same field, so where the film's closing frame meets the
   doors section there is no edge to see — the gradient simply continues.
   Box-anchored gradients restart per section and leave a visible band. */
.film__grad {
  position: absolute;
  inset: 0;
  background: var(--gradient-deep);
  background-attachment: fixed;
}

.film__copy {
  position: relative;
  z-index: 2;
  width: 100%;
  text-align: center;
  color: var(--color-hero-ink);
}

.film__close {
  margin-top: var(--space-7);
}

.film__cue {
  display: none;
}

.film h1 {
  max-width: 18ch;
  margin-inline: auto;
}

/* The title card carries the page on its own, over moving picture. It can
   take more size and tighter leading than a heading in a column of text —
   the measure is what keeps it from sprawling. */
.film__open h1 {
  font-size: clamp(2.375rem, 7.5vw, 4.25rem);
  line-height: 1.06;
  letter-spacing: -0.025em;
}

.film__intro .eyebrow {
  margin-bottom: var(--space-4);
  color: var(--color-hero-ink);
}

.film__intro .lede {
  max-width: 42ch;
  margin: var(--space-5) auto 0;
  color: var(--color-hero-ink);
}

.film__intro .btn-row {
  margin-top: var(--space-6);
}

.film__close h2 {
  max-width: 20ch;
  margin-inline: auto;
}

.film__close .lede {
  margin: var(--space-4) auto 0;
  color: color-mix(in srgb, var(--color-hero-ink) 84%, transparent);
}

.film__close .btn-row {
  margin-top: var(--space-6);
}

.film__close .btn {
  font-size: 1.0625rem;
  padding: var(--space-4) var(--space-7);
}

.film--cinematic .film__cue {
  display: block;
  margin-top: var(--space-6);
  font-size: 0.8125rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* 70% was fine over flat black. Over film it has to survive a bright frame
     at 13px, which is small text by WCAG's reckoning. */
  color: color-mix(in srgb, var(--color-hero-ink) 84%, transparent);
}

.film__cue::after {
  content: '';
  display: block;
  width: 1px;
  height: var(--space-6);
  margin: var(--space-3) auto 0;
  background: linear-gradient(
    to bottom,
    color-mix(in srgb, var(--color-hero-ink) 70%, transparent),
    transparent
  );
}

/* --- Cinematic: the pinned, scroll-driven version. Added by site.js. --- */

.film--cinematic {
  /* Scroll distance, not visual height. The film plays on its own clock, so
     this is not its running room — it is the travel the three snap stops
     (title, film, closing line) are spread across.

     It was 600vh, with a film gate in site.js, back when length was the only
     thing stopping one hard flick from clearing the film, the green and the
     closing line in one go. The snap does that job now (see initSnap): a
     swipe moves exactly one stop however hard it is thrown. So 300vh again,
     and the gate is gone.

     The road's fade-in is measured off this element's real height at runtime,
     so it follows this number without being told. */
  height: 300vh;
}

.film--cinematic .film__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100dvh;
  min-height: 0;
  padding: 0;
  background: var(--color-film);
}

.film--cinematic .film__curtain {
  opacity: var(--curtain-o, 1);
  transition: opacity 1.2s ease;
}

.film--cinematic .film__grad {
  opacity: var(--grad-o, 1);
}

/* Both copy blocks occupy the same grid cell, so they cross-fade in place
   instead of pushing each other around. */
.film--cinematic .film__copy {
  grid-area: 1 / 1;
}

/* The opening line rides up and out rather than just dimming — it should read
   as the title card clearing the frame, not as a light being turned down. */
.film--cinematic .film__open {
  opacity: var(--open-o, 1);
  transform: translateY(calc(var(--open-y, 0) * -7rem));
  will-change: transform, opacity;

  /* Fill the stage rather than sitting in a lump at its centre. The headline
     takes the middle, the cue marks the bottom edge of the frame, and the
     film runs between them — a composed frame instead of a centred block
     floating on picture. */
  height: 100%;
  display: flex;
  flex-direction: column;
  padding-block: calc(var(--header-h) + var(--space-7)) var(--space-6);
}

/* Keep the pitch and campaign action together above the scroll cue. */
.film--cinematic .film__intro {
  margin-block: auto;
}

.film--cinematic .film__open h1 {
  margin-block: 0;
}

.film--cinematic .film__open[inert] {
  pointer-events: none;
}

.film--cinematic .film__open .film__cue {
  margin-top: var(--space-6);
}

.film--cinematic .film__close {
  margin-top: 0;
  opacity: var(--close-o, 0);
  pointer-events: none;
}

.film--cinematic .film__close--on {
  pointer-events: auto;
}

/* ==========================================================
   gateway — variants: driver-led
   The page "Get started" lands on. Fills the screen with the same gradient
   the home page closed on, so arriving reads as continuing rather than as a
   page change.
   ========================================================== */

.gateway {
  display: flex;
  align-items: center;
  min-height: 100svh;
  background: transparent;
  color: var(--color-ink);
}

.gateway__body {
  max-width: 44ch;
}

.gateway h1 {
  font-size: clamp(2.5rem, 8vw, 4rem);
}

.gateway .lede,
.gateway p {
  margin-top: var(--space-4);
}

.gateway__points {
  list-style: none;
  margin: var(--space-6) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}

.gateway__points li {
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
  color: var(--color-ink-soft);
}

/* min-width:0 so the text can shrink below its longest word. Without it a
   flex item refuses to go narrower than its widest unbreakable content, and
   one long token drags the whole list wider than the screen. */
.gateway__points li > span {
  flex: 1 1 auto;
  min-width: 0;
}

.gateway__points li::before {
  content: '';
  flex: none;
  width: var(--space-4);
  height: 2px;
  border-radius: 1px;
  background: var(--color-accent);
  transform: translateY(-0.3em);
}

.gateway .btn-row {
  margin-top: var(--space-7);
}

/* The other audience is offered, not advertised. Quieter type, smaller
   button, below the main call — the weighting is the point. */
.gateway__alt {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-6);
  font-size: 0.9375rem;
}

.gateway__alt span {
  color: var(--color-ink-soft);
}

/* ==========================================================
   form — variants: stacked
   ========================================================== */

.form-section__intro {
  max-width: 52ch;
}

.form {
  margin-top: var(--space-6);
  max-width: 44rem;
}

.form__grid {
  display: grid;
  gap: 0 var(--space-5);
}

@media (min-width: 40rem) {
  .form__grid {
    grid-template-columns: repeat(2, 1fr);
  }

  /* Full width unless the field asks to share a row. */
  .form__grid > .field {
    grid-column: 1 / -1;
  }

  .form__grid > .field--half {
    grid-column: span 1;
  }
}

.form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
  margin-top: var(--space-3);
}

.form__note {
  font-size: 0.875rem;
  color: var(--color-ink-soft);
  max-width: 38ch;
  margin: 0;
}

.form__error {
  margin-top: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius);
  border: 1px solid var(--color-accent);
  background: var(--gradient-soft);
  font-size: 0.9375rem;
}

/* Replaces the form on success. Announced via role="status". */
.form__done {
  margin-top: var(--space-6);
  max-width: 44rem;
  padding: var(--space-6);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  background: var(--gradient-soft);
}



.form__done p {
  margin-top: var(--space-3);
  color: var(--color-ink-soft);
}

/* ==========================================================
   steps — variants: numbered-3
   ========================================================== */

.steps__list {
  counter-reset: step;
  margin-top: var(--space-6);
}

.step {
  counter-increment: step;
}

.step h3::before {
  content: counter(step);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9em;
  height: 1.9em;
  margin-right: var(--space-3);
  border-radius: 50%;
  background: var(--color-accent-soft);
  color: var(--color-accent-soft-ink);
  font-size: 0.8em;
  font-weight: 700;
}

.step p {
  color: var(--color-ink-soft);
  margin-top: var(--space-3);
}

.steps__list .card h3 {
  display: flex;
  align-items: center;
}

/* ==========================================================
   strip — variants: facts
   The credibility line. Short, checkable statements only.
   ========================================================== */

/* Transparent like every other section — it reads as a band because of where
   it sits in the page gradient, not because it paints one. */
.strip {
  background: transparent;
  padding-block: var(--space-6);
}

.strip__items {
  display: grid;
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (min-width: 48rem) {
  .strip__items {
    grid-auto-flow: column;
    grid-auto-columns: 1fr;
    gap: var(--space-6);
  }
}

.strip__item {
  font-family: var(--font-display);
  font-weight: 600;
  display: flex;
  gap: var(--space-3);
  align-items: baseline;
}

.strip__item::before {
  content: '';
  flex: none;
  width: var(--space-4);
  height: 3px;
  border-radius: 2px;
  background: var(--color-accent);
  transform: translateY(-0.3em);
}

/* ==========================================================
   doors — variants: two-up
   The self-sort: driver on one side, advertiser on the other.
   ========================================================== */

.doors__grid {
  margin-top: var(--space-6);
}

.door {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: var(--space-5);
}

.door p {
  color: var(--color-ink-soft);
}

.door .btn {
  align-self: flex-start;
}

/* doors--split-full: where "Get started" lands. Same gradient world as the
   film's closing frame, so arriving here doesn't feel like a page change —
   the two choices just resolve out of it. */
.doors--split-full {
  background: var(--gradient-deep);
  background-attachment: fixed; /* continuous with the film — see .film__grad */
  color: var(--color-hero-ink);
  padding-block: var(--space-8);
}

.doors--split-full h2 {
  text-align: center;
  margin-bottom: var(--space-3);
}

.doors--split-full .lede {
  text-align: center;
  margin-inline: auto;
  color: color-mix(in srgb, var(--color-hero-ink) 82%, transparent);
}

.doors--split-full .door {
  background: color-mix(in srgb, var(--color-hero-ink) 7%, transparent);
  border: 1px solid var(--color-line-on-dark);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: none;
  gap: var(--space-6);
  transition: background-color 0.25s ease, border-color 0.25s ease;
}

.doors--split-full .door:hover {
  background: color-mix(in srgb, var(--color-hero-ink) 12%, transparent);
  border-color: color-mix(in srgb, var(--color-hero-ink) 42%, transparent);
}

.doors--split-full .door h3 {
  font-size: clamp(1.375rem, 3vw, 1.875rem);
}

.doors--split-full .door p {
  color: color-mix(in srgb, var(--color-hero-ink) 82%, transparent);
}

/* ==========================================================
   faq-preview — variants: list
   ========================================================== */

.faq__list {
  margin-top: var(--space-6);
  display: grid;
  gap: var(--space-3);
}

.faq__item {
  background: var(--gradient-surface);
  border: 1px solid var(--color-line);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
}

.faq__item summary {
  font-family: var(--font-display);
  font-weight: 600;
  cursor: pointer;
  list-style: none;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-4);
}

.faq__item summary::-webkit-details-marker {
  display: none;
}

.faq__item summary::after {
  content: '+';
  flex: none;
  font-size: 1.4em;
  line-height: 1;
  color: var(--color-accent);
}

.faq__item[open] summary::after {
  content: '\2013';
}

.faq__item p {
  color: var(--color-ink-soft);
  margin-top: var(--space-3);
}

.faq__cta {
  margin-top: var(--space-6);
}

/* ==========================================================
   prose — variants: default, narrow
   Plain body copy. Carries the stub pages until their slice.
   ========================================================== */

.prose {
  max-width: 68ch;
}

.prose--narrow {
  max-width: 58ch;
}

.prose p + p {
  margin-top: var(--space-4);
}

.prose p {
  color: var(--color-ink-soft);
}

/* ==========================================================
   product — variants: image-right, image-left
   ========================================================== */

.product {
  display: grid;
  gap: var(--space-6);
  align-items: center;
}

@media (min-width: 48rem) {
  .product {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-7);
  }

  .product--image-left .product__media {
    order: -1;
  }
}

.product__media img,
.product__media video {
  width: 100%;
  height: auto;

  /* Square, not rounded. A radius here reads as UI chrome — a card, a
     component — and the point is a product sitting on a page, not a widget
     framed on it. */
  border-radius: 0;
}

/* display:block kills the inline baseline gap under the element; the black
   background is what shows through while the clip is still loading, and it
   matches the footage's own ground so there is no flash of another colour. */
.product__media video {
  display: block;
  background: var(--color-film);
}

/* ==========================================================
   Section-level scaffolding note
   Stub sections carry a marker so an unfinished page can never read as
   finished work. Removed as each page's slice lands.
   ========================================================== */

.stub-note {
  display: inline-block;
  margin-top: var(--space-5);
  font-size: 0.875rem;
  color: var(--color-ink-soft);
  border-left: 3px solid var(--color-accent);
  padding-left: var(--space-3);
}

/* ==========================================================
   statement — variants: lead, center, left
   A beat in a long-scroll page. One idea, room around it, tall enough to
   hold the screen as you pass through it.
   ========================================================== */

.statement {
  display: flex;
  align-items: center;
  min-height: 62svh;
}

.statement__body {
  max-width: 46ch;
}

/* The steps used to alternate sides for rhythm. They all sit left now, in one
   column, and the rhythm comes from the road running down the right instead —
   type in a fixed place, picture moving past it. */
.align-left .statement__body {
  margin-right: auto;
}

.align-right .statement__body {
  margin-left: auto;
}

.statement h2 {
  font-size: clamp(2rem, 5.2vw, 3.25rem);
  max-width: 18ch;
}

.statement p {
  font-size: clamp(1.0625rem, 2.2vw, 1.25rem);
  margin-top: var(--space-5);
  max-width: 54ch;
}

.statement .btn-row {
  margin-top: var(--space-7);
}

.statement--center .statement__body {
  margin-inline: auto;
  text-align: center;
}

.statement--center h2,
.statement--center p {
  margin-inline: auto;
}

/* The opening beat and the closing one: larger, and they get the screen.
   `finale` is `lead` centred — a named variant rather than two classes, so a
   content file can never emit a stray bare class name. */
.statement--lead,
.statement--finale {
  min-height: 82svh;
}

.statement--lead h2,
.statement--finale h2 {
  font-size: clamp(2.5rem, 7vw, 4.5rem);
  max-width: 16ch;
}

.statement--finale .statement__body {
  margin-inline: auto;
  text-align: center;
}

.statement--finale h2,
.statement--finale p {
  margin-inline: auto;
}

/* ==========================================================
   Continuous home ground
   The legacy `"dark": true` marker selects home's continuous gradient.
   Its tokens now follow the visitor's light or dark palette. Anchoring the
   gradient to the viewport keeps consecutive sections seamless.
   ========================================================== */

[data-dark]:not(.film) {
  background: var(--gradient-deep);
  background-attachment: fixed;
  color: var(--color-hero-ink);
}

/* The pitch deck's text hierarchy (2026-10-02): headlines in full cream,
   body copy at 80%, eyebrows at 62%, on the deck's black. On the green the
   body had to be full cream to hold AA under the fog; on black, 80% cream
   still reads at about 12:1. */
[data-dark] p,
[data-dark] .lede {
  color: var(--color-hero-body);
}

[data-dark] .eyebrow {
  color: var(--color-hero-label);
}

/* A halo in the ground's own black around the copy. Invisible on the ground
   itself; where the road (the same cream as the text, at full strength)
   runs behind a line, the road fades out just short of each letter, the way
   a map label sits on a road. Without it the two creams merged and the road
   cut letters in half on phones, where it has to cross the text column. */
[data-dark] .statement__body {
  text-shadow:
    0 0 3px var(--color-copy-halo),
    0 0 8px var(--color-copy-halo),
    0 0 16px var(--color-copy-halo);
}

/* Buttons carry their own cream ground; the halo would smudge their labels. */
[data-dark] .statement__body .btn {
  text-shadow: none;
}

[data-dark] a {
  text-decoration-color: var(--color-accent-soft);
}

/* Panels go DARKER than the ground, not lighter. A pale panel on this green
   lifts the ground under the text and costs the contrast the dark ground was
   chosen to provide. */
[data-dark] .card,
[data-dark] .faq__item {
  background: var(--gradient-glass);
  border-color: var(--color-line-on-dark);
  box-shadow: none;
}

[data-dark] .step h3::before {
  background: var(--gradient-glass);
  border: 1px solid var(--color-line-on-dark);
  color: var(--color-hero-ink);
}

[data-dark] .faq__item summary::after {
  color: var(--color-accent-soft);
}

[data-dark] .strip {
  background: transparent;
}

[data-dark] .strip__item::before {
  background: var(--color-accent-soft);
}

/* Primary actions follow the chosen palette on the continuous home ground. */
[data-dark] .btn--primary {
  background: var(--color-button-bg);
  color: var(--color-button-ink);
}

[data-dark] .btn--primary:hover {
  background: var(--color-button-hover);
}

[data-dark] .btn--secondary {
  background: transparent;
  color: var(--color-accent);
  border-color: var(--color-accent);
}

[data-dark] .product__media img {
  border: 1px solid var(--color-line-on-dark);
}

[data-dark] .stub-note {
  border-left-color: var(--color-accent-soft);
  color: color-mix(in srgb, var(--color-hero-ink) 78%, transparent);
}

/* ==========================================================
   is-screen
   One swipe's worth: a section exactly one screen tall, which the swipe
   lands on (site.js puts a snap point at its top). Nothing in it animates.
   The text moves only as the page moves, so it stays with the finger.

   Every section after the film on home, since 2026-10-02. They were locked
   panels: taller than the screen, with a sticky copy box that pinned, faded
   and drifted through each handover, and on the steps a reveal that opened
   each line out from a seam through its middle. Jack cut all of it. With
   the snap doing the one-idea-at-a-time job, all the pin still did was hold
   the outgoing copy still for the first part of every swipe, so the text
   trailed the finger.

   At rest every section sits exactly where it did as a locked panel: the
   copy box below is the panel's, unchanged.
   ========================================================== */

/* vh, not dvh: on a phone vh is the full height with the toolbar tucked
   away, so the stops don't move as the toolbar comes and goes. The text
   still centres on the visible screen; the wrap below is dvh. */
.is-screen {
  position: relative; /* anchors its snap point */
  min-height: 100vh;
  padding-block: 0;
  display: block;
}

/* The copy box: one screen tall, the copy centred in it below the header. */
.is-screen > .wrap {
  max-width: 62rem;
  min-height: 100vh;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-block: calc(var(--header-h) + var(--space-6)) var(--space-6);
}

/* Reduced motion: no full-screen stops (there is no snap to land on them).
   The page becomes an ordinary sequence of sections, which is shorter to get
   through and says exactly the same thing. */
@media (prefers-reduced-motion: reduce) {
  .is-screen {
    min-height: 0;
  }

  .is-screen > .wrap {
    min-height: 0;
    padding-block: var(--space-7);
  }
}

/* ==========================================================
   Home ground: the road and the fog
   The green lives on the body here rather than on each section, so the road
   can sit on top of it and scroll with the page while the gradient itself
   stays viewport-anchored. Both layers are negative-z pseudo-elements, which
   paints them above the body's background and below every section's content —
   the road passes behind the type, never over it.
   ========================================================== */

body[data-page='home'] {
  position: relative;
  /* Required, not decorative. `position: relative` alone leaves body with
     z-index auto, so it creates no stacking context and the negative-z-index
     road below paints in the ROOT context — behind body's own background,
     invisible. isolation gives body a context, which puts its background
     first and the road on top of it. */
  isolation: isolate;
  background: var(--gradient-deep);
  background-attachment: fixed;
}

/* Sections must not paint over the road. */
body[data-page='home'] [data-dark]:not(.film) {
  background: transparent;
}

/* One seamless tile repeating down the document. The tile is a half-wave of
   Jack's road plus its own mirror, so the curve continues through every join
   however long the page gets — cropped at the two points where the road stops
   turning, which is the only place a mirror doesn't leave a kink. */
body[data-page='home']::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;

  /* The road is drawn in the text's own cream (Jack, 2026-10-02: the same
     colour as the text). The layer is painted in --color-hero-ink and the
     road tile is its mask, so the lines are exactly the colour of the copy
     rather than white thinned over the ground. This is a mask carrier, not a
     visible fill: only the road's lines ever show. */
  background-color: var(--color-hero-ink);
  opacity: calc(var(--road-opacity) * var(--road-fade, 1));

  /* Two mask layers, intersected: the fade-in on top, the road beneath.
     The order matters in Safari: it intersects only where the TOP layer is
     drawn, and the road tile covers just its own column, so with the road on
     top the fade showed through everywhere else as a cream wash. The fade
     covers the whole page, so on top it intersects everywhere.

     The road: right, not centre, because the steps are a single left-hand
     column, so it runs down the side they are not on. Repeated down only:
     a road that repeats on both axes reads as two roads overlapping. Held off
     the right edge, because the wave swings roughly 130px across the tile and
     flush right puts its widest point hard on the viewport edge, where the
     eye reads the clipped swing as a second line.

     The fade-in ramps from where the road can first be SEEN, not from the top
     of the document. The film is opaque and covers roughly the first third of
     the page, so a ramp measured from 0% finishes behind it and the road pops
     into existence at the film's bottom edge, a hard cut. --road-start and
     --road-full are measured in site.js (initRoad) from the film's height and
     the first step: zero level with the step's headline, full a little below
     its copy. `black` is an alpha value here, not a colour.

     --road-fade, also from site.js, dissolves the road once the reader is
     past the sign-up. It is a plain function of scroll position, so it comes
     back on the way up. */
  -webkit-mask-image: linear-gradient(to bottom, transparent var(--road-start, 0px), black var(--road-full, 34%), black 100%),
    url('../assets/img/road-tile.png');
  -webkit-mask-repeat: no-repeat, repeat-y;
  -webkit-mask-position: 0 0, right 4vw top 0;
  -webkit-mask-size: 100% 100%, min(22rem, 62vw) auto;
  -webkit-mask-composite: source-in;
  mask-image: linear-gradient(to bottom, transparent var(--road-start, 0px), black var(--road-full, 34%), black 100%),
    url('../assets/img/road-tile.png');
  mask-repeat: no-repeat, repeat-y;
  mask-position: 0 0, right 4vw top 0;
  mask-size: 100% 100%, min(22rem, 62vw) auto;
  mask-composite: intersect;
}

/* Fog gathering at the foot of the page. */
/* The last panel keeps its copy above the densest fog rather than sitting in
   it, which is what makes a strong-enough fog and legible text compatible. */
body[data-page='home'] main > section:last-child > .wrap {
  padding-bottom: 26vh;
}

body[data-page='home']::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 55vh;
  z-index: -1;
  background: var(--gradient-fog);
  pointer-events: none;
}

/* ==========================================================
   snap
   One swipe, one stop — the paging Safari gives a native iOS scroll view,
   which is what Apple's own pages lean on: mandatory snapping with
   scroll-snap-stop: always, so a hard flick still lands on the NEXT stop
   rather than flying past three. The browser supplies the physics; nothing
   here hijacks the wheel or the touch.

   site.js adds .is-snapping to <html> and drops a zero-size .snap-point at
   each stop, so with scripting off — or reduced motion on — the page scrolls
   freely as before.
   ========================================================== */
html.is-snapping {
  scroll-snap-type: y mandatory;
}

.snap-point {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
