/* The /participate landing: a pitch deck that happens to be a web page.
   Shown on a projector in a restaurant's back office, then emailed to the
   owner afterwards, so it has to work as a room-scale presentation *and*
   as a page on a phone.

   Loaded after tokens.css and base.css and nothing else. landing.css and
   participate.css are deliberately not in the chain: this page shares no
   layout with the form pages, and pulling in a stylesheet built around a
   centred 32rem column only to override most of it is how the two get
   tangled. The handful of primitives that are genuinely shared — the
   wordmark, the buttons, the eyebrow, the paper card scope, the app-store
   badges, the footer — are copied below, marked where they came from and
   where this page needed them to differ.

   The motion contract, which the whole file depends on: the DEFAULT state
   of every animated thing is its FINISHED state. Only the `.has-fx` scope,
   which pitch.js adds unless the visitor asked for reduced motion,
   introduces the hidden and offset starting states. Delete the JS and the
   page still says everything it has to say.

   What drives that motion is TIME, not scroll position. A scene is about
   one screen tall; pitch.js watches it with an IntersectionObserver and
   adds `is-playing` when it settles on screen, and the whole block plays
   itself out over --scene-dur. Scrolling inside a block does nothing,
   because there is nothing left inside a block to scroll through. */

:root {
  --topbar-h: 3.5rem;
  --stage-pad-x: clamp(1.25rem, 5vw, 4.5rem);
  --stage-max: 78rem;

  /* The one timeline every scene is cut against. Each animated element
     carries --from/--to naming its own slice of it, so a whole block runs
     in about --scene-dur plus one --fx-dur: quick enough that nobody
     waits, slow enough to read as deliberate rather than as a flicker.
     pitch.js reads --scene-dur and --fx-dur back off the element rather
     than holding its own copies, so these two numbers are the only place
     the page's tempo is written. */
  --scene-dur: 1600ms;
  --fx-dur: 620ms;
  --fx-ease: cubic-bezier(0.2, 0.7, 0.2, 1);

  /* The delivery-app red, and the face it is set in. Both live here rather
     than in tokens.css because neither belongs to NoTip: they are how the
     category this page argues with presents itself, and nothing else on
     the site should reach for them. They name a category and never a
     company — no competitor is named, drawn, or alluded to by mark.

     The value is measured, not picked. #eb1700 is the obvious app red, but
     --paper on it is 4.10:1, under the 4.5:1 the reversed-out header bar
     needs. #d91f00 gives 4.61:1 for --paper on the bar and for the red
     total on paper, and 3.88:1 for the headline against --ink — over the
     3:1 large text needs on the night canvas.

     No webfont is possible: _headers pins font-src to 'self', and a
     blocked font fails silently, so this is a system stack that lands
     geometric and heavy on macOS (Avenir Next), on Windows (Century
     Gothic, then Trebuchet MS) and on anything else (system-ui). */
  --app-red: #d91f00;
  --app-face: "Avenir Next", Avenir, Futura, "Century Gothic", "Trebuchet MS", system-ui, sans-serif;
}

body {
  /* A guard, not a fix: every cause of horizontal overflow is dealt with
     where it happens. `clip` rather than `hidden` because `hidden` would
     make the viewport a scroll container, which changes what `position:
     fixed` and the IntersectionObserver's viewport root both mean. */
  overflow-x: clip;
  -webkit-font-smoothing: antialiased;
}

/* ────────────────────────────────────────────────────────────────────────
   Primitives copied from landing.css
   ──────────────────────────────────────────────────────────────────────── */

.eyebrow {
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 0.8rem;
}

.micro {
  font-size: 0.78rem;
  color: var(--faint);
  line-height: 1.55;
  margin: 0.8rem 0 0;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  padding: 0.7rem 1.1rem;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
  text-decoration: none;
  border-radius: var(--r-control);
  transition:
    transform 150ms ease,
    background-color 150ms ease,
    border-color 150ms ease;
}

.btn:hover {
  transform: translateY(-1px);
}

.btn:active {
  transform: translateY(0);
}

.btn:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.btn--primary {
  background: var(--accent);
  color: var(--ink);
  border: 1px solid transparent;
}

.btn--primary:hover {
  background: var(--accent-soft);
}

/* As in landing.css: the label is the text colour of whatever the button
   sits on, and the hover's wash is that colour at 6%. This page's one ghost
   button is on the night canvas, so both are paper; inside a paper card
   they would be ink. */
.btn--ghost {
  background: transparent;
  color: inherit;
  border: 1px solid var(--line);
}

.btn--ghost:hover {
  border-color: var(--muted);
  background: color-mix(in srgb, currentColor 6%, transparent);
}

.btn--lg {
  padding: 0.95rem 1.6rem;
  font-size: clamp(1rem, 1.5vw, 1.15rem);
}

/* The paper scope. Same reassignments as landing.css — descendants reading
   --muted/--faint/--line/--well/--focus-ring flip to their ink-on-paper
   values automatically. The one omission is `animation: stage-in`: on this
   page every card's arrival is choreographed on its scene's timeline, and
   a second, competing entrance animation would fight it. */
.stage-card {
  background: var(--paper);
  color: var(--ink);
  color-scheme: light;
  --muted: var(--ink-muted);
  --faint: var(--ink-faint);
  --line: var(--paper-line);
  --line-soft: var(--paper-line-soft);
  --well: var(--paper-well);
  --focus-ring: var(--ink);
  border-radius: var(--r-card);
  /* Named rather than written into `padding` directly: the delivery-app
     card bleeds its header bar out to the card's edge with a negative
     margin, and that margin has to be this exact number. */
  --pad: clamp(1rem, 2.2vw, 1.6rem);
  padding: var(--pad);
  text-align: left;
}

.badges {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.badge {
  display: flex;
  align-items: center;
  gap: 0.65rem;
  padding: 0.55rem 1rem;
  border-radius: var(--r-control);
  border: 1px solid var(--line);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  transition:
    transform 150ms ease,
    background-color 150ms ease;
}

.badge:hover,
.badge:focus-visible {
  background: var(--paper-bright);
  transform: translateY(-1px);
}

.badge:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.badge svg,
.badge img {
  flex: none;
  width: 24px;
  height: 24px;
}

.badge-text {
  display: flex;
  flex-direction: column;
  text-align: left;
  line-height: 1.15;
}

.badge-text .eyebrow {
  font-size: 0.6rem;
  font-weight: 500;
  letter-spacing: 0.02em;
  margin: 0;
  /* The badge is paper; its own eyebrow is ink, not the canvas's faint. */
  color: var(--ink-faint);
}

.badge-text .name {
  font-size: 1rem;
  font-weight: 700;
}

.site-footer {
  font-size: 0.78rem;
  color: var(--faint);
  text-align: center;
}

.site-footer a {
  color: var(--faint);
  border-radius: 2px;
  text-underline-offset: 2px;
  transition: color 150ms ease;
}

.site-footer a:hover {
  color: var(--paper);
}

.site-footer a:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
  color: var(--paper);
}

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

/* ────────────────────────────────────────────────────────────────────────
   The fixed header
   ──────────────────────────────────────────────────────────────────────── */

/* Default is the solid state, because that is the state it ends in and the
   one that is legible over anything. `.has-fx` is what makes it start out
   transparent over the hero. */
.topbar {
  position: fixed;
  inset: 0 0 auto;
  z-index: 40;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  height: var(--topbar-h);
  padding: 0 var(--stage-pad-x);
  background: rgba(11, 11, 12, 0.72);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--line-soft);
}

.topbar .btn {
  padding: 0.45rem 0.9rem;
  font-size: 0.85rem;
}

.has-fx .topbar {
  background: transparent;
  border-bottom-color: transparent;
  backdrop-filter: none;
  transition:
    background-color 320ms ease,
    border-color 320ms ease;
}

.has-fx .topbar.is-solid {
  background: rgba(11, 11, 12, 0.72);
  backdrop-filter: blur(12px);
  border-bottom-color: var(--line-soft);
}

/* ────────────────────────────────────────────────────────────────────────
   The progress rail
   ──────────────────────────────────────────────────────────────────────── */

/* Real navigation that only works once its click handler exists, so it
   stays out of the page until pitch.js says so. `has-js`, not `has-fx`:
   under reduced motion the rail is still useful — it just jumps instead of
   gliding. */
.rail {
  display: none;
}

.has-js .rail {
  position: fixed;
  z-index: 40;
  top: 50%;
  right: clamp(0.6rem, 1.4vw, 1.25rem);
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.7rem;
}

.rail-dot {
  position: relative;
  display: block;
  width: 28px;
  height: 24px;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
  border-radius: var(--r-control);
}

.rail-dot::after {
  content: "";
  position: absolute;
  top: 50%;
  right: 9px;
  width: 10px;
  height: 10px;
  margin-top: -5px;
  border-radius: 50%;
  border: 1px solid var(--line);
  transition:
    background-color 200ms ease,
    border-color 200ms ease;
}

.rail-dot:hover::after,
.rail-dot:focus-visible::after {
  border-color: var(--muted);
}

.rail-dot.is-current::after {
  background: var(--accent);
  border-color: var(--accent);
}

.rail-dot:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

.rail-tip {
  position: absolute;
  top: 50%;
  right: 26px;
  transform: translateY(-50%);
  padding: 0.3rem 0.6rem;
  border-radius: 999px;
  border: 1px solid var(--line-soft);
  background: rgba(11, 11, 12, 0.9);
  color: var(--paper);
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  white-space: nowrap;
  opacity: 0;
  pointer-events: none;
  transition: opacity 160ms ease;
}

.rail-dot:hover .rail-tip,
.rail-dot:focus-visible .rail-tip {
  opacity: 1;
}

/* ────────────────────────────────────────────────────────────────────────
   Scenes
   ──────────────────────────────────────────────────────────────────────── */

/* A scene is one screen of the pitch and nothing more. It used to be a tall
   scroll runway with a stage pinned inside it, because the animation was
   scrubbed and needed distance to scrub through; now the animation plays on
   its own clock, so the distance has no job and the scene is simply as tall
   as a screen — or as tall as its own content, on the two scenes (the
   comparison and the "what we ask" list) that ask for more.

   `position: relative` is load-bearing: the customers photograph is
   absolutely positioned against it. */
.scene {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
}

.stage {
  position: relative;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: clamp(0.9rem, 2.2vh, 1.75rem);
  padding: calc(var(--topbar-h) + clamp(0.75rem, 2.5vh, 2rem)) var(--stage-pad-x)
    clamp(1.25rem, 4vh, 3rem);
}

.stage > * {
  width: 100%;
  max-width: var(--stage-max);
}

.scene-head {
  text-align: center;
}

.scene-head h2 {
  font-size: clamp(2rem, 5vw, 4rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.02;
  text-wrap: balance;
  margin: 0;
}

/* ────────────────────────────────────────────────────────────────────────
   The timeline system
   ──────────────────────────────────────────────────────────────────────── */

/* Every animated element carries `fx` and an inline window
   (style="--from:.1;--to:.3") naming the slice of its scene it belongs to.
   Those are the same numbers the scrubbed version was tuned with; what
   changed is what they are a fraction OF. They used to be a fraction of
   the scene's scroll progress. They are now a fraction of --scene-dur, and
   --from becomes an animation-delay.

   Most elements only need --from: they arrive at a moment, over the shared
   --fx-dur. --to is read only by the few things whose window is a SPAN
   rather than a moment — the track's fill and Noah riding it, which have to
   take as long as the span says — and by the counting totals in pitch.js.

   --from/--to are declared on `.fx` itself rather than inherited, so a
   windowed element nested inside another one starts from the defaults
   rather than quietly borrowing its parent's timing. --off is the opposite
   case: the receipt pointers deliberately read their row's value. */
.fx {
  --from: 0;
  --to: 1;
  /* How long a spanning element takes: its own window, in milliseconds. */
  --fx-span: calc((var(--to) - var(--from)) * var(--scene-dur));
}

/* The resting state, held from the moment `has-fx` lands until the scene
   is played. Restated as declarations rather than as a keyframe so that an
   element whose scene is never reached — a phone that stops half way down
   the page — is simply not there yet, rather than mid-animation. */
.has-fx .fx {
  opacity: 0;
  transform: translateY(28px);
}

.has-fx .fx--left {
  transform: translateX(-40px);
}

.has-fx .fx--right {
  transform: translateX(40px);
}

.has-fx .fx--scale {
  transform: scale(0.92);
}

/* Bars and rules: they draw themselves rather than arriving from somewhere,
   so they are fully opaque from the first pixel. */
.fx--grow {
  transform-origin: left center;
}

.has-fx .fx--grow {
  opacity: 1;
  transform: scaleX(0);
}

/* A shorter rise for elements in tight stacks — receipt rows and list lines
   sliding the full 28px paint over their neighbours on the way in. */
.has-fx .fx--near {
  transform: translateY(12px);
}

/* Carries a window but paints nothing by itself: the track's step cards
   steer their own colour, and Noah steers his position, each from a
   keyframe of its own hung on the same --from. */
.has-fx .fx--bare {
  opacity: 1;
  transform: none;
}

/* The play trigger. `is-playing` lands on a PLAY ROOT — normally the scene,
   but the comparison hands the job to whichever example is on screen, so
   that a chip can replay one example's build without touching the other's.
   pitch.js finds the roots from the markup rather than by name.

   `both` fill matters at both ends: backwards it holds the start state
   through the delay, and forwards it holds the finished state until the
   hand-back below takes over. */
.has-fx .is-playing .fx {
  animation: fx-in var(--fx-dur) var(--fx-ease) both;
  animation-delay: calc(var(--from) * var(--scene-dur));
}

.has-fx .is-playing .fx--left {
  animation-name: fx-in-left;
}

.has-fx .is-playing .fx--right {
  animation-name: fx-in-right;
}

.has-fx .is-playing .fx--scale {
  animation-name: fx-in-scale;
}

.has-fx .is-playing .fx--near {
  animation-name: fx-in-near;
}

/* A drawn bar takes its whole window rather than the shared duration: the
   window is how long the line should be drawing for, not when it starts. */
.has-fx .is-playing .fx--grow {
  animation-name: fx-in-grow;
  animation-duration: var(--fx-span);
}

/* Timing only. An animation is still declared, so `fx--bare` behaves like
   every other window and the elements hanging keyframes off it start at
   the same instant. */
.has-fx .is-playing .fx--bare {
  animation-name: fx-in-bare;
}

@keyframes fx-in {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fx-in-near {
  from {
    opacity: 0;
    transform: translateY(12px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fx-in-left {
  from {
    opacity: 0;
    transform: translateX(-40px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fx-in-right {
  from {
    opacity: 0;
    transform: translateX(40px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fx-in-scale {
  from {
    opacity: 0;
    transform: scale(0.92);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes fx-in-grow {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

@keyframes fx-in-bare {
  from {
    opacity: 1;
  }
  to {
    opacity: 1;
  }
}

/* Once the run is over pitch.js swaps `is-playing` for `is-played`, which
   drops the animation and hands the element back to the state the page
   ships in. Two things need that. A filled animation outranks every
   ordinary declaration, so the badges' hover lift would be dead for the
   rest of the session; and "settled" ought to mean the finished stylesheet
   rather than the last frame of something still running. The swap happens
   at the end of the run, so the two states are pixel-identical and there
   is nothing to see. */
.has-fx .is-played .fx,
.has-fx .is-played .ptr::before,
.has-fx .is-played .ptr::after,
.has-fx .is-played .hl > span {
  animation: none;
  opacity: 1;
  transform: none;
}

.has-fx .is-played .fx--grow {
  transform: scaleX(1);
}

/* Promoted only while the scene is the one on screen. Left on for the
   whole page it would hold a compositor layer for every element at once. */
.has-fx .scene.is-active .fx {
  will-change: transform, opacity;
}

/* Buttons and badges ship with a hover lift on `transform`. Without this
   the moment `has-fx` lands would ease every one of them 28px downwards
   into its resting position instead of just putting it there. */
.btn.fx,
.badge.fx {
  transition-property: background-color, border-color;
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 0 — the hero
   ──────────────────────────────────────────────────────────────────────── */

/* The soft glow behind the cutout is wider than the cutout on purpose, and
   the entrance below starts the whole group 6vw to the right — between them
   they reach past the viewport. Clipped at the stage rather than shrunk:
   the glow's whole job is to fall off softly, and it is already transparent
   by the time it meets this edge. */
[data-scene="hero"] > .stage {
  overflow: hidden;
}

/* Wider than the rest of the page: the hero's job is to be the biggest
   thing anyone sees, and 78rem is not enough for two lines of it. */
.hero {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  align-items: center;
  gap: clamp(1.5rem, 4vw, 3rem);
  max-width: 88rem;
}

/* Sized so "Robot delivery." holds one line in the text column at every
   width — the headline is two lines, and a third would break the shape. */
.hero h1 {
  font-size: clamp(3rem, 7.4vw, 6.6rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 0.95;
  text-wrap: balance;
  margin: 0 0 clamp(0.6rem, 1.8vh, 1.1rem);
}

.hero h1 .hero-accent {
  display: block;
  color: var(--accent);
}

.hero-sub {
  font-size: clamp(1.1rem, 2vw, 1.5rem);
  color: var(--muted);
  line-height: 1.35;
  margin: 0 0 clamp(1.1rem, 3vh, 2rem);
  max-width: 24ch;
}

.hero-noah {
  position: relative;
  display: flex;
  justify-content: center;
}

/* The glow is a sibling of the cutout, not a background on it: the PNG has
   an alpha channel and a background would show through the transparent
   corners as a hard square. */
.hero-noah::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: 118%;
  aspect-ratio: 1;
  transform: translate(-50%, -50%);
  background: radial-gradient(closest-side, rgba(255, 159, 67, 0.28), transparent);
  pointer-events: none;
}

.hero-noah img {
  position: relative;
  display: block;
  /* The 100% term keeps the cutout inside its own column; without it a
     wide viewport hands it 640px in a 490px slot and the grid gives way. */
  width: min(46vw, 640px, 100%);
  height: auto;
}

/* The hero is the one scene that is never scroll-gated. It is what a room
   sees the moment the page opens — usually on a projector, before the
   presenter has touched the trackpad — so the eyebrow, the headline, the
   sub, the button and Noah are all complete at scrollY 0. A scroll window
   here would mean opening the pitch on a headline with no promise under it
   and no robot beside it.

   What `has-fx` adds instead is a one-shot entrance on load: each piece
   rises in on its own delay and then stays put, so the page still opens
   with movement without holding anything back behind a scroll nobody has
   made yet. The stagger index is `--i`, set inline in the markup, because
   the order the hero assembles in is a property of the markup rather than
   of this file. Without `has-fx` — reduced motion, or no JS — none of this
   matches and the default state is already the finished one. */
.has-fx .hero .eyebrow,
.has-fx .hero h1,
.has-fx .hero-sub,
.has-fx .hero .btn,
.has-fx .hero-noah {
  animation: hero-in 900ms cubic-bezier(0.2, 0.7, 0.2, 1) both;
  animation-delay: calc(var(--i, 0) * 110ms);
}

/* Noah arrives from the side rather than from below, with a shade of drift
   on top of the slide: the cutout settles level rather than sliding in
   flat, which is the difference between "animated" and "placed". He starts
   with the sub line and takes longer than anything else to land, so the
   button — the last thing to arrive — is what the eye is left on. */
.has-fx .hero-noah {
  animation-name: hero-noah-in;
  animation-duration: 1100ms;
}

@keyframes hero-in {
  from {
    opacity: 0;
    transform: translateY(18px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes hero-noah-in {
  from {
    opacity: 0;
    transform: translateX(6vw) rotate(2deg);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: clamp(0.9rem, 3vh, 2rem);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  width: auto;
  max-width: none;
  color: var(--faint);
  font-size: 0.65rem;
  font-weight: 700;
  letter-spacing: 0.2em;
  text-transform: uppercase;
}

/* One of the four things still tied to the wheel rather than to a clock: a
   cue that says "scroll" has to answer the scroll itself. --cue-out is the
   fraction of the fade pitch.js writes on the hero, 0 at rest and 1 once
   the page has moved about a third of a screen. */
.has-fx .scroll-cue {
  opacity: calc(1 - var(--cue-out, 0));
}

.scroll-cue svg {
  width: 18px;
  height: 18px;
  animation: cue-bob 1.6s ease-in-out infinite;
}

@keyframes cue-bob {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(5px);
  }
}

/* The hero stacks well before the rest of the page does. Two columns stop
   working once the text column is narrower than one line of the headline,
   which happens around 760px — long before the receipts or the track need
   to give way. Stacked, the headline is measured against the full width,
   so it takes a steeper scale of its own. */
@media (max-width: 760px) {
  .hero {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
    gap: clamp(1rem, 3vh, 2rem);
  }

  .hero h1 {
    font-size: clamp(2.2rem, 12.6vw, 5.5rem);
  }

  .hero-sub {
    max-width: none;
  }

  .hero-noah img {
    width: min(70vw, 460px);
  }
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 1 — the comparison
   ──────────────────────────────────────────────────────────────────────── */

/* Stacked — no JS, or reduced motion — the pair of examples is taller than
   the screen it would be centred in, and a centred grid that overflows
   loses the top of its content above the viewport edge. Centred only once
   `has-fx` has put the examples in one cell, where the scene is a screen
   again. */
[data-scene="receipts"] > .stage {
  align-content: start;
}

.has-fx [data-scene="receipts"] > .stage {
  align-content: center;
}

/* The category, set as the category sets itself: heavy, geometric, tight,
   and red. It is an allusion to a kind of app, not to any one of them —
   the word stays generic here and on the card, which is labelled
   "Delivery app" in words for the same reason. */
.apps {
  color: var(--app-red);
  font-family: var(--app-face);
  font-weight: 800;
  /* Tighter than the headline's own -0.03em: at this size the word has to
     lock up as a mark rather than read as one more word in the sentence. */
  letter-spacing: -0.045em;
}

/* ── The examples ────────────────────────────────────────────────────── */

/* Presenter navigation, and the same bargain as the rail: a button that
   swaps examples is useless until its handler exists, so it stays out of
   the page until pitch.js says so. Nothing is lost without it — every
   example is already on the page, one under the next. */
.ex-chips {
  display: none;
}

.has-js .ex-chips {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.4rem;
}

.ex-chip {
  padding: 0.35rem 0.95rem;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  font: inherit;
  font-size: 0.8rem;
  font-weight: 700;
  cursor: pointer;
  transition:
    background-color 160ms ease,
    border-color 160ms ease,
    color 160ms ease;
}

.ex-chip:hover {
  border-color: var(--muted);
  color: var(--paper);
}

.ex-chip:focus-visible {
  outline: 2px solid var(--focus-ring);
  outline-offset: 2px;
}

/* Filled rather than outlined: which restaurant is on screen has to be
   legible from the back of a room, and the outline-and-dot idiom is
   already spoken for by the rail. */
.ex-chip[aria-current] {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
}

/* The default is the honest one: every example on the page, one after
   another, each with its own footnote. `has-fx` is what collapses them
   into a single cell for the chips to choose between. */
.examples {
  display: grid;
  gap: clamp(2rem, 7vh, 4rem);
}

.has-fx .examples {
  gap: 0;
}

/* Stacked in one cell, one of them current. `visibility` rather than only
   opacity, so the example nobody is looking at is out of the tab order and
   out of the accessibility tree as well as invisible; it is delayed by the
   length of the cross-fade so the outgoing example is still paintable while
   it dissolves. */
.has-fx .example {
  grid-area: 1 / 1;
  opacity: 0;
  visibility: hidden;
  transition:
    opacity 300ms ease,
    visibility 0s linear 300ms;
}

.has-fx .example.is-current {
  opacity: 1;
  visibility: visible;
  transition:
    opacity 300ms ease,
    visibility 0s;
}

.example-in {
  display: grid;
  justify-items: center;
  gap: clamp(0.7rem, 2vh, 1.4rem);
}

/* The callout is deliberately not in this list: it is a pill that sizes to
   its own text and is centred by the grid. */
.example-head,
.example-in > .receipts,
.example-in > .receipt-note {
  width: 100%;
}

.example-head {
  text-align: center;
}

.example-title {
  font-size: clamp(1.1rem, 2.2vw, 1.7rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.15;
  text-wrap: balance;
  margin: 0;
}

.example-sub {
  font-size: clamp(0.78rem, 1.15vw, 0.98rem);
  color: var(--muted);
  margin: 0.2rem 0 0;
}

/* ── The two checkouts ───────────────────────────────────────────────── */

.receipts {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(0.8rem, 2vw, 1.6rem);
  align-items: start;
  max-width: 58rem;
}

.receipt-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding-bottom: 0.6rem;
  border-bottom: 1px solid var(--line-soft);
}

.receipt-brand {
  font-size: clamp(0.95rem, 1.5vw, 1.15rem);
  font-weight: 800;
  letter-spacing: -0.01em;
  margin: 0;
}

.receipt-tag {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0;
  white-space: nowrap;
}

/* The loud one. A solid red bar bled out to the card's edges — the
   negative margin is the card's own padding, which is why that padding is
   a variable — carrying the category's name and the tag that used to sit
   on a hairline. Everything below the bar stays paper: the numbers are the
   argument, and they have to read as a receipt rather than as a screen. */
.card--app .receipt-head {
  margin: calc(var(--pad) * -1) calc(var(--pad) * -1) 0.7rem;
  padding: 0.5rem var(--pad) 0.55rem;
  border-radius: var(--r-card) var(--r-card) 0 0;
  border-bottom: 0;
  background: var(--app-red);
  color: var(--paper);
}

.card--app .receipt-brand {
  font-family: var(--app-face);
  letter-spacing: -0.02em;
}

/* --faint here is the paper scope's ink tier, which on red is unreadable.
   Paper on --app-red measures 4.61:1, which the tag needs at this size. */
.card--app .receipt-tag {
  color: var(--paper);
}

/* The line the whole card is built to land on. */
.card--app .receipt-total {
  color: var(--app-red);
}

/* The quiet one, and quieter than it was: no bar, no colour, hairlines,
   and a name that sits at text weight instead of answering the red
   opposite. The orange is saved for the savings pill underneath — the
   argument is that the plain checkout is the cheaper one, which only lands
   if it looks plain. */
.card--notip .receipt-brand {
  font-weight: 700;
  letter-spacing: 0;
}

.lines {
  list-style: none;
  margin: 0;
  padding: 0;
}

.line {
  /* The pointer lives in this padding rather than in the gutter outside the
     card, so it never has to reach across a stacked layout on a phone. */
  position: relative;
  padding: 0.5rem 0 0.5rem 34px;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  border-bottom: 1px solid var(--line-soft);
  font-size: clamp(0.82rem, 1.05vw, 0.95rem);
  /* Past the end of the timeline: a pointer with no successor never dims. */
  --off: 2;
}

.line-label {
  display: flex;
  flex-direction: column;
}

.line-label small {
  font-size: 0.78em;
  color: var(--faint);
  margin-top: 0.1rem;
}

.line-amount {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
}

.receipt-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.75rem;
  padding: 0.75rem 0 0 34px;
  font-size: clamp(1.05rem, 1.9vw, 1.5rem);
  font-weight: 800;
  letter-spacing: -0.02em;
}

.receipt-total .line-amount {
  font-size: inherit;
  font-weight: 800;
}

/* The pointing the pitch is built around: a connector drawing itself
   leftwards into a dot, arriving with the row it belongs to. Its colour is
   the card's, so the fee rows on the delivery-app card point in red and
   nothing has to know which card it is in. */
.ptr {
  position: absolute;
  left: 0;
  top: 50%;
  width: 28px;
  height: 10px;
  margin-top: -5px;
  pointer-events: none;
  --ptr-color: var(--accent);
}

.card--app .ptr {
  --ptr-color: var(--app-red);
}

.ptr::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 18px;
  height: 2px;
  margin-top: -1px;
  background: var(--ptr-color);
  transform-origin: right center;
}

.ptr::after {
  content: "";
  position: absolute;
  right: 0;
  top: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--ptr-color);
}

/* Every pointer but the last has handed off by the time the run ends, so
   the finished pair — the one a visitor without JS sees — shows exactly one
   live pointer, on the line the argument lands on. */
.ptr--spent::before,
.ptr--spent::after {
  background: var(--faint);
}

/* The window comes from the row: the pointer and its line are the same
   event, and duplicating the numbers in the markup is how they drift
   apart. --off is the row's hand-off, which is the next row's --from — so
   a pointer lights up with its line, then dims as the line below it
   arrives, and the pair walks down the receipt. */
.has-fx .ptr::before {
  transform: scaleX(0);
}

.has-fx .ptr::after {
  transform: scale(0);
}

.has-fx .is-playing .ptr::before,
.has-fx .is-playing .ptr::after {
  animation-duration: var(--fx-dur);
  animation-timing-function: var(--fx-ease);
  animation-fill-mode: both;
  animation-delay: calc(var(--from) * var(--scene-dur));
}

.has-fx .is-playing .ptr::before {
  animation-name: ptr-draw;
}

.has-fx .is-playing .ptr::after {
  animation-name: ptr-pop;
}

/* The hand-off, as a second animation on the same pseudo-element: it drives
   `background` while the first drives `transform`, so the two compose
   instead of one cancelling the other. Only spent pointers carry it — the
   last one on a card has no successor and never dims. */
.has-fx .is-playing .ptr--spent::before,
.has-fx .is-playing .ptr--spent::after {
  animation-duration: var(--fx-dur), 320ms;
  animation-timing-function: var(--fx-ease), linear;
  animation-fill-mode: both, both;
  animation-delay: calc(var(--from) * var(--scene-dur)), calc(var(--off) * var(--scene-dur));
}

.has-fx .is-playing .ptr--spent::before {
  animation-name: ptr-draw, ptr-spend;
}

.has-fx .is-playing .ptr--spent::after {
  animation-name: ptr-pop, ptr-spend;
}

@keyframes ptr-draw {
  from {
    transform: scaleX(0);
  }
  to {
    transform: scaleX(1);
  }
}

@keyframes ptr-pop {
  from {
    transform: scale(0);
  }
  to {
    transform: scale(1);
  }
}

@keyframes ptr-spend {
  from {
    background: var(--ptr-color);
  }
  to {
    background: var(--faint);
  }
}

.callout {
  position: relative;
  width: auto;
  max-width: min(100%, 40rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.1rem;
  padding: clamp(0.6rem, 1.6vh, 1rem) clamp(1.2rem, 3vw, 2.2rem);
  border-radius: 999px;
  background: var(--accent);
  color: var(--ink);
  text-align: center;
}

.callout-main {
  font-size: clamp(1.05rem, 2.6vw, 1.9rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0;
}

.callout-sub {
  font-size: clamp(0.72rem, 1.2vw, 0.9rem);
  font-weight: 700;
  margin: 0;
  color: rgba(11, 11, 12, 0.72);
}

/* Points up at the NoTip total. The pill is centred under both receipts,
   so on a wide screen the arrow has to stand off to the right of it to
   land under the right-hand card; stacked on a phone it comes back inside. */
.callout-arrow {
  position: absolute;
  bottom: 100%;
  left: 100%;
  margin-left: clamp(0.75rem, 3vw, 3rem);
  width: 2px;
  height: clamp(16px, 3vh, 30px);
  background: var(--accent);
}

.callout-arrow::before {
  content: "";
  position: absolute;
  top: 0;
  left: 50%;
  width: 9px;
  height: 9px;
  border-left: 2px solid var(--accent);
  border-top: 2px solid var(--accent);
  transform: translateX(-50%) rotate(45deg);
}

/* One footnote per example, never one under the pair: these two say
   different things, and the captured-order wording under modelled figures
   would be a claim the numbers do not support. */
.receipt-note {
  max-width: 48rem;
  text-align: center;
  margin: 0;
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 2 — customers
   ──────────────────────────────────────────────────────────────────────── */

[data-scene="customers"] > .stage {
  overflow: hidden;
}

.photo,
.photo-shade {
  position: absolute;
  inset: 0;
  max-width: none;
}

.photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transform: scale(1.08);
}

/* The last thing on the page still tied to the wheel, and the reason it is
   worth keeping: a photograph that shifts against its frame as you scroll
   past reads as depth, and unlike everything else here it costs no extra
   scroll length to do it. --pv is the fraction of the viewport the scene
   has crossed, written by pitch.js; the 6% either side of centre is small
   enough that nobody notices it happening. */
.has-fx .photo {
  transform: scale(1.08) translateY(calc((0.5 - var(--pv, 0.5)) * 6%));
}

/* Heaviest toward the bottom-left, which is where the heading and the
   caption sit. The photograph is a bright sunlit lawn; anything lighter
   than this and white text on it is a guess rather than a read. */
.photo-shade {
  background:
    linear-gradient(to top, rgba(11, 11, 12, 0.95), rgba(11, 11, 12, 0.28) 60%, rgba(11, 11, 12, 0.06) 88%),
    linear-gradient(to right, rgba(11, 11, 12, 0.92), rgba(11, 11, 12, 0.42) 42%, transparent 72%);
}

/* Everything that is not the photograph sits above both layers. The tags
   are already positioned; they get their stacking from .tags rather than
   from a `position` here, which would undo their placement. */
[data-scene="customers"] .scene-head {
  position: relative;
  z-index: 2;
  justify-self: start;
  text-align: left;
  max-width: 32rem;
}

/* --faint is tuned for the flat night canvas; over a photograph the
   eyebrow needs the next tier up to hold its own. */
[data-scene="customers"] .eyebrow {
  color: var(--muted);
}

.tags,
.photo-caption {
  z-index: 2;
}

/* On a wide screen this is only a positioning context for the tags; on a
   phone (below) it becomes a plain stacked column. */
.tags {
  position: absolute;
  inset: 0;
  max-width: none;
  pointer-events: none;
}

.tag {
  position: absolute;
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  max-width: min(20rem, 62vw);
  padding: 0.5rem 0.9rem;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-size: clamp(0.78rem, 1.15vw, 0.95rem);
  font-weight: 700;
  line-height: 1.25;
  box-shadow: var(--shadow-pop);
}

/* A short leader line and a dot, angled at the subject of the photograph. */
.tag-pin {
  position: absolute;
  top: 60%;
  width: 46px;
  height: 2px;
  background: var(--accent);
  transform-origin: left center;
  transform: rotate(var(--angle, 30deg));
  pointer-events: none;
}

.tag-pin::after {
  content: "";
  position: absolute;
  right: -4px;
  top: 50%;
  width: 9px;
  height: 9px;
  margin-top: -4.5px;
  border-radius: 50%;
  background: var(--accent);
}

.tag--a {
  top: 24%;
  left: 6%;
}

.tag--a .tag-pin {
  left: 100%;
  --angle: 34deg;
}

.tag--b {
  top: 47%;
  right: 7%;
}

.tag--b .tag-pin {
  left: 0;
  --angle: 152deg;
}

.tag--c {
  bottom: 26%;
  left: 12%;
}

.tag--c .tag-pin {
  left: 100%;
  --angle: -26deg;
}

.photo-caption {
  position: absolute;
  right: var(--stage-pad-x);
  bottom: clamp(0.9rem, 3vh, 2rem);
  width: auto;
  max-width: none;
  margin: 0;
  color: var(--muted);
  font-size: 0.72rem;
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 3 — how it works
   ──────────────────────────────────────────────────────────────────────── */

/* The rail and Noah live in a 44px band above the cards, so the line never
   has to cross a card border to get to the next dot. */
.track {
  position: relative;
  padding-top: 44px;
  max-width: 64rem;
}

.track-rail,
.track-fill {
  position: absolute;
  top: 21px;
  /* Ends on the centre of the first and last card, not on the page edge. */
  left: 16.667%;
  right: 16.667%;
  height: 2px;
  border-radius: 999px;
}

.track-rail {
  background: var(--line-soft);
}

.track-fill {
  background: var(--accent);
}

/* Noah rides the rail from the first dot to the last — the same 16.667%…
   83.333% span the fill draws across, so he always sits on its head. */
.track-noah {
  position: absolute;
  top: 0;
  left: 83.333%;
  width: 44px;
  height: 44px;
  margin-left: -22px;
}

/* Noah takes the whole window rather than the shared duration, for the
   reason the fill does: the window says how long the ride lasts, and the
   two have to arrive at the last dot together. `left` rather than a
   transform because the distance is a percentage of the track, which a
   translate would read as a percentage of Noah. */
.has-fx .track-noah {
  left: 16.667%;
}

.has-fx .is-playing .track-noah {
  animation: noah-ride var(--fx-span) var(--fx-ease) both;
  animation-delay: calc(var(--from) * var(--scene-dur));
}

/* Noah is the one element whose finished state is not the resting one — he
   ends the ride at the far dot — so the hand-back has to say where. */
.has-fx .is-played .track-noah {
  left: 83.333%;
}

@keyframes noah-ride {
  from {
    left: 16.667%;
  }
  to {
    left: 83.333%;
  }
}

.steps {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.75rem, 2vw, 1.5rem);
  margin: 0;
  padding: 0;
}

.step {
  position: relative;
  border: 1px solid var(--paper);
  border-radius: var(--r-card);
  padding: clamp(1.1rem, 2.4vw, 1.75rem);
}

/* The three cards used to steer their own colour from --k. Now each one
   hangs a keyframe off the same --from its `fx--bare` window carries, so a
   card, its dot and its title all warm up together the moment the line
   reaches them. --from inherits from the card, which is why the dot and
   the title do not have to repeat it. */
.has-fx .step {
  border-color: var(--line-soft);
}

.has-fx .is-playing .step {
  animation: step-card-in var(--fx-dur) var(--fx-ease) both;
  animation-delay: calc(var(--from) * var(--scene-dur));
}

.has-fx .is-played .step {
  border-color: var(--paper);
}

@keyframes step-card-in {
  from {
    border-color: var(--line-soft);
  }
  to {
    border-color: var(--paper);
  }
}

.step-dot {
  position: absolute;
  top: -22px;
  left: 50%;
  width: 14px;
  height: 14px;
  margin: -7px 0 0 -7px;
  border-radius: 50%;
  background: var(--accent);
}

.has-fx .step-dot {
  background: var(--line);
}

.has-fx .is-playing .step-dot {
  animation: step-dot-in var(--fx-dur) var(--fx-ease) both;
  animation-delay: calc(var(--from) * var(--scene-dur));
}

.has-fx .is-played .step-dot {
  background: var(--accent);
}

@keyframes step-dot-in {
  from {
    background: var(--line);
  }
  to {
    background: var(--accent);
  }
}

.step-title {
  font-size: clamp(1.05rem, 2vw, 1.6rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin: 0 0 0.4rem;
  color: var(--paper);
}

.has-fx .step-title {
  color: var(--line);
}

.has-fx .is-playing .step-title {
  animation: step-title-in var(--fx-dur) var(--fx-ease) both;
  animation-delay: calc(var(--from) * var(--scene-dur));
}

.has-fx .is-played .step-title {
  color: var(--paper);
}

@keyframes step-title-in {
  from {
    color: var(--line);
  }
  to {
    color: var(--paper);
  }
}

.step-sub {
  font-size: clamp(0.85rem, 1.2vw, 1rem);
  line-height: 1.5;
  color: var(--muted);
  margin: 0;
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 4 — zero work
   ──────────────────────────────────────────────────────────────────────── */

/* One statement, set close. Each row used to hold 28vh of its own so that
   only one or two sat in the scroll band at a time; with the scrub gone
   that spacing had no job left, and the block now reads as a single stacked
   sentence inside one screen at 1440 and at 390. The leading does the
   separating — the gap is a hair, not a margin. */
.zero-list {
  list-style: none;
  display: grid;
  gap: clamp(0.05rem, 0.5vh, 0.3rem);
  margin: 0;
  padding: 0;
  max-width: var(--stage-max);
}

.hl {
  margin: 0;
  font-size: clamp(2rem, 10.5vw, 5rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.14;
  color: var(--paper);
}

/* The underline follows the words rather than the row. It is a background
   on an inline span, not a bar under a block, because the closing line
   wraps at every width this page is read at — `box-decoration-break: clone`
   gives each line fragment its own copy, each only as wide as its own
   words, instead of one rule running out to the far edge of an empty
   second line. */
.hl > span {
  padding-bottom: 0.06em;
  box-decoration-break: clone;
  -webkit-box-decoration-break: clone;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 100% 0.06em;
}

.hl--accent {
  color: var(--accent);
  font-size: clamp(2.2rem, 11.2vw, 5.5rem);
}

/* Orange on orange would be an invisible rule; the closer's bar is paper. */
.hl--accent > span {
  background-image: linear-gradient(var(--paper), var(--paper));
}

/* The underline grows under each line just after the line itself lands —
   the row's own --from plus a beat, so the eye reads the words and then
   sees them get underscored, rather than both at once. */
.has-fx .hl > span {
  background-size: 0% 0.06em;
}

.has-fx .is-playing .hl > span {
  animation: hl-underline var(--fx-dur) var(--fx-ease) both;
  animation-delay: calc(var(--from) * var(--scene-dur) + 160ms);
}

.has-fx .is-played .hl > span {
  background-size: 100% 0.06em;
}

@keyframes hl-underline {
  from {
    background-size: 0% 0.06em;
  }
  to {
    background-size: 100% 0.06em;
  }
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 5 — exposure
   ──────────────────────────────────────────────────────────────────────── */

/* Both columns shrink to their content and the pair is centred, so the
   phone and the tiles read as one object rather than as a left-hand mock
   with an empty half beside it. */
.exposure {
  display: grid;
  grid-template-columns: auto auto;
  justify-content: center;
  align-items: center;
  gap: clamp(1.5rem, 5vw, 4.5rem);
  max-width: 62rem;
}

.phone {
  position: relative;
  width: clamp(190px, 30vh, 300px);
  aspect-ratio: 300 / 600;
  border: 1px solid var(--line);
  border-radius: 34px;
  background: var(--card);
  box-shadow: var(--shadow-pop);
}

.phone::before {
  content: "";
  position: absolute;
  z-index: 1;
  top: 14px;
  left: 50%;
  width: 74px;
  height: 5px;
  margin-left: -37px;
  border-radius: 999px;
  background: var(--line-soft);
}

/* Inset rather than a percentage height: the frame's own height comes from
   an aspect-ratio, and a percentage against that resolves inconsistently. */
.phone-screen {
  position: absolute;
  inset: 10px;
  padding: 30px 10px 10px;
  border-radius: 26px;
  background: var(--well);
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;
}

.phone-card {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  column-gap: 0.55rem;
  row-gap: 0.1rem;
  padding: 0.7rem;
  border-radius: 12px;
}

.phone-star {
  grid-column: 1;
  grid-row: 1 / span 2;
  align-self: center;
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent);
  color: var(--paper-bright);
  font-size: 0.85rem;
}

.phone-name {
  grid-column: 2;
  grid-row: 1;
  align-self: end;
  font-size: 0.85rem;
  font-weight: 800;
  margin: 0;
}

.phone-sub {
  grid-column: 2;
  grid-row: 2;
  align-self: start;
  font-size: 0.68rem;
  color: var(--accent-ink);
  font-weight: 700;
  margin: 0;
}

.chips {
  grid-column: 1 / -1;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0.35rem 0 0;
  padding: 0;
}

.chips li {
  padding: 0.15rem 0.4rem;
  border-radius: 999px;
  background: var(--well);
  color: var(--ink-muted);
  font-size: 0.6rem;
  font-weight: 700;
}

.phone-order {
  grid-column: 1 / -1;
  justify-self: start;
  margin: 0.15rem 0 0;
  padding: 0.3rem 0.85rem;
  border-radius: 999px;
  background: var(--accent);
  color: var(--ink);
  font-size: 0.7rem;
  font-weight: 800;
}

/* Two nameless grey rows: the list has to read as a list without inventing
   restaurants that have not signed anything. */
.phone-skel {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.phone-skel span {
  display: block;
  height: 46px;
  border-radius: 12px;
  background: var(--card);
}

.tiles {
  display: grid;
  gap: 0.7rem;
  max-width: 22rem;
}

.tiles-more {
  color: var(--muted);
  font-size: clamp(0.9rem, 1.3vw, 1.05rem);
  margin: 0.3rem 0 0;
}

/* ────────────────────────────────────────────────────────────────────────
   Scene 6 — participate
   ──────────────────────────────────────────────────────────────────────── */

.closer {
  text-align: center;
  display: grid;
  justify-items: center;
  gap: clamp(0.9rem, 2.5vh, 1.6rem);
  max-width: 52rem;
}

.closer h2 {
  font-size: clamp(2.5rem, 7vw, 6.5rem);
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 0.98;
  text-wrap: balance;
  margin: 0;
}

.closer h2 .brand {
  color: var(--accent);
}

.closer-sub {
  font-size: clamp(1.05rem, 1.8vw, 1.4rem);
  color: var(--muted);
  margin: 0;
}

.closer .micro {
  margin: 0;
}

[data-scene="cta"] .site-footer {
  margin-top: clamp(1.5rem, 5vh, 3rem);
}

/* ────────────────────────────────────────────────────────────────────────
   Phones
   ──────────────────────────────────────────────────────────────────────── */

@media (max-width: 600px) {
  :root {
    --topbar-h: 3.25rem;
  }

  .has-js .rail {
    display: none;
  }

  .hero-noah img {
    width: 78vw;
    max-width: 78vw;
  }

  /* One screen has to hold a heading, an example's own heading, two
     receipts, the callout and the footnote. Everything below tightens the
     comparison enough that the headline is still on screen when the totals
     land. */
  .stage {
    gap: 0.45rem;
    padding: calc(var(--topbar-h) + 0.45rem) var(--stage-pad-x) 0.75rem;
  }

  /* The scene's headline is a sentence rather than a phrase now, and at the
     shared scale it costs four lines before the receipts start. */
  [data-scene="receipts"] .scene-head h2 {
    font-size: clamp(1.25rem, 5.6vw, 2rem);
  }

  [data-scene="receipts"] .eyebrow {
    margin-bottom: 0.5rem;
  }

  .example-in {
    gap: 0.4rem;
  }

  .example-sub {
    margin-top: 0.1rem;
  }

  /* The pair never stacks, at any width. A reader who can only see one
     receipt at a time is not comparing anything — the argument IS the two
     totals in one glance — so instead of breaking the columns, the pair is
     scaled down as a unit and its rows are tightened vertically. */
  .receipts {
    gap: 0.35rem;
  }

  /* Set through --pad rather than `padding` so the delivery-app card's
     bled header bar keeps meeting the card's edge exactly. */
  [data-scene="receipts"] .stage-card {
    --pad: 0.7rem;
  }

  /* Brand over tag rather than beside it: half of a phone's width has no
     room for "Delivery app" and "their checkout" on one line, and dropping
     either would cost the card the label that keeps it generic. */
  .receipt-head {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.05rem;
    padding-bottom: 0.35rem;
  }

  .receipt-brand {
    font-size: 0.82rem;
  }

  .receipt-tag {
    font-size: 0.5rem;
    letter-spacing: 0.08em;
  }

  [data-scene="receipts"] .card--app .receipt-head {
    margin-bottom: 0.45rem;
    padding: 0.35rem var(--pad) 0.4rem;
  }

  .line {
    padding: 0.26rem 0 0.26rem 32px;
    font-size: 0.76rem;
    gap: 0.35rem;
  }

  .line-label small {
    margin-top: 0;
  }

  .receipt-total {
    padding: 0.35rem 0 0 32px;
    gap: 0.4rem;
    font-size: 0.98rem;
  }

  .receipt-note {
    font-size: 0.66rem;
    line-height: 1.45;
  }

  .callout {
    padding: 0.4rem 1.2rem;
  }

  .callout-arrow {
    left: auto;
    right: 2rem;
    margin-left: 0;
  }

  /* The track turns on its side: the rail runs down the left of a stacked
     column of cards. Equal rows are what keeps the dots on the line, so the
     steps grid is given three explicit fractions rather than auto rows. */
  .track {
    position: relative;
    padding: 0 0 0 44px;
  }

  .track-rail,
  .track-fill {
    top: 16.667%;
    bottom: 16.667%;
    left: 21px;
    right: auto;
    width: 2px;
    height: auto;
  }

  .track-fill {
    transform-origin: center top;
  }

  /* Same two animations as the wide layout, turned through ninety degrees:
     the fill draws downwards and Noah rides down beside it. Named
     separately rather than reusing the horizontal keyframes, because a
     keyframe that moves `left` has nothing to say about a vertical rail. */
  .has-fx .track-fill {
    transform: scaleY(0);
  }

  .has-fx .is-playing .track-fill {
    animation-name: track-fill-down;
  }

  .has-fx .is-played .track-fill {
    transform: scaleY(1);
  }

  @keyframes track-fill-down {
    from {
      transform: scaleY(0);
    }
    to {
      transform: scaleY(1);
    }
  }

  .track-noah {
    top: 83.333%;
    left: 0;
    margin: -22px 0 0;
  }

  .has-fx .track-noah {
    left: 0;
    top: 16.667%;
  }

  .has-fx .is-playing .track-noah {
    animation-name: noah-ride-down;
  }

  .has-fx .is-played .track-noah {
    left: 0;
    top: 83.333%;
  }

  @keyframes noah-ride-down {
    from {
      top: 16.667%;
    }
    to {
      top: 83.333%;
    }
  }

  .steps {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: repeat(3, minmax(0, 1fr));
    gap: 0.6rem;
  }

  .step {
    display: flex;
    flex-direction: column;
    justify-content: center;
  }

  .step-dot {
    top: 50%;
    left: -23px;
  }

  .exposure {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: clamp(1rem, 4vh, 2rem);
  }

  /* Height-relative rather than fixed, the way the wide layout sizes it:
     the phone mock is the tallest single object on the page, and on a
     short screen it is what pushes the badges under the fold. */
  .phone {
    width: clamp(160px, 27vh, 220px);
  }

  .tiles {
    max-width: 20rem;
    width: 100%;
  }

  /* The tags stop pointing at anything useful once the photograph is
     letterboxed into a phone, so they stack down the shaded side instead. */
  [data-scene="customers"] .scene-head {
    text-align: left;
  }

  .tags {
    position: static;
    inset: auto;
    pointer-events: auto;
    width: 100%;
    display: grid;
    gap: 0.5rem;
    justify-items: start;
  }

  .tag {
    position: relative;
    top: auto;
    right: auto;
    bottom: auto;
    left: auto;
    max-width: 100%;
  }

  .tag-pin {
    display: none;
  }

  .photo-caption {
    right: auto;
    left: var(--stage-pad-x);
  }

  /* A phone crops the photograph to a portrait slice, so the bright lawn
     lands directly behind the heading instead of beside it. The wash goes
     heavier across the whole frame rather than just the bottom-left. */
  .photo-shade {
    background:
      linear-gradient(to top, rgba(11, 11, 12, 0.95), rgba(11, 11, 12, 0.55) 55%, rgba(11, 11, 12, 0.2) 88%),
      linear-gradient(to right, rgba(11, 11, 12, 0.72), rgba(11, 11, 12, 0.34) 60%, rgba(11, 11, 12, 0.18));
  }
}

/* ────────────────────────────────────────────────────────────────────────
   Narrow phones
   ──────────────────────────────────────────────────────────────────────── */

/* Below about 440px a receipt column is around 130px wide, and a label and
   an amount stop fitting on one line together. Rather than break the pair
   apart — which would end the comparison — each row puts its amount under
   its label, and the pointer gives up its connector and keeps only the dot:
   at this width the line had nowhere to reach anyway, and the 28px of
   gutter it needed is worth more to the numbers. */
@media (max-width: 440px) {
  .line {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    padding-left: 17px;
  }

  .ptr {
    width: 10px;
    /* The dot alone, sitting on the row's first line rather than on the
       middle of a two-line row. */
    top: 0.62em;
  }

  .ptr::before {
    display: none;
  }

  .receipt-total {
    padding-left: 17px;
    gap: 0.3rem;
    font-size: 0.86rem;
  }

  .line-amount {
    font-size: 1.02em;
  }
}

/* ────────────────────────────────────────────────────────────────────────
   Short viewports
   ──────────────────────────────────────────────────────────────────────── */

/* Nothing is pinned any more, so a scene taller than the screen simply
   scrolls — it no longer buries its own last line under the fold. What is
   still worth defending on a short screen is the comparison's one job:
   both totals and the savings pill visible at the same time. This is the
   last squeeze available before the numbers stop being readable across a
   desk. */
@media (max-width: 600px) and (max-height: 780px) {
  [data-scene="receipts"] .stage-card {
    --pad: 0.55rem;
  }

  [data-scene="receipts"] .line {
    padding-top: 0.2rem;
    padding-bottom: 0.2rem;
  }

  [data-scene="receipts"] .receipt-total {
    padding-top: 0.3rem;
  }

  .receipt-note {
    font-size: 0.62rem;
  }
}

/* ────────────────────────────────────────────────────────────────────────
   Reduced motion
   ──────────────────────────────────────────────────────────────────────── */

/* pitch.js already withholds `has-fx` here, so none of the `.has-fx` rules
   below can normally match — they are the belt to that pair of braces, and
   they restate the finished value of everything the scope would otherwise
   have moved. Restated rather than reverted: `revert-layer` in an unlayered
   sheet falls all the way back to the user-agent value, which would take
   the page's own colours with it.

   `animation: none` leads, because a filled animation outranks any
   declaration a rule below could make, and `is-playing` could in principle
   still be sitting on a scene when the visitor flips the OS setting with
   the page open. */
@media (prefers-reduced-motion: reduce) {
  .has-fx .fx,
  .has-fx .fx--left,
  .has-fx .fx--right,
  .has-fx .fx--scale,
  .has-fx .fx--grow,
  .has-fx .fx--near,
  .has-fx .fx--bare,
  .has-fx .hero-noah,
  .has-fx .scroll-cue,
  .has-fx .ptr::before,
  .has-fx .ptr::after,
  .has-fx .track-noah,
  .has-fx .track-fill,
  .has-fx .step,
  .has-fx .step-dot,
  .has-fx .step-title {
    animation: none;
    opacity: 1;
    transform: none;
  }

  /* The one element whose finished position is not its resting one. */
  .has-fx .track-noah {
    left: 83.333%;
  }

  /* Not covered by the resets above: the underline is a background, and a
     background has no opacity or transform of its own to put back. */
  .has-fx .hl > span {
    animation: none;
    background-size: 100% 0.06em;
  }

  .has-fx .photo {
    transform: scale(1.08);
  }

  /* Every example on the page at once, each under its own footnote, and
     the stage topped out because the pair is taller than the screen. */
  .has-fx .examples {
    gap: clamp(2rem, 7vh, 4rem);
  }

  .has-fx .example {
    grid-area: auto;
    opacity: 1;
    visibility: visible;
    transition: none;
  }

  .has-fx [data-scene="receipts"] > .stage {
    align-content: start;
  }

  .has-fx .hl {
    color: var(--paper);
  }

  .has-fx .hl--accent {
    color: var(--accent);
  }

  .has-fx .step {
    border-color: var(--paper);
  }

  .has-fx .step-dot {
    background: var(--accent);
  }

  .has-fx .step-title {
    color: var(--paper);
  }

  .has-fx .ptr::before,
  .has-fx .ptr::after {
    background: var(--ptr-color);
  }

  .has-fx .ptr--spent::before,
  .has-fx .ptr--spent::after {
    background: var(--faint);
  }

  .has-fx .topbar {
    background: rgba(11, 11, 12, 0.72);
    backdrop-filter: blur(12px);
    border-bottom-color: var(--line-soft);
  }

  .scroll-cue svg,
  .badge,
  .btn,
  .ex-chip,
  /* The hero's entrance. Cancelled by name rather than left to the
     transform/opacity resets above, because a filled animation outranks
     any declaration those rules could make. */
  .has-fx .hero .eyebrow,
  .has-fx .hero h1,
  .has-fx .hero-sub,
  .has-fx .hero-noah,
  .rail-dot::after,
  .rail-tip,
  .site-footer a,
  .topbar,
  .has-fx .topbar {
    animation: none;
    transition: none;
  }

  .btn:hover,
  .badge:hover {
    transform: none;
  }
}

/* The belt above puts Noah back at the end of a horizontal track. On a
   phone the track is vertical, and `left` is not the axis he rides. */
@media (prefers-reduced-motion: reduce) and (max-width: 600px) {
  .has-fx .track-noah {
    left: 0;
    top: 83.333%;
  }
}
