/*
 * The public landing page (WP007).
 *
 * A vendored file, not a build artifact (ADR-0004), and a file rather than an
 * inline style element so that `style-src 'self'` holds with no `unsafe-inline`
 * (ADR-0022).
 *
 * ═══════════════════════════════════════════════════════════════════════════
 *  EVERYTHING HERE IS PURE CSS. THERE IS NO JAVASCRIPT ON THIS PAGE (ADR-0026).
 *
 *  Two independent animations run on this page:
 *
 *    1. THE PHOTO SLIDESHOW, looping forever. The server shuffles the folder
 *       and renders up to five slides in random order; the keyframes below
 *       cross-fade them on a four and a half second step.
 *
 *    2. THE A/B/C/D REVEAL, which runs once. Every step is an `animation-delay`
 *       with `animation-fill-mode: both`, so the sequence is deterministic and
 *       needs nothing to orchestrate it.
 *
 *  The reveal timeline, in seconds from page load:
 *
 *      0.20  the question fades in, alone
 *      1.70  A) Love
 *      2.90  B) Adventure
 *      4.10  C) Family
 *      5.30  D) All the Above
 *      5.80  ALL FOUR HOLD, for a full five seconds, long enough to read the
 *            whole quiz and arrive at the answer yourself
 *     10.80  D is chosen: the option hands off to the heading at the same size
 *            in the same place, which then rises and grows into the answer
 *     11.30  A, B and C set off DOWNWARD, staggered, travelling 4.5rem as they
 *            go, toward where their words are about to land
 *     12.00  Love arrives from above and pulls its sentence in beside it
 *     12.42  Adventure
 *     12.84  Family
 *     13.67  finished. The question stays. The photographs keep changing.
 *
 *  Sign-in is NOT part of this. It sits in the navbar, on screen from the first
 *  frame (Fix 2).
 * ═══════════════════════════════════════════════════════════════════════════
 */

/*
 * ═══════════════════════════════════════════════════════════════════════════
 *  THE TOKENS MOVED TO public/assets/css/tokens.css, WHICH THIS PAGE LOADS
 *  FIRST. Same values, byte for byte; neutral names, because the trip
 *  dashboard now sits on the same panel and "--panel" was the wrong
 *  name for something two halves of the product share.
 *
 *  THE CONTRAST GUARANTEE IS UNCHANGED AND STILL LIVES WITH THE VALUES.
 *  Every block of text on this page sits on --panel at every phase of the
 *  animation: 0.72 alpha over an unknown photograph, worst case a pure white
 *  one, flattens to rgb(90, 86, 83) — --ink 6.76:1, --ink-muted 5.47:1, both
 *  past AA's 4.5:1. tests/landing_test.php recomputes them from tokens.css.
 * ═══════════════════════════════════════════════════════════════════════════
 */

/*
 * The narrative face. Self hosted under public/assets/fonts, never a CDN: the
 * policy is `font-src 'self'` with no external origins (ADR-0022), and a
 * landing page should not depend on a third party being reachable from a
 * campground (C1). Licence and provenance are in that directory's README.
 *
 * `swap` so the sentences are readable in the fallback serif from the first
 * paint and simply change face when the 23KB arrives.
 */
@font-face {
  font-family: "EB Garamond";
  src: url("/assets/fonts/eb-garamond-latin-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/*
 * The italic, for the quotes. An old-style Garamond italic is genuinely
 * calligraphic: it descends from chancery hands rather than from a sloped
 * roman, so it reads as written by a person while staying legible at body size.
 * A true script face was considered for this and rejected. Three full sentences
 * of one is decoration paid for in legibility, and the instruction that asked
 * for character also asked not to sacrifice reading.
 */
@font-face {
  font-family: "EB Garamond";
  src: url("/assets/fonts/eb-garamond-latin-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

* { box-sizing: border-box; }

html, body { height: 100%; }

body.landing {
  margin: 0;
  min-height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Fix 3 folded the question back into the stage, so nothing floats above the
     card any more and this only has to clear the fixed navbar. The geometry
     test still recomputes the requirement from the declared values. */
  padding: 7.5rem 1.25rem 3rem;
  background-color: var(--ground);
  background-image: radial-gradient(120% 90% at 50% 15%, var(--ground-glow), var(--ground) 70%);
  color: var(--ink);
  font: 400 16px/1.6 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

/* ─────────────────────────────────────────────────────────────── navbar */

/*
 * Static, fixed to the top, and deliberately outside the reveal.
 *
 * Fix 2 moved sign-in here from phase 4 of the sequence. That is not only a
 * layout change: it removes the whole question of whether the link is reachable
 * before the animation finishes. It is on screen and in the focus order from
 * the first frame, so there is nothing to time and nothing to escape.
 *
 * It carries the same `--panel` backing as every other block of text on
 * this page, so its contrast is the same measured 6.74:1 against the worst
 * photograph the slideshow could put behind it. The slideshow keeps running
 * underneath; the navbar is a layer, not a lid.
 */
.navbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.7rem 1.25rem;
  background: var(--panel);
  border-bottom: 1px solid rgba(250, 246, 239, 0.10);
}

@supports (backdrop-filter: blur(6px)) {
  .navbar { backdrop-filter: blur(6px) saturate(105%); }
}

.wordmark {
  color: var(--ink);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.01em;
}

/*
 * Opaque, so no photograph can affect it at all: #17161a on #c9a87c is 8.04:1.
 * Smaller than the old in-sequence button was, because up here it is a
 * persistent action rather than the page's finale.
 */
.signin {
  display: inline-block;
  padding: 0.4rem 1rem;
  border: 1px solid transparent;
  border-radius: 4px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: 0.9375rem;
  font-weight: 500;
  text-decoration: none;
  transition: background-color 160ms ease;
}

.signin:hover,
.signin:focus-visible { background: var(--accent-hover); }

/* Focus has to be visible against a photograph, so it is drawn in the light ink
   rather than left to the browser default, which can vanish on a bright frame. */
.signin:focus-visible,
a:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 3px;
}

/* ─────────────────────────────────────────────────────── the photo layer */

.slides {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background-color: var(--ground);
}

.slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* Full bleed, centred, never distorted. This is also what lets the owner
     upload a 12 megapixel photograph straight off a phone without resizing it:
     the browser scales it to the viewport. */
  object-fit: cover;
  object-position: center;
  opacity: 0;
  will-change: opacity;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

/*
 * Slide 1 is the opaque floor beneath the whole cycle. It is on screen when the
 * page loads, it is what the last slide fades back to, and it is what
 * guarantees something always covers the viewport. It carries no
 * animation-name in any rule below, so this is the only thing making it
 * visible. Remove it and the page renders its background colour and nothing
 * else.
 */
.slide:first-child { opacity: 1; }

/* One photograph: nothing to cross-fade to, so nothing animates at all. */
.slides-1 .slide { animation: none; }

/* ─────────────────────────────────────────────── the cross-fade, per count */

/*
 * THE STACKING IS THE TRICK, AND IT IS NOT THE OBVIOUS ONE.
 *
 * The obvious cross-fade, outgoing slide fading out while the incoming fades
 * in, puts BOTH at 0.5 opacity halfway through. Two half transparent layers
 * cover only 75% of what is behind them, so the background shows through and
 * every transition dips. A simulation in tests/landing_test.php catches exactly
 * that, and it caught it here.
 *
 * So instead: slide 1 is a permanently opaque floor, and slides 2 to K fade in
 * ON TOP of it one after another, since they come later in the DOM and stack
 * above. Each snaps to transparent only once the next has fully covered it,
 * which is invisible. Only the last slide fades back OUT, and what it reveals
 * is slide 1, still opaque underneath.
 *
 * Result: coverage is 100% at every instant, and at most one slide is ever mid
 * fade, so a transition blends exactly two photographs and never a muddy stack
 * of five. Both properties are asserted.
 *
 *      step S = 9.0s      fade F = 1.8s      cycle T = K * S
 *
 * Slide n of K is transparent until (n-1)S, fades in by (n-1)S+F, holds, then
 * drops at nS+F. The last holds to T-F and fades out by T. Recompute every
 * percentage if S or F changes; CSS cannot derive them without a build step,
 * and this project does not have one.
 *
 * S AND F WERE BOTH DOUBLED BY WP008 Fix 5. At 4.5s a photograph was gone before it
 * had been looked at, and the 1.2s cross-fade read as a cut rather than as a
 * dissolve. Nine seconds on screen and 1.8s between is the same mechanism at
 * half speed; every percentage below was recomputed from the formula.
 */

/* 2 photos, cycle 18.0s. Slide 1 is the opaque floor; slides 2 to 2 fade
   in on top of it in turn, and only the last one fades back out. */
.slides-2 .slide { animation-duration: 18.0s; }
.slides-2 .slide:nth-child(2) { animation-name: landing-fade-2-2; }

/* 3 photos, cycle 27.0s. Slide 1 is the opaque floor; slides 2 to 3 fade
   in on top of it in turn, and only the last one fades back out. */
.slides-3 .slide { animation-duration: 27.0s; }
.slides-3 .slide:nth-child(2) { animation-name: landing-fade-3-2; }
.slides-3 .slide:nth-child(3) { animation-name: landing-fade-3-3; }

/* 4 photos, cycle 36.0s. Slide 1 is the opaque floor; slides 2 to 4 fade
   in on top of it in turn, and only the last one fades back out. */
.slides-4 .slide { animation-duration: 36.0s; }
.slides-4 .slide:nth-child(2) { animation-name: landing-fade-4-2; }
.slides-4 .slide:nth-child(3) { animation-name: landing-fade-4-3; }
.slides-4 .slide:nth-child(4) { animation-name: landing-fade-4-4; }

/* 5 photos, cycle 45.0s. Slide 1 is the opaque floor; slides 2 to 5 fade
   in on top of it in turn, and only the last one fades back out. */
.slides-5 .slide { animation-duration: 45.0s; }
.slides-5 .slide:nth-child(2) { animation-name: landing-fade-5-2; }
.slides-5 .slide:nth-child(3) { animation-name: landing-fade-5-3; }
.slides-5 .slide:nth-child(4) { animation-name: landing-fade-5-4; }
.slides-5 .slide:nth-child(5) { animation-name: landing-fade-5-5; }

@keyframes landing-fade-2-2 {
  0%       { opacity: 0; }
  50.000%  { opacity: 0; }
  60.000%  { opacity: 1; }
  90.000%  { opacity: 1; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-3-2 {
  0%       { opacity: 0; }
  33.333%  { opacity: 0; }
  40.000%  { opacity: 1; }
  73.333%  { opacity: 1; }
  73.334%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-3-3 {
  0%       { opacity: 0; }
  66.667%  { opacity: 0; }
  73.333%  { opacity: 1; }
  93.333%  { opacity: 1; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-4-2 {
  0%       { opacity: 0; }
  25.000%  { opacity: 0; }
  30.000%  { opacity: 1; }
  55.000%  { opacity: 1; }
  55.001%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-4-3 {
  0%       { opacity: 0; }
  50.000%  { opacity: 0; }
  55.000%  { opacity: 1; }
  80.000%  { opacity: 1; }
  80.001%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-4-4 {
  0%       { opacity: 0; }
  75.000%  { opacity: 0; }
  80.000%  { opacity: 1; }
  95.000%  { opacity: 1; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-5-2 {
  0%       { opacity: 0; }
  20.000%  { opacity: 0; }
  24.000%  { opacity: 1; }
  44.000%  { opacity: 1; }
  44.001%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-5-3 {
  0%       { opacity: 0; }
  40.000%  { opacity: 0; }
  44.000%  { opacity: 1; }
  64.000%  { opacity: 1; }
  64.001%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-5-4 {
  0%       { opacity: 0; }
  60.000%  { opacity: 0; }
  64.000%  { opacity: 1; }
  84.000%  { opacity: 1; }
  84.001%  { opacity: 0; }
  100%     { opacity: 0; }
}

@keyframes landing-fade-5-5 {
  0%       { opacity: 0; }
  80.000%  { opacity: 0; }
  84.000%  { opacity: 1; }
  96.000%  { opacity: 1; }
  100%     { opacity: 0; }
}

/* ───────────────────────────────────────────────────────────── the scrim */

/*
 * Mood, and defence in depth for legibility. Deliberately gentle, because the
 * panels below are what actually carry the contrast ratio and a heavy scrim
 * would fight the warmth.
 */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      rgba(20, 15, 11, 0.30) 0%,
      rgba(20, 15, 11, 0.12) 38%,
      rgba(20, 15, 11, 0.46) 100%);
}

/* ══════════════════════════════════════════════ the A/B/C/D reveal ══════ */

/*
 * THE STAGE CARRIES ONE PANEL, AND THAT IS A CONTRAST DECISION, NOT A STYLING
 * ONE.
 *
 * The options and the answer cross-fade against each other in the same grid
 * cell. If each carried its own translucent panel, the two would overlap during
 * the hand-off and the effective backing would briefly go LIGHTER, not darker:
 * two layers at half opacity of a 0.72 panel composite to about 0.59, which
 * drops primary text to roughly 4.3:1 and under AA for the length of the
 * transition. One panel on the stage stays at 0.72 for every frame of the
 * sequence, so the measured 6.74:1 holds at every phase.
 *
 * The price is that the card is sized for the final content from the start, so
 * during the exam phase there is empty panel below the option list, roughly
 * four rem of it. That is a visible cost, accepted deliberately: a card with
 * room beneath the question reads as an exam paper, and a contrast dip mid
 * animation does not read as anything, it just fails.
 */
.stage {
  position: relative;
  z-index: 2;
  width: 100%;
  max-width: 38rem;
  padding: 1.6rem 1.75rem 1.75rem;
  border: var(--panel-edge);
  border-radius: var(--panel-radius);
  background: var(--panel);
}

@supports (backdrop-filter: blur(3px)) {
  .stage { backdrop-filter: blur(3px) saturate(105%); }
}

/* ─────────────────────────────────────────────────── phase 0, the premise */

/*
 * Set like the header of an exam question, not a hero headline. It fades in
 * alone, and then it STAYS: the answer below is a sentence fragment without it,
 * so it is present in the final state too, animated or not.
 */
.prompt {
  margin: 0 0 1.15rem;
  color: var(--ink);
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: 0.01em;
  opacity: 0;
  animation: reveal-fade-in 0.6s ease-out 0.2s both;
}

/*
 * The options and the answer share one grid cell, stacked on top of each other.
 * That is what lets the list dissolve without leaving a hole in the layout, and
 * what lets the answer rise up through the space the list was occupying.
 */
.cell {
  display: grid;
}

.cell > * {
  grid-area: 1 / 1;
  align-self: start;
}

/* ────────────────────────────────────────────── phase 1, the exam options */

.option {
  margin: 0;
  padding: 0.08rem 0;
  color: var(--ink);
  font-size: 1rem;
  line-height: 1.35;
  opacity: 0;
  animation-name: reveal-fade-in;
  animation-duration: 0.5s;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

/*
 * Each option fades in at its own moment, the four hold together for a full five
 * seconds so the quiz can actually be READ, and then A, B and C set off DOWNWARD,
 * staggered, toward where their words will end up. They travel rather than simply
 * vanishing: that is what makes the next phase read as the same three answers
 * arriving somewhere new.
 *
 * THEY LEAVE DURING THE SELECTION, AND THEY ARE GONE BEFORE THE HEADING MOVES.
 * That order was arrived at by rendering it (WP008 Fix 5). Delaying their departure
 * until after the hand-off looked correct written down and was wrong on screen:
 * the heading climbed straight through a list that was still fully visible, and
 * "D) All the Above" at half size crossed over "C) Family". So A, B and C now
 * set off as D is being chosen, between 10.80s and 12.15s, and the rise that
 * starts at 11.70s travels through space they have already left.
 *
 * They start at different times but each travels for the same 0.95s, so their
 * durations differ and each needs its own percentages.
 */
.option-a { animation: reveal-option-a 10.05s cubic-bezier(0.4, 0, 0.2, 1) 1.70s both; }
.option-b { animation: reveal-option-b 9.05s cubic-bezier(0.4, 0, 0.2, 1) 2.90s both; }
.option-c { animation: reveal-option-c 8.05s cubic-bezier(0.4, 0, 0.2, 1) 4.10s both; }

/*
 * D is the one option that does not travel. It appears with the others, waits
 * with them, and then hands over to the heading below, which fades in over
 * exactly the same window at the same size in the same place.
 *
 * That window is 0.9s (WP008 Fix 5), not the 0.35s it was. A third of a second is
 * long enough to be seen as a cut and too short to be seen as a dissolve, which
 * is what made the whole moment feel abrupt. Both halves of the cross-fade were
 * lengthened together; they must stay identical or the swap becomes visible.
 */
.option-d { animation: reveal-option-d 6.40s ease-out 5.30s both; }

/* ──────────────────────────────────────────────── phase 2, the answer ─── */

/*
 * THE MIGRATION OF D, and the reason it reads as one element rather than two.
 *
 * The heading starts scaled down to about the size of an option line and
 * translated down to where option D sits, roughly 3.75rem below its own
 * position. It fades in there while option D fades out, at nearly the same size
 * in nearly the same place, so the hand-off is not visible as a swap. It then
 * rises and grows into its own position.
 *
 * `transform-origin: left center` keeps the left edge fixed while it scales, so
 * the text grows rightward from the same starting point rather than swelling
 * outward from its middle. The stage is left aligned throughout for exactly
 * this reason: it makes the movement purely vertical.
 *
 * THE EASING CHANGED WITH THE DURATION (WP008 Fix 5), and the easing mattered more.
 * cubic-bezier(0.22, 1, 0.36, 1) leaves the start line at enormous velocity: the
 * instant the cross-fade ended, the heading was already most of the way up. Read
 * back to back with a 0.35s fade, the option did not become the heading, it was
 * replaced by one. cubic-bezier(0.4, 0, 0.2, 1) accelerates out of rest and
 * decelerates into place, so the growth begins where the fade ends instead of
 * interrupting it, and it now has 2.4s to do it in rather than 1.3s.
 */
.answer {
  margin: 0 0 0.85rem;
  font-size: clamp(1.75rem, 6.5vw, 2.75rem);
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
  color: var(--ink);
  transform-origin: left center;
  animation: reveal-answer 3.30s cubic-bezier(0.4, 0, 0.2, 1) 10.80s both;
}

/* ─────────────────────────────────── phase 3, the narrative paragraph ─── */

/*
 * ONE PARAGRAPH. Not three. Not a list, not a grid, not three cards.
 *
 * `.beat` and `.quote` are inline, so the three sentences wrap and reflow as
 * continuous prose and the bold words sit inside the text rather than above it.
 * The two earlier attempts stacked three separate blocks, which reads as a
 * glossary. A test asserts this layout rather than the word order, because word
 * order passed twice while the page looked wrong.
 */
/*
 * FULLY JUSTIFIED, by owner request. Both edges flush is what makes a block of
 * serif prose read as a printed page rather than as web copy, which is the
 * register the whole reveal is written in.
 *
 * Hyphenation comes with it and is not optional. Justifying a 38rem measure
 * without it stretches the word spaces on any line holding a long word — the
 * rivers that give justified text on the web its bad name. `lang="en"` is
 * declared on <html>, which is what the browser needs to hyphenate correctly.
 */
.narrative {
  margin: 0;
  font-family: "EB Garamond", Iowan Old Style, Palatino, Georgia, serif;
  font-size: 1.125rem;
  line-height: 1.7;
  color: var(--ink);
  text-align: justify;
  text-justify: inter-word;
  -webkit-hyphens: auto;
  hyphens: auto;
  /* Only hyphenate words worth hyphenating. Without this the browser breaks
     "doing" as "do-ing", which is correct and looks like a mistake. */
  hyphenate-limit-chars: 8 4 3;
}

.beat {
  display: inline;
}

/* A breath between the three ideas, without breaking the prose flow. */
.beat + .beat {
  margin-left: 0.4rem;
}

/*
 * The one element that is not purely inline, and the reason is mechanical: a
 * non-replaced inline element cannot be transformed, and these words have to
 * move. A single word never breaks across lines anyway, so inline-block costs
 * nothing in how the paragraph wraps.
 *
 * They keep the page's sans face and its bold weight, so they stand clearly
 * apart from the serif sentences around them.
 */
.word {
  display: inline-block;
  font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.005em;
  color: var(--ink);
  opacity: 0;
  animation-name: reveal-word;
  animation-duration: 0.6s;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
  animation-fill-mode: both;
}

/*
 * The quote is set in the italic: elegant, hand-written in feel, and clearly a
 * different voice from the words it follows. The words themselves stay in the
 * page's plain bold sans, which is the contrast the owner asked for.
 */
.quote {
  display: inline;
  font-style: italic;
  opacity: 0;
  animation-name: reveal-fade-in;
  animation-duration: 0.55s;
  animation-timing-function: ease-out;
  animation-fill-mode: both;
}

/*
 * Each word lands, then its sentence is pulled in beside it.
 *
 * Pushed back 1.3s by WP008 Fix 5 to follow the slower hand-off. The first word now
 * arrives in the last third of the heading's rise, so the two movements overlap
 * and the eye is carried from one to the next rather than waiting for the
 * heading to finish. Sequence ends at about 15.0s.
 */
.beat-1 .word  { animation-delay: 13.30s; }
.beat-1 .quote { animation-delay: 13.58s; }
.beat-2 .word  { animation-delay: 13.72s; }
.beat-2 .quote { animation-delay: 14.00s; }
.beat-3 .word  { animation-delay: 14.14s; }
.beat-3 .quote { animation-delay: 14.42s; }

/* ──────────────────────────────────────────────────────────── keyframes */

@keyframes reveal-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes reveal-option-a {
  0%        { opacity: 0; transform: translateY(0); }
  4.975%   { opacity: 1; transform: translateY(0); }
  90.547%  { opacity: 1; transform: translateY(0); animation-timing-function: linear; }
  95.025%  { opacity: 0; transform: translateY(1.4rem); animation-timing-function: linear; }
  100%      { opacity: 0; transform: translateY(4.5rem); }
}

@keyframes reveal-option-b {
  0%        { opacity: 0; transform: translateY(0); }
  5.525%   { opacity: 1; transform: translateY(0); }
  89.503%  { opacity: 1; transform: translateY(0); animation-timing-function: linear; }
  94.475%  { opacity: 0; transform: translateY(1.4rem); animation-timing-function: linear; }
  100%      { opacity: 0; transform: translateY(4.5rem); }
}

@keyframes reveal-option-c {
  0%        { opacity: 0; transform: translateY(0); }
  6.211%   { opacity: 1; transform: translateY(0); }
  88.199%  { opacity: 1; transform: translateY(0); animation-timing-function: linear; }
  93.789%  { opacity: 0; transform: translateY(1.4rem); animation-timing-function: linear; }
  100%      { opacity: 0; transform: translateY(4.5rem); }
}

/*
 * D: fades in with the others, waits out the five second hold, and goes out over
 * the last 0.9s while the heading fades in behind it. 0.5/6.40 = 7.813%,
 * 5.50/6.40 = 85.938%.
 */
@keyframes reveal-option-d {
  0%        { opacity: 0; }
  7.813%   { opacity: 1; }
  85.938%  { opacity: 1; }
  100%      { opacity: 0; }
}

/*
 * The heading. 3.30s total: fade in over the first 0.9s while still small and
 * still down at option D's position, then rise and grow over the remaining
 * 2.4s. 0.9/3.30 = 27.273%.
 *
 * It stays perfectly still for the whole fade. It has to: while D is still
 * partly visible the two are the same words at the same size in the same place,
 * and any movement during that window shows there are two of them.
 */
@keyframes reveal-answer {
  0%        { opacity: 0; transform: translateY(3.75rem) scale(0.36); }
  27.273%   { opacity: 1; transform: translateY(3.75rem) scale(0.36); }
  100%      { opacity: 1; transform: translateY(0) scale(1); }
}

/*
 * Each word arrives from ABOVE, continuing the descent the option it came from
 * began. The option travels 4.5rem down as it fades; the word picks the motion
 * up 2.6rem above its resting place and settles. Same direction, overlapping in
 * time, so the eye reads one movement rather than two.
 *
 * THE OPTION IS INVISIBLE FOR MOST OF THAT 4.5rem, and deliberately (WP008 Fix 5).
 * C) Family sits about 1.5rem above D) All the Above, so an option falling
 * through its full distance at a readable opacity passes directly over the
 * heading while the heading is sitting at D's place waiting to rise — two sets
 * of words in the same spot, which is precisely the smear the hand-off was
 * being rebuilt to remove. Each option is therefore at zero by 1.4rem of
 * travel; it keeps moving after that so the exit still eases out of the
 * layout, but nothing is left to collide with.
 */
@keyframes reveal-word {
  from { opacity: 0; transform: translateY(-2.6rem); }
  to   { opacity: 1; transform: translateY(0); }
}

/* ══════════════════════════════════ reduced motion ═════════════════════ */

/*
 * ═══════════════════════════════════════════════════════════════════════════
 *  THIS BLOCK WAS THE BUG, AND IT IS WHY THREE FIXES "PASSED" WHILE THE OWNER
 *  SAW A STATIC PAGE. Diagnosed with headless-Chrome screenshots in Fix 4.
 *
 *  It used to skip the whole sequence and set `.options { display: none }`.
 *  On a machine with Reduce Motion switched on, which the owner's is, the page
 *  therefore rendered the finished state at t=0 with the multiple choice list
 *  absent entirely. That is exactly what was reported, and the earlier
 *  instructions had each asked for precisely that behaviour, so nothing looked
 *  wrong from the inside.
 *
 *  WHAT IT DOES NOW: it removes MOTION, not the story.
 *
 *  `prefers-reduced-motion` exists for vestibular triggers, which are movement,
 *  scaling, parallax and spinning. A gentle opacity cross-fade is not one, and
 *  removing it costs the page its entire meaning: the question, the four
 *  options, the selection and the narrative are the content, not decoration.
 *
 *  So under reduced motion the sequence still plays, at the same pace, with the
 *  options appearing exactly as they do for everyone else. What is stripped is
 *  every transform: the heading no longer rises and grows, it simply fades in
 *  at its final size and position, and the three words no longer drop in, they
 *  fade in where they belong. Nothing on the page moves. Nothing is hidden.
 *
 *  The slideshow is the one thing still frozen to a single photograph, because
 *  it is endless rather than a one-off and a background that changes forever is
 *  a different proposition from a reveal that finishes in nine seconds.
 * ═══════════════════════════════════════════════════════════════════════════
 */
@media (prefers-reduced-motion: reduce) {
  /*
   * THE SLIDESHOW KEEPS CROSS-FADING (changed by WP008 Part C).
   *
   * It used to be frozen to a single photograph here, which meant the owner,
   * whose machine has Reduce Motion on, reported that the background never
   * changed. That was the same root cause as WP007 Fix 4's static reveal, in the
   * same media block, and it survived that fix because freezing the slideshow
   * looked like the conservative choice at the time.
   *
   * It is not the conservative choice, it is just a smaller page. The cross-fade
   * is opacity and nothing else: no movement, no scaling, no parallax, so it is
   * not a vestibular trigger and it is exactly the kind of animation that
   * `prefers-reduced-motion` was never meant to remove. The same reasoning that
   * kept the reveal playing in Fix 4 applies here, and applying it in one place
   * and not the other was the inconsistency.
   *
   * Nothing else in this block touches .slide, so the keyframes above run for
   * everyone.
   */

  /*
   * The question and the options are unchanged: they were only ever opacity.
   * The two that carried movement are swapped for opacity-only keyframes, so
   * the timing, the order and the pacing all survive without anything moving.
   */
  .answer { animation-name: reveal-answer-still; }
  .word   { animation-name: reveal-word-still; }

  /*
   * The options now TRAVEL as they leave, which is movement and has to go here
   * too. Their keyframes carry opacity and transform in one set, with different
   * percentages per option, so three more "-still" variants would say the same
   * thing three times. An important declaration says it once: an author
   * !important beats an animation in the cascade, so the fade survives and the
   * journey does not.
   */
  .option { transform: none !important; }

  .signin { transition: none; }
}

/* The heading appears at full size in its own place, without rising or growing. */
@keyframes reveal-answer-still {
  0%       { opacity: 0; }
  27.273%  { opacity: 1; }
  100%     { opacity: 1; }
}

/* Each word appears where it belongs, without dropping in. */
@keyframes reveal-word-still {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ─────────────────────────────────────────────────────────── small screens */

@media (max-width: 30rem) {
  body.landing { padding: 7rem 1rem 2rem; }

  .navbar { padding: 0.6rem 1rem; }

  .wordmark { font-size: 0.875rem; }

  .stage { padding: 1.25rem 1.15rem 1.4rem; }

  .narrative { font-size: 1.0625rem; }

  /* The heading is smaller here, so the scale it starts at has to be larger to
     still match an option line, and it has less distance to travel. */
  .answer { animation-name: reveal-answer-narrow; }

  @keyframes reveal-answer-narrow {
    0%       { opacity: 0; transform: translateY(3.4rem) scale(0.55); }
    27.273%  { opacity: 1; transform: translateY(3.4rem) scale(0.55); }
    100%     { opacity: 1; transform: translateY(0) scale(1); }
  }
}
