/* The scroll-driven order card that opens the landing page.
 *
 * Four beats share one card. It turns 180 degrees per beat and swaps the
 * content of whichever face is pointing away, so two faces carry four states.
 * Nothing here animates on a timer: story.js writes --story-rot, --story-x,
 * --story-y and --story-scale from the scroll offset on every frame, so the
 * card is wherever the scroll says it is. Stop scrolling and it stops, mid-turn
 * if that is where you stopped.
 *
 * Every class is prefixed so nothing collides with landing.css, which is 47KB
 * and already owns .card, .face and friends. The buttons and .eyebrow are
 * deliberately NOT prefixed: those are the site's real components and the story
 * should use them rather than grow its own lookalikes.
 */

.story {
  position: relative;
  /* Pulled up under the sticky header. The rail is sticky at top:0, but it only
     reaches the top of the viewport once the page has scrolled past the header
     above it. Until then everything pinned inside it sits a header's height too
     low, which pushed the bottom of the hero below the fold on a phone. The
     header then simply overlays the top of the story, which is what
     --story-nudge already compensates for. */
  margin-top: calc(-1 * var(--story-header-h, 0px));
  /* Capped in px as well as vw: past roughly 1500px the card would otherwise
     keep drifting outward and reopen the dead band in the middle. */
  --story-travel: min(23vw, 300px);
  --story-lift: 15vh;
  --story-end-scale: 1.02;
  /* Half the sticky header, so the composition optically centres in the space
     below it rather than in the raw viewport. Set by story.js. */
  --story-nudge: 0px;

  --story-card-w: clamp(300px, min(33vw, 60vh), 520px);
  /* A real bank card is 1.586. This sits just under it: still unmistakably
     card-shaped, but tall enough for the shipping face's three option rows and
     its footer, which is the densest thing any face has to hold. */
  --story-card-ratio: 1.45;

  --story-gutter: clamp(20px, 5vw, 72px);
  /* Kept narrower than the copy could physically be, so the gap between the
     text block and the card stays open at wide sizes. */
  --story-copy-w: min(42vw, 520px);
}

/* svh, not vh: on iOS the collapsing address bar changes vh mid-scroll and a
   vh-pinned rail visibly jumps. svh is the stable small viewport. */
.story-rail {
  position: sticky;
  top: 0;
  height: 100svh;
  z-index: 2;
  pointer-events: none;
}

/* The spacers only have height once story.js has taken over. With the script
   blocked the story collapses to a single screen showing the hero exactly as it
   did before, rather than three viewports of empty scrolling. */
.story-beats { position: relative; z-index: 1; }
.story.is-active .story-beats { margin-top: -100svh; }
.story.is-active .story-beat { min-height: 100svh; }

/* Same reason: the first block of copy is visible by default and the script
   takes over from there. */
.story-copy[data-copy="0"] { opacity: 1; visibility: visible; pointer-events: auto; }

/* Custom properties do not resolve to pixels in getComputedStyle, so the only
   reliable way to hand a responsive length to JS is to let layout resolve it on
   a real element first. These carry --story-travel and --story-lift out to the
   script, which keeps the media queries below as the single source of truth. */
.story-probe { position: absolute; top: 0; left: 0; height: 0; overflow: hidden; visibility: hidden; }
.story-probe i { display: block; height: 0; }
#story-probe-travel { width: var(--story-travel); }
#story-probe-lift { width: var(--story-lift); }

/* ---------- the ground ----------
   One accent light rides with the card, so whichever side the card is on is the
   side the page is lit from. A weaker counter-light balances the far side, and
   a haze builds across the whole story so the page gains warmth as it resolves.
   Same green as the rest of the site throughout; only the amount changes.

   These are elements moved with transform and opacity rather than gradients
   whose positions are animated. Repainting a viewport-sized gradient every
   scroll frame is how smooth scrolling turns into a slideshow on a cheap phone.
   They live inside the rail so they never touch the sections below the story. */
.story-light { position: absolute; inset: 0; overflow: hidden; z-index: 0; }
.story-glow {
  position: absolute;
  left: 50%;
  border-radius: 50%;
  will-change: transform, opacity;
}
.story-glow-main {
  top: 46%;
  width: 92vmax; height: 68vmax;
  margin-left: -46vmax; margin-top: -34vmax;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 30%, transparent),
    color-mix(in srgb, var(--accent) 8%, transparent) 54%,
    transparent 78%);
  transform: translate3d(var(--story-gx, 0px), 0, 0);
  opacity: var(--story-ga, 0.5);
}
.story-glow-counter {
  top: 70%;
  width: 72vmax; height: 56vmax;
  margin-left: -36vmax; margin-top: -28vmax;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 15%, transparent), transparent 72%);
  transform: translate3d(var(--story-gx2, 0px), 0, 0);
  opacity: var(--story-ga2, 0.26);
}
.story-haze {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(180deg, rgba(12, 35, 26, 0.85) 0%, transparent 58%),
    linear-gradient(0deg, rgba(9, 31, 22, 0.65) 0%, transparent 42%);
  opacity: var(--story-haze, 0);
  will-change: opacity;
}

/* ---------- copy ----------
   No opacity transition: story.js writes it every frame from the scroll
   position, and a transition would fight the scrubbing and carry on animating
   after the scroll had stopped. */
.story-copy {
  position: absolute;
  width: var(--story-copy-w);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  z-index: 3;
}
.story-copy.is-live { pointer-events: auto; }

/* Mirrored against the card rather than pinned to the viewport edge. The card
   sits at centre plus travel, so the copy is centred on centre minus travel and
   the two end up with matching outer margins. max() keeps it off the edge on
   narrower windows, where the mirrored position would fall outside the gutter. */
.story-copy[data-align="left"] {
  left: max(var(--story-gutter), calc(50% - var(--story-travel) - var(--story-copy-w) / 2));
  top: calc(50% + var(--story-nudge));
  transform: translateY(-50%);
}
.story-copy[data-align="right"] {
  right: max(var(--story-gutter), calc(50% - var(--story-travel) - var(--story-copy-w) / 2));
  top: calc(50% + var(--story-nudge));
  transform: translateY(-50%);
}
/* The card centres on the closing beat, so the copy shares the column with it
   rather than sitting beside it, and has to stay compact. */
/* Hung off the bottom of the card rather than off the bottom of the viewport.
   Anchoring it to the viewport meant that every time the reserved space at the
   bottom grew, the copy climbed into the card. This way the pair moves as one
   and the gap between them is fixed, whatever else is on screen. */
.story-copy[data-align="end"] {
  left: 50%;
  transform: translateX(-50%);
  top: calc(
    50% + var(--story-nudge) - var(--story-lift)
    + (var(--story-card-w) / var(--story-card-ratio) / 2 * var(--story-end-scale))
    + clamp(18px, 3vh, 38px)
  );
  bottom: auto;
  width: min(90vw, 560px);
  text-align: center;
}

.story-copy h1,
.story-copy h2 {
  font-family: 'Sora', 'Segoe UI', sans-serif;
  font-weight: 800;
  letter-spacing: -0.022em;
  line-height: 1.06;
  text-wrap: balance;
  margin: 0 0 16px;
}
/* The hero heading, set in the browser against the live page and folded back
   in here. Three changes from the h1/h2 rule above: a lower ceiling, a looser
   line, and centred.
   The ceiling is a ceiling rather than the flat 70px the editor wrote, because
   a fixed size would have overridden the clamp and taken the responsive rules
   further down this file with it: at 380px wide the heading would still have
   been asking for 70px. 5.6vw reaches 70px at 1250px of viewport, so every
   window wide enough to have shown 70px still shows it. */
/* The four labels sit centred over their block of copy.
 *
 * The live design editor had been doing this with a per-beat pixel translate,
 * 121px, 201px and 194px, each eyeballed at one window width and none of them
 * on the fourth beat, which is centred already and so needed none. At 360px
 * those pushed the first pill 26px off the right edge of the screen: a
 * translate is a paint offset and takes no notice of the box it is leaving.
 *
 * Auto margins land within 17px of every one of those hand values at 1280 and
 * keep doing it at any width. Measured against each block's own centre: beat
 * one was 17px left of it, beat two 5px, beat three 4px. */
.story-copy .eyebrow {
  display: block;
  width: fit-content;
  margin-inline: auto;
}
/* The hero's label runs one step larger, which is where the editor left it. */
.story-copy[data-copy='0'] .eyebrow { font-size: 14px; }

/* letter-spacing here rather than on the shared h1/h2 rule above, which stays
   at -0.022em. The live design editor had been holding this headline at a flat
   -1.2px, which is -0.017em at the 70px it was set against. Folded in as em so
   it tracks the clamp instead of staying at 70px's value on a phone. */
.story-copy h1 {
  font-size: clamp(40px, 5.6vw, 70px);
  line-height: 1.2;
  text-align: center;
  letter-spacing: -0.017em;
}
.story-copy h2 { font-size: clamp(34px, 4.6vw, 58px); }
.story-copy p {
  margin: 0;
  max-width: 46ch;
  font-size: clamp(17px, 1.4vw, 20px);
  line-height: 1.6;
  color: var(--muted);
}
.story-copy p + p { margin-top: 15px; }
.story-copy[data-align="end"] h2 { font-size: clamp(28px, 3.4vw, 44px); }
.story-copy[data-align="end"] p { margin-inline: auto; }

/* ---------------------------------------------------------------------------
   Per-beat typography, folded out of public/overrides.css on 2026-08-08 when
   the design editor was removed.

   These were generated selectors (`div.story-copy:nth-of-type(3) > p:nth-of-type(1)`)
   carrying !important. Re-stated here against data-copy, which is what the
   markup actually keys on, and without !important: none is needed, because
   every one of these sets a property directly on the element that was
   otherwise only inheriting it.

   That last point is load-bearing on a phone. The breakpoint further down this
   file sets `.story-copy { text-align: left }`, and these beats are centred
   against that by being told directly. Beat three is deliberately absent from
   the whole block: it is `data-align="end"`, already centred, and so goes left
   on a phone with its container. That difference is inherited from the editor
   rather than designed, and it is the reason this is written per beat instead
   of as one rule over all four.

   Kept exactly as the editor left them, values included, so that removing the
   file changed nothing on screen. Three of them look like drift rather than
   intent, and are worth collapsing next time this copy is edited:
     - the two headings differ by 0.04 (1.2 against 1.24)
     - #ededed is two units off --ink (#eff1ed), invisible, and probably meant
       to be it
     - #d1d5cd is a genuinely darker grey than --muted (#e0e1dd) and is the one
       value here that is doing something a token does not already do
   -------------------------------------------------------------------------- */

/* Every selector here uses the child combinator, which the generated ones did
   too. It is not tidiness: beat one contains the try-form, and a descendant
   selector reaches past the beat's own copy into that form's note and its error
   line. The error line is red on purpose, and `> p` is what leaves it alone. */

/* Beat one: the hero. Lighter and looser than the shared paragraph rule, which
   suits a single line of lead-in over a block of body copy. */
.story-copy[data-copy='0'] > p {
  text-align: center;
  line-height: 1.4;
  font-weight: 300;
  color: #ededed;
}

/* Beat two. */
.story-copy[data-copy='1'] > h2 {
  text-align: center;
  line-height: 1.2;
}
.story-copy[data-copy='1'] > p {
  text-align: center;
  line-height: 1.5;
}

/* Beat three. Its two paragraphs are coloured apart: the first carries the
   claim, the second qualifies it, and the second is a step quieter. */
.story-copy[data-copy='2'] > h2 {
  text-align: center;
  line-height: 1.24;
}
.story-copy[data-copy='2'] > p {
  text-align: center;
  line-height: 1.5;
}
.story-copy[data-copy='2'] > p:first-of-type { color: #ededed; }
.story-copy[data-copy='2'] > p:nth-of-type(2) { color: #cfd6c8; }

/* A two-pixel lift on the first beat only, also from the editor. Too small to
   have been reasoned about and too small to be worth the risk of removing
   blind; unlike the horizontal nudges it cannot push anything off screen.
   Delete it the next time this section is touched and see if anyone notices. */
.story-beat:first-child { translate: 0 -2px; }

/* The footnote marker pointing at /terms. landing.css sets `a { color: inherit }`
   globally, so the accent has to be stated here. */
.story-ref {
  color: var(--accent);
  font-weight: 700;
  text-decoration: none;
  padding: 0 1px;
}
.story-ref:hover { opacity: 0.75; }
.story-ref:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 3px;
}

.story-actions { display: flex; gap: 13px; flex-wrap: wrap; margin-top: 30px; }
.story-copy[data-align="end"] .story-actions { justify-content: center; }
.story-note { margin-top: 16px; font-size: 14px; color: var(--muted-light); }

/* ---------- the card ---------- */

.story-shell {
  --story-x: 0px;
  --story-y: 0px;
  --story-cy: -50%;
  --story-rot: 0deg;
  --story-scale: 1;
  position: absolute;
  left: 50%;
  top: calc(50% + var(--story-nudge));
  width: var(--story-card-w);
  aspect-ratio: var(--story-card-ratio);
  perspective: 1800px;
  transform: translate(-50%, var(--story-cy)) translate3d(var(--story-x), var(--story-y), 0) scale(var(--story-scale));
  will-change: transform;
  z-index: 2;
}

.story-card {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transform: rotateY(var(--story-rot));
  will-change: transform;
}

/* Opaque, not the site's translucent --card-bg: a see-through face would show
   the other face through it during the turn. */
.story-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  /* Was a cool blue-grey ramp, then an olive one cut to match the old
     five-colour brand palette. The page under it is a near-black green now
     (#111d13) with a sage accent, and olive on that read as a brown card
     borrowed from the palette before it. Same three-stop shape and the same
     lightness steps, moved onto the page's own hue, roughly 137 degrees.
     Lifted about two points as it moved: the page went darker than the one the
     olive ramp was cut against, so matching its old lightness would have let
     the card's bottom corner sink into the floor. It clears it by 1.09:1 now,
     which is enough to read as a lit object without the ramp itself becoming
     the edge. That is still the border and the shadow's job.
     Text is unaffected: bone on the top stop is 10.32:1 and the accent 5.33:1,
     both measured where the ramp is lightest and contrast is tightest.

     Taken down a step since. The two upper stops carry nearly all of it and the
     bottom one barely moves, on purpose: the whole ramp shifted by the same
     amount would have walked the card's bottom corner into the floor, which is
     the one thing this gradient is not allowed to do. Card against page goes
     from 1.097:1 to 1.071:1 at the bottom, still a visible step, and every text
     pairing on it improves, since the only direction the background moved is
     darker. */
  background: linear-gradient(158deg, #173a27 0%, #102f1d 52%, #0e2515 100%);
  border: 1px solid var(--border);
  border-radius: calc(var(--story-card-w) * 0.047);
  padding: calc(var(--story-card-w) * 0.050) calc(var(--story-card-w) * 0.055);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-color: color-mix(in srgb, var(--accent) calc(42% * var(--story-glow, 0)), var(--border));
  box-shadow:
    0 0 0 1px color-mix(in srgb, var(--accent) calc(24% * var(--story-glow, 0)), transparent),
    0 22px 70px color-mix(in srgb, var(--accent) calc(30% * var(--story-glow, 0)), transparent),
    0 24px 48px rgba(0, 0, 0, 0.45);
}
.story-face-back { transform: rotateY(180deg); }

/* The accent edge the live hero card already carries. */
.story-face::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 3px;
  background: var(--accent);
}

/* The border light: two comets orbiting the card's edge.
 *
 * One conic gradient, turned by --beam-a, cut down to a two pixel band by two
 * masks composited with `exclude`. An outer mask covers the whole box, an inner
 * one covers everything inside the padding, and what survives is the padding
 * itself, which is the band. It is the only way to put a gradient on a border
 * that a border-radius does not ruin. Both the prefixed and unprefixed forms
 * are needed; Safari still wants the old syntax, where the operator is `xor`.
 *
 * A pseudo-element rather than a child, and this is load bearing: public/story.js
 * swaps beats by writing face.innerHTML outright, so any element inside a face
 * is deleted on the first beat change. A pseudo-element is not in the DOM and
 * survives every swap. It is also why the ring is drawn per face rather than
 * once around the card: the faces are what the flip turns, and a ring outside
 * them would hang in the air while the card rotated away from it.
 *
 * inset: 0, not the usual -1px. The face clips its overflow, and overflow clips
 * at the padding box, so a band hung outside that edge is simply cut off.
 *
 * public/beam.js writes the angle. With no script, or with reduced motion, it
 * stays where it started and this is a card with one lit edge. */
.story-face::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* The face's own radius less its 1px border, which is the radius of the box
     this is actually being clipped to. */
  border-radius: calc(var(--story-card-w) * 0.047 - 1px);
  padding: 2px;
  /* Sage into bone for the leading comet, the accent for the tail and the
     brand's light for the head, so its bright point is the same white the card's
     type is set in. The trailing comet is a deeper green, told apart by hue
     rather than by brightness.
   *
     The tails start from a 4% mix of their own colour rather than from
     `transparent`: `transparent` is rgba(0,0,0,0), so a ramp into it ramps
     through black, and on a green card that reads as a grey smudge chasing the
     light. */
  background: conic-gradient(
    from var(--beam-a, 0deg),
    transparent 0deg,
    color-mix(in srgb, #7dbf8a 4%, transparent) 18deg,
    color-mix(in srgb, #7dbf8a 55%, transparent) 46deg,
    #eef2eb 56deg,
    #ffffff 60deg,
    transparent 63deg,
    transparent 198deg,
    color-mix(in srgb, #36bb83 4%, transparent) 216deg,
    color-mix(in srgb, #36bb83 50%, transparent) 244deg,
    #36bb83 254deg,
    color-mix(in srgb, #36bb83 74%, white) 258deg,
    transparent 261deg,
    transparent 360deg
  );
  -webkit-mask:
    linear-gradient(#ffffff 0 0) content-box,
    linear-gradient(#ffffff 0 0);
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#ffffff 0 0) content-box,
    linear-gradient(#ffffff 0 0);
  mask-composite: exclude;
}

/* Windows high contrast mode replaces the palette outright, so a gradient
   border there is either invisible or the wrong colour. The card's own border
   says the same thing. */
@media (forced-colors: active) {
  .story-face::after { display: none; }
}

/* The card's thickness. Both faces are edge-on and invisible at exactly 90deg,
   and the copy is faded out for the turn, so stopping precisely there would
   otherwise leave an empty screen. A timed flip crossed that angle in
   milliseconds; a scrolled one can rest on it. This plane is turned the
   opposite way, so it is invisible while a face shows and squares up exactly
   when the faces vanish. No backface-visibility: it renders from both sides. */
.story-edge {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 50%;
  width: calc(var(--story-card-w) * 0.018);
  margin-left: calc(var(--story-card-w) * -0.009);
  transform: rotateY(-90deg);
  background: linear-gradient(180deg, #235435 0%, #174329 45%, #0f321c 100%);
  border-radius: calc(var(--story-card-w) * 0.006);
  /* Only visible in the last stretch of the turn, where the faces have gone
     too narrow to read. This plane sits on the card's centre line, so at a
     partial angle it would otherwise draw a grey seam straight down the middle
     of a face that is still perfectly readable. story.js drives it. */
  opacity: var(--story-edge, 0);
}

/* ---------- card face internals ----------
   Every size is a fraction of the card width, so the card scales as one object
   instead of becoming a big box of small text. */

.story-c-head { display: flex; align-items: center; gap: calc(var(--story-card-w) * 0.024); }
.story-c-name {
  font-family: 'Sora', 'Segoe UI', sans-serif;
  font-weight: 700;
  font-size: calc(var(--story-card-w) * 0.045);
  letter-spacing: -0.01em;
}
.story-c-stamp {
  font-size: calc(var(--story-card-w) * 0.027);
  font-weight: 800;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  padding: calc(var(--story-card-w) * 0.009) calc(var(--story-card-w) * 0.022);
  border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 17%, transparent);
  color: var(--accent);
  border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent);
}
.story-c-meta {
  font-size: calc(var(--story-card-w) * 0.028);
  letter-spacing: 0.075em;
  text-transform: uppercase;
  color: var(--muted-light);
  margin-top: calc(var(--story-card-w) * 0.014);
}
.story-c-rule {
  height: 1px;
  background: var(--border);
  margin: calc(var(--story-card-w) * 0.022) 0;
}

/* Three products, each on its own cycle. The thumb is smaller than it was as a
   single item, because three of them plus the header and footer is the tallest
   thing any face has to hold. */
.story-c-items {
  display: flex;
  flex-direction: column;
  gap: calc(var(--story-card-w) * 0.013);
}
.story-c-item { display: flex; align-items: center; gap: calc(var(--story-card-w) * 0.030); }
.story-c-thumb {
  width: calc(var(--story-card-w) * 0.085);
  height: calc(var(--story-card-w) * 0.085);
  border-radius: calc(var(--story-card-w) * 0.021);
  flex: none;
  display: grid;
  place-items: center;
  color: var(--muted-light);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--border);
}
.story-c-thumb svg { width: 55%; height: 55%; }
.story-c-item-name {
  display: block;
  font-size: calc(var(--story-card-w) * 0.036);
  font-weight: 600;
}
.story-c-item-sub {
  display: block;
  font-size: calc(var(--story-card-w) * 0.031);
  color: var(--muted-light);
  margin-top: calc(var(--story-card-w) * 0.005);
}
.story-c-price {
  margin-left: auto;
  font-size: calc(var(--story-card-w) * 0.038);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The body of each face takes the slack between header and footer, so a taller
   card reads as breathing room rather than a hole above the progress bar. */
.story-c-items,
.story-c-cal,
.story-c-ship,
.story-c-lines { margin-block: auto; }

.story-c-foot { margin-top: auto; padding-top: calc(var(--story-card-w) * 0.026); }
.story-c-foot-row {
  display: flex;
  align-items: baseline;
  gap: calc(var(--story-card-w) * 0.026);
  font-size: calc(var(--story-card-w) * 0.031);
  color: var(--muted);
  margin-bottom: calc(var(--story-card-w) * 0.022);
}
/* Faces whose footer is a bare line with no bar under it, where the bar's
   bottom margin would push the text into the card edge. */
.story-c-foot-row:last-child { margin-bottom: 0; }
.story-c-count { margin-left: auto; color: var(--muted-light); font-variant-numeric: tabular-nums; }
.story-c-bar {
  height: calc(var(--story-card-w) * 0.014);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.08);
  overflow: hidden;
}
.story-c-bar span { display: block; height: 100%; border-radius: 999px; background: var(--accent); }

/* calendar face. A fortnight rather than a whole month: two rows carry both
   marked days, and the cells stay big enough to read at phone size. Replaced a
   timeline whose marker line sat directly above the footer's progress bar, so
   the card had two near-identical green rules doing different jobs. */
.story-c-cal {
  display: flex;
  flex-direction: column;
  gap: calc(var(--story-card-w) * 0.012);
}
.story-c-cal-head,
.story-c-cal-grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: calc(var(--story-card-w) * 0.010);
}
.story-c-cal-head span {
  text-align: center;
  font-size: calc(var(--story-card-w) * 0.024);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted-light);
}
.story-c-cal-grid span {
  display: grid;
  place-items: center;
  aspect-ratio: 1.3;
  border-radius: calc(var(--story-card-w) * 0.016);
  font-size: calc(var(--story-card-w) * 0.030);
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  background: rgba(255, 255, 255, 0.03);
}
/* The day we place the order: outlined, because nothing arrives on it. */
.story-c-cal-grid .is-order {
  color: var(--ink);
  background: transparent;
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 50%, transparent);
}
/* The day the customer picked, and the only filled cell on the card. */
.story-c-cal-grid .is-arrive {
  color: #0a1d12;
  background: var(--accent);
  font-weight: 700;
}

/* shipping face. Mirrors the real chooser in app.js: label, ETA under it, price
   in its own right-hand column so the options compare straight down the card. */
.story-c-ship {
  display: flex;
  flex-direction: column;
  gap: calc(var(--story-card-w) * 0.013);
  margin-top: calc(var(--story-card-w) * 0.005);
}
.story-c-ship-row {
  display: flex;
  align-items: center;
  gap: calc(var(--story-card-w) * 0.024);
  padding: calc(var(--story-card-w) * 0.015) calc(var(--story-card-w) * 0.024);
  border-radius: calc(var(--story-card-w) * 0.024);
  border: 1px solid var(--border);
  background: rgba(255, 255, 255, 0.015);
}
.story-c-ship-row.is-on {
  border-color: color-mix(in srgb, var(--accent) 46%, transparent);
  background: color-mix(in srgb, var(--accent) 9%, transparent);
}
.story-c-radio {
  width: calc(var(--story-card-w) * 0.034);
  height: calc(var(--story-card-w) * 0.034);
  border-radius: 50%;
  border: 1.5px solid var(--muted-light);
  flex: none;
}
.story-c-ship-row.is-on .story-c-radio {
  border-color: var(--accent);
  box-shadow:
    inset 0 0 0 calc(var(--story-card-w) * 0.007) #0a1d12,
    inset 0 0 0 calc(var(--story-card-w) * 0.024) var(--accent);
}
.story-c-ship-name {
  display: block;
  font-size: calc(var(--story-card-w) * 0.033);
  font-weight: 600;
}
.story-c-ship-eta {
  display: block;
  font-size: calc(var(--story-card-w) * 0.028);
  color: var(--muted-light);
  margin-top: calc(var(--story-card-w) * 0.003);
}
.story-c-ship-price {
  margin-left: auto;
  font-size: calc(var(--story-card-w) * 0.032);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.story-c-ship-price.is-free { color: var(--accent); }

/* done face. The tick sits in the header beside the title rather than alone
   above it, which is what left the card looking empty. */
.story-c-check {
  width: calc(var(--story-card-w) * 0.075);
  height: calc(var(--story-card-w) * 0.075);
  border-radius: 50%;
  flex: none;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: #0a1d12;
}
.story-c-check svg { width: 55%; height: 55%; }

/* The order restated as plain lines: no icons, because this card confirms what
   was bought rather than presenting it again. */
.story-c-lines {
  display: flex;
  flex-direction: column;
  gap: calc(var(--story-card-w) * 0.018);
}
.story-c-line {
  display: flex;
  align-items: baseline;
  gap: calc(var(--story-card-w) * 0.026);
  font-size: calc(var(--story-card-w) * 0.033);
}
.story-c-line b { font-weight: 600; color: var(--ink); }
.story-c-line span {
  margin-left: auto;
  color: var(--muted-light);
  font-size: calc(var(--story-card-w) * 0.029);
}

/* ---------- beat indicator ---------- */

.story-dots {
  position: fixed;
  left: 22px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 11px;
  z-index: 40;
  opacity: var(--story-dots, 0);
  transition: opacity 300ms ease;
  pointer-events: none;
}
.story-dots i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.18);
  transition: background 300ms ease, transform 300ms ease;
}
.story-dots i.is-on { background: var(--accent); transform: scale(1.5); }

/* ---------- room for the pinned overlays ----------
   landing.js pins the cookie notice and the survey tab to the bottom of the
   viewport and reserves the space they take in --bottom-overlay-h. The closing
   beat puts its call to action in exactly that spot, so it lifts clear while
   that reservation is in force. story.js re-measures when the class changes. */
body.has-bottom-overlay .story-copy[data-align="end"] h2 { font-size: clamp(26px, 3vw, 36px); }
body.has-bottom-overlay .story-copy[data-align="end"] p { font-size: 16px; }
/* Desktop only: on a phone the card is anchored near the top rather than
   centred, so lifting it would push it off the screen. The card also gives up
   a little size, because with both the notice and the survey tab pinned there
   is not enough height left for a full-size card and a readable close. */
@media (min-width: 901px) {
  body.has-bottom-overlay .story { --story-lift: 23vh; --story-end-scale: 0.88; }
}

/* ---------- short screens ----------
   The closing beat is the tight one: the card and the copy share one column
   there. Tighten the copy rather than the card. */
@media (max-height: 760px) {
  .story { --story-lift: 17vh; }
  .story-copy[data-align="end"] h2 { font-size: clamp(26px, 3vw, 34px); }
  .story-copy[data-align="end"] p { font-size: 16px; }
}

/* ---------- mobile: a different story, not a squeezed one ---------- */
@media (max-width: 900px) {
  .story {
    /* Taller than the desktop proportion. The shipping face carries three
       option rows plus a footer, and at phone width the card ratio leaves no
       room for the footer, which flex then silently squeezes to nothing.
       Vertical space is free here since the card stands alone. */
    --story-card-w: min(86vw, 380px);
    --story-card-ratio: 1.3;
    /* No lateral travel: at this width there is no left or right to move to.
       The card pins near the top and only turns. */
    --story-travel: 0px;
    --story-lift: 0vh;
    --story-end-scale: 1.03;
  }

  /* Clear of the sticky header, which now overlays the top of the section. */
  .story-shell { top: calc(var(--story-header-h, 0px) + 2svh); --story-cy: 0%; }

  /* Every block hangs off the bottom of the card, not off the bottom of the
     viewport. Anchoring to the viewport meant the tallest beat, the hero with
     its two-line headline and two buttons, ran past the fold. */
  .story-copy,
  .story-copy[data-align="left"],
  .story-copy[data-align="right"],
  .story-copy[data-align="end"] {
    left: var(--story-gutter);
    right: var(--story-gutter);
    width: auto;
    bottom: auto;
    top: calc(
      var(--story-header-h, 0px) + 2svh
      + (var(--story-card-w) / var(--story-card-ratio))
      + var(--story-copy-gap, 22px)
    );
    transform: none;
    text-align: left;
  }
  .story-copy[data-align="end"] .story-actions { justify-content: flex-start; }

  .story-copy h1 { font-size: clamp(34px, 8.6vw, 48px); }
  .story-copy h2,
  .story-copy[data-align="end"] h2 { font-size: clamp(30px, 7.6vw, 42px); }
  .story-copy p,
  .story-copy[data-align="end"] p { font-size: 16.5px; }
  .story-dots { left: 12px; }

  /* The pinned overlays reach the hero here, not just the end of the page.
     Every block of copy hangs off the bottom of the card on a phone, so the
     taller the card, the lower the hero's buttons sit; with the cookie notice
     up, a 360-wide screen put the call to action 61px underneath it, where it
     cannot be tapped at all. The card gives up the height rather than the
     copy, because the copy is the part being read.

     This buys back the first visit, which is the one that matters. Scrolling
     down and back up stacks the survey tab on top of the notice as well, and
     226px of reserved screen on a 740px-tall phone is more than the hero can
     be tightened out of. See B9 in docs/browser-tests.md for the measurements
     at 360x740, 390x844 and 412x915. */
  body.has-bottom-overlay .story {
    --story-card-w: min(66vw, 300px);
    --story-copy-gap: 14px;
  }
  body.has-bottom-overlay .story-copy[data-copy="0"] h1 { font-size: clamp(30px, 7.4vw, 40px); }
  body.has-bottom-overlay .story-copy[data-copy="0"] p { font-size: 15.5px; }
}

/* ---------- reduced motion ----------
   Nothing eases: the card changes face and the copy changes text the instant a
   beat becomes current. Every word survives, only the movement goes. */
@media (prefers-reduced-motion: reduce) {
  .story-dots i { transition: none; }
}
