/* The journeys, laid out to the founders' concept draft.
 *
 * One fixed canvas painting one continuous take; one fixed copy layer; one
 * empty spacer as the scroll track. Every chapter's words sit lower left with
 * one gold phrase; the CTA dock keeps the page's action always in reach; the
 * rail names chapters as they are reached; the ring closes as the loop comes
 * home. Nothing else is in document flow, so there is no seam anywhere. */

.film {
  display: block; pointer-events: none;
  --edge-l: calc(var(--gut) + var(--rail-clear, 0rem));
  --edge-b: clamp(6.5rem, 15vh, 9.5rem);       /* clears the dock */
  --label-inset: max(var(--gut), 4vw);         /* and clears the rail */
}
@media (min-width: 641px)  { .film { --rail-clear: 2.6rem; --label-inset: max(4vw, 5.5rem); } }
@media (min-width: 1401px) { .film { --rail-clear: 12rem; --label-inset: max(4vw, 15rem); } }

.flight { position: relative; }
/* ---------------------------------------------------- landing on a chapter

   The film settles on a chapter rather than wherever the thumb ran out, the
   way a feed settles on an item. The targets already existed: `.anchor` is one
   per chapter, sitting at the rest, which is the frame that chapter's words
   were written over. All this does is tell the scroller they are snap points.

   `mandatory`, not `proximity`, because the request was that it lands on a
   chapter however hard you scroll, and proximity gives up when you throw it.
   Mandatory has one trap and it is a bad one: a scroll position with no snap
   point near it becomes unreachable. The last chapter sits at 77 per cent of
   the track, so with the anchors alone the page simply stopped there and the
   next-stop card and the whole footer could not be reached at all. Measured:
   6673 of 8550, 1877px of page that no longer existed. The two rules at the
   bottom are what make the tail reachable, and they are not decoration.

   Scrubbing is unaffected: the film still runs continuously under the drag.
   What changes is only where it comes to rest. */
.anchor { position: absolute; left: 0; width: 1px; height: 1px; }
.anchor--top { top: 0; }
/* The snap targets are placed by stage.js, at the middle of each copy block's
   readable plateau. They are not the anchors: see the note at `placeSnaps`.
   `data-snap` is only set once they have been positioned, so the page never
   snaps to a target that is still at its default place. */
/* `scroll-snap-stop: always` is here for prefers-reduced-motion ONLY, and it
   is not what delivers "one section per scroll" for anybody else.

   Both snap properties below are inert for the ordinary reader: `html` only
   carries `data-snap="on"` under reduced motion, so `scroll-snap-type`
   computes to `none` and neither `align` nor `stop` has anything to act on.
   One-section-per-gesture comes entirely from the wheel and touch handler in
   stage.js. Measured: disable that handler and an identical 40-event flick
   crosses 4 stops in Chromium and 3 in WebKit; force CSS snapping back on and
   WebKit still crosses 4. That is the same failure stage.js records at its own
   handler, and it is why the handler exists.

   An earlier version of this note said the opposite — that `always` "is the
   whole of one section per scroll" and that a hand-rolled wheel handler is
   what you end up fighting. It read well and it was wrong, and stage.js said
   so in the same build. In a file where the prose is the design record and two
   sessions edit at once, a maintainer trusting that note would delete the
   handler that does the work.

   What IS true, and is why the properties stay: without `always` a snap
   position is only a PREFERRED landing, so under reduced motion the browser
   would sail over as many as momentum carried it. Measured before it was
   added, from the top: a 3000px flick crossed 2 chapters, 6000px crossed 6,
   and 12000px crossed 7 — the entire film in one gesture.

   The rail's jump links are deliberately exempt: travelTo() in stage.js clears
   scroll-snap-type inline for the length of a flight, which it already had to
   do, and which matters far more now — without it, clicking chapter 8 would be
   forced to stop at every chapter on the way. */
[data-snaps] > i { position: absolute; left: 0; width: 1px; height: 1px;
  scroll-snap-align: start; scroll-snap-stop: always; }
html[data-snap="on"] { scroll-snap-type: y mandatory; }
/* The tail. Without these the footer is unreachable, not merely awkward. */
/* `.foot` is load-bearing and must not be tidied away. Under
   prefers-reduced-motion the CSS snap is live and mandatory, and mandatory
   snapping makes any stretch with no snap point in it unreachable. The last
   chapter stop sits at 6821 of 8271 on the homepage. Measured with this one
   declaration removed: the page stops dead at 6821, **1450px short**, and the
   footer is never seen at all; 1718px short on the ecosystem journey. It is in
   flow, unlike `.nextstop`, so `end` alignment means what it says.

   `.nextstop` used to carry `scroll-snap-align: center` here and it is gone on
   purpose. On the homepage it sits inside `.copylayer`, which is
   `position: fixed`, so it is not in the scroll container's coordinate space
   and its reported offset is a viewport coordinate with a scroll value added
   to it. On the ecosystem journey it does not exist. Both engines ignore it,
   so removing it changes no measured behaviour, but a snap target that is not
   one is the kind of thing the next reader spends an hour on. */
.foot { scroll-snap-align: end; }
.spacer { pointer-events: none; }

/* ----------------------------------------------------------- the stage */

.stage {
  position: fixed; inset: 0; z-index: 0;
  width: 100vw; height: 100vh; height: 100svh;
  display: block; background: var(--film); pointer-events: none;
}

/* The full-resolution frame that lands when the scroll comes to rest. An <img>,
   not a canvas paint: the canvas backing store is capped at the frame width,
   while an image gets the browser's own scaler at whatever the display can
   actually show. It sits over the scrub and under everything else. */
.sharp {
  position: fixed; inset: 0; z-index: 0;
  width: 100vw; height: 100vh; height: 100svh;
  object-fit: cover; object-position: center;
  pointer-events: none; opacity: 0;
  transition: opacity .42s ease;
}
.sharp[data-on] { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .sharp { transition: none; } }

/* ------------------------------------------------ phone journeys
   This attribute is stamped in <head>, before first paint. Phones never display
   or allocate the fixed canvas journey; the route's mobile script moves the
   existing copy into ordinary sections and stage.js exits before any work. */
html:is([data-mobile-home], [data-mobile-ecosystem]) .rail,
html:is([data-mobile-home], [data-mobile-ecosystem]) .stage,
html:is([data-mobile-home], [data-mobile-ecosystem]) .sharp,
html:is([data-mobile-home], [data-mobile-ecosystem]) .loader,
html:is([data-mobile-home], [data-mobile-ecosystem]) .ring,
html:is([data-mobile-home], [data-mobile-ecosystem]) .hint,
html:is([data-mobile-home], [data-mobile-ecosystem]) .dock { display: none !important; }
html:is([data-mobile-home], [data-mobile-ecosystem]) .film { min-height: 100vh; background: var(--film); }
html[data-mobile-home]:not([data-mobile-home-ready]) .film,
html[data-mobile-ecosystem]:not([data-mobile-ecosystem-ready]) .film { visibility: hidden; }
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-journey {
  --chapter-count: 1;
  position: relative;
  display: block;
  height: calc(var(--chapter-count) * 100svh);
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-journey__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter {
  position: absolute;
  inset: 0;
  min-height: 100svh;
  display: grid;
  align-items: end;
  overflow: clip;
  isolation: isolate;
  opacity: 0;
  will-change: opacity;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter[aria-hidden="true"],
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter[aria-hidden="true"] * {
  pointer-events: none !important;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__image,
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__shade {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__image {
  z-index: -2;
  object-fit: cover;
  object-position: center;
  will-change: opacity;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__shade {
  z-index: -1;
  background: linear-gradient(180deg, rgba(7, 6, 5, .12) 5%, rgba(7, 6, 5, .42) 45%, rgba(7, 6, 5, .9) 100%);
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__body {
  width: min(100%, 46rem);
  margin-inline: auto;
  padding: clamp(7rem, 20svh, 11rem) max(var(--gut), env(safe-area-inset-left))
    max(3.5rem, calc(2rem + env(safe-area-inset-bottom)));
  display: grid;
  gap: 1.2rem;
  will-change: opacity, transform;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__label {
  margin: 0;
  color: #e3bf67;
  font: 600 .76rem/1.2 var(--sans);
  letter-spacing: .14em;
  text-transform: uppercase;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .copy,
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels,
html[data-mobile-ecosystem] .mobile-chapter .note {
  position: static !important;
  inset: auto !important;
  width: auto !important;
  max-width: none !important;
  opacity: 1 !important;
  transform: none !important;
  translate: none !important;
  scale: none !important;
  pointer-events: auto;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .copy { display: grid; gap: 1rem; }
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels ul {
  position: static;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: .6rem;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels li {
  position: static;
  opacity: 1 !important;
  transform: none !important;
  translate: none !important;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels li::before,
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels li::after,
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels::before,
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .pin { display: none; }
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels .lbl {
  padding: .45rem .7rem;
  background: rgba(8, 6, 4, .76);
  border: 1px solid rgba(255, 255, 255, .24);
  border-radius: 999px;
}
html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter .labels .lbl::before { display: none; }
html[data-mobile-home] .mobile-chapter .panel {
  display: grid;
  grid-template-columns: 1fr;
  grid-template-areas: "say" "do" "meta";
  gap: 1.5rem;
}
html[data-mobile-home] .mobile-chapter .panel__do > form.reg { display: grid; }
html[data-mobile-home] .mobile-chapter .panel__cta { display: none; }
html[data-mobile-home] .mobile-chapter .panel__meta { display: grid; grid-template-columns: 1fr; }
/* The final Summit chapters leave the cinematic stack and return to ordinary
   document flow. Their form, controls, links, and cards remain on screen for
   as long as each visitor needs rather than being tied to scroll progress. */
html[data-mobile-home] .mobile-journey-tail {
  position: relative;
  background: var(--film);
}
html[data-mobile-home] .mobile-journey-tail .mobile-chapter {
  position: relative !important;
  inset: auto !important;
  min-height: 100svh;
  height: auto !important;
  opacity: 1 !important;
  pointer-events: auto !important;
  overflow: hidden !important;
  will-change: auto;
}
html[data-mobile-home] .mobile-journey-tail .mobile-chapter__body {
  transform: none !important;
  opacity: 1 !important;
  will-change: auto;
}
html[data-mobile-home] .mobile-journey-tail .mobile-chapter__image,
html[data-mobile-home] .mobile-journey-tail .mobile-chapter__shade {
  opacity: 1 !important;
}
html[data-mobile-home] .mobile-journey-tail .mobile-chapter--return {
  align-items: start;
}
html[data-mobile-home] .mobile-chapter--return .mobile-chapter__body {
  padding-top: max(6.5rem, calc(5rem + env(safe-area-inset-top)));
  padding-bottom: max(2rem, calc(1rem + env(safe-area-inset-bottom)));
}
html[data-mobile-home] .mobile-chapter--climb { color: #17120b; }
html[data-mobile-home] .mobile-chapter--climb .mobile-chapter__shade {
  background: linear-gradient(180deg, rgba(232, 238, 241, .16), rgba(232, 238, 241, .92) 62%, #e8eef1 100%);
}
html[data-mobile-home] .mobile-chapter--climb .mobile-chapter__label { color: #46340a; }
html[data-mobile-home] .mobile-chapter .nextstop { max-width: none; }
html:is([data-mobile-home], [data-mobile-ecosystem]) .foot { scroll-snap-align: none; }
html[data-mobile-ecosystem] .mobile-chapter .note {
  display: block;
  margin-top: .5rem;
}
html[data-mobile-ecosystem] .mobile-chapter--invitation .mobile-chapter__body {
  padding-top: 8rem;
}

@media (orientation: landscape) and (max-height: 600px) {
  html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter {
    min-height: 100svh;
    align-items: center;
  }
  html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter__body {
    padding-top: 6rem;
    padding-bottom: max(2rem, env(safe-area-inset-bottom));
  }
  html:is([data-mobile-home], [data-mobile-ecosystem]) .display--xl { font-size: clamp(2.25rem, 7vw, 4rem); }
  html:is([data-mobile-home], [data-mobile-ecosystem]) .display--lg { font-size: clamp(2rem, 6vw, 3.5rem); }
}

@media (prefers-reduced-motion: reduce) {
  html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter *,
  html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter *::before,
  html:is([data-mobile-home], [data-mobile-ecosystem]) .mobile-chapter *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

/* If a browser cannot allocate a 2D canvas, stage.js marks the document and
   leaves the real chapter markup as a lightweight scrolling presentation.
   This is progressive enhancement rather than a blank loader or a dead page. */
html[data-journey-static] .stage,
html[data-journey-static] .sharp,
html[data-journey-static] .rail,
html[data-journey-static] .spacer,
html[data-journey-static] .wash,
html[data-journey-static] .loader,
html[data-journey-static] .ring,
html[data-journey-static] .hint,
html[data-journey-static] .defocus,
html[data-journey-static] .mend { display: none !important; }
html[data-journey-static] .flight { position: static; }
html[data-journey-static] .copylayer {
  position: static; display: block; overflow: visible;
}
html[data-journey-static] .copy,
html[data-journey-static] .labels,
html[data-journey-static] .note {
  position: static !important; opacity: 1 !important; transform: none !important;
  min-height: 70vh; display: grid; align-content: center;
  padding: 5rem var(--gut);
  background: linear-gradient(rgba(9,7,5,.55), rgba(9,7,5,.55)),
    var(--journey-poster, url(/assets/p1/poster-0.jpg)) center / cover no-repeat;
}
html[data-journey-static] .copylayer > :nth-child(3) { --journey-poster: url(/assets/p1/poster-1.jpg); }
html[data-journey-static] .copylayer > :nth-child(4) { --journey-poster: url(/assets/p1/poster-2.jpg); }
html[data-journey-static] .copylayer > :nth-child(5) { --journey-poster: url(/assets/p1/poster-3.jpg); }
html[data-journey-static] .copylayer > :nth-child(6) { --journey-poster: url(/assets/p1/poster-4.jpg); }
html[data-journey-static] .copylayer > :nth-child(7) { --journey-poster: url(/assets/p1/poster-5.jpg); }
html[data-journey-static] .copylayer > :nth-child(8) { --journey-poster: url(/assets/p1/poster-6.jpg); }
html[data-journey-static] .copylayer > :nth-child(n+9) { --journey-poster: url(/assets/p1/poster-7.jpg); }
html[data-journey-static] .labels ul {
  display: flex; flex-wrap: wrap; justify-content: center; gap: .55rem 2.4rem;
}
html[data-journey-static] .labels li { transform: none !important; }
html[data-journey-static] .pin,
html[data-journey-static] .trailslayer { display: none; }
html[data-journey-static] .dock {
  position: static; opacity: 1 !important; padding: 1.5rem var(--gut);
}

/* the soften into the final chapter: the scene stays, defocused and lifted */
/* The soften into the final chapter. It used to run at 11px, from when the
   plate under the words was a flat wash and the film had to be pushed right
   back for them to read. The two cloud floors now carry that on their own, so
   the city can stay legible under the mist: at 6px the belfry and the canal are
   still recognisable, which is the point of ending the flight over Bruges. */
.defocus {
  position: fixed; inset: 0; z-index: 1; pointer-events: none; opacity: 0;
  backdrop-filter: blur(6px) saturate(1.06) brightness(1.03);
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, #000 15%, #000 82%, rgba(0,0,0,.35) 100%);
  mask-image: linear-gradient(180deg, transparent 0%, #000 15%, #000 82%, rgba(0,0,0,.35) 100%);
  background: linear-gradient(180deg,
    rgba(255,252,244,.52) 0%, rgba(254,249,238,.3) 28%, rgba(251,244,228,.1) 56%,
    rgba(18,13,9,.06) 78%, rgba(14,10,7,.2) 100%);
}
/* the ascent comes home to its own valley, so the close darkens rather than
   brightens: the mountain stays the picture and the cream cards sit on it */
.defocus--valley {
  backdrop-filter: blur(8px) saturate(1.1) brightness(1);
  -webkit-mask-image: linear-gradient(180deg, rgba(0,0,0,.3) 0%, #000 18%, #000 80%, rgba(0,0,0,.3) 100%);
  mask-image: linear-gradient(180deg, rgba(0,0,0,.3) 0%, #000 18%, #000 80%, rgba(0,0,0,.3) 100%);
  background: radial-gradient(126% 110% at 50% 44%,
    rgba(12,9,6,0) 0%, rgba(10,8,5,.06) 62%, rgba(8,6,4,.2) 100%);
}

.copylayer { position: fixed; inset: 0; z-index: 2; pointer-events: none; overflow: hidden; }
[data-copy] { opacity: 0; will-change: opacity, transform; }

/* a quiet vignette so the frame never feels raw at the edges */
.wash {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background:
    linear-gradient(90deg, rgba(10,8,6,.55) 0%, rgba(10,8,6,.26) 14%, rgba(10,8,6,0) 30%),
    radial-gradient(128% 92% at 50% 44%, transparent 42%, rgba(10,8,6,.24) 76%, rgba(10,8,6,.5) 100%),
    linear-gradient(180deg, rgba(10,8,6,.42) 0%, transparent 18%),
    linear-gradient(0deg, rgba(10,8,6,.5) 0%, transparent 30%);
}

/* --------------------------------------------------------- copy blocks
   One anchor for the whole journey: lower left, above the dock, per the
   concept draft. The scrim is a soft plate shaped to the block itself. */

.copy {
  position: absolute; z-index: 2;
  left: var(--edge-l); bottom: var(--edge-b);
  width: min(44rem, calc(100vw - var(--edge-l) - var(--gut)));
  display: grid; gap: 1rem; justify-items: start; text-align: left;
}
.copy::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  inset: -2.2rem -7rem -7rem -8rem; border-radius: 4rem;
  background: rgba(8,6,4,.78); filter: blur(46px);
}
.copy .display .em { color: var(--gold-hi); }
.copy__sub {
  font-size: clamp(.98rem, .92rem + .3vw, 1.14rem);
  color: var(--bone-soft); max-width: 46ch;
}
.copy__acts { display: flex; flex-wrap: wrap; gap: .8rem 1.1rem; align-items: center; }

/* ------------------------------------------------------ the label sets
   Bone-white small caps with thin leader lines dropping toward the scene,
   at slightly varied heights, per the concept draft's ridge frame. */

.labels {
  position: absolute; z-index: 1;
  left: var(--label-inset); right: var(--label-inset);
  top: clamp(5.5rem, 13vh, 9rem);
  pointer-events: none;
}
/* The band starts above the viewport on purpose: a scrim whose top edge lands
   on screen draws a hard horizontal line across the film, which is exactly
   what the old bottom-anchored doorway row did. */
.labels::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  left: -50vw; right: -50vw; top: -10rem; height: 23rem;
  background: linear-gradient(180deg,
    rgba(8,6,4,.82) 0%, rgba(8,6,4,.78) 46%, rgba(8,6,4,.64) 66%,
    rgba(8,6,4,.4) 82%, rgba(8,6,4,.14) 93%, rgba(8,6,4,0) 100%);
}
.labels ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  align-items: start; gap: clamp(.3rem, 1vw, 1rem);
}
.labels li {
  --d: calc(var(--i) - var(--mid, 3));
  position: relative;
  font-family: var(--sans); font-size: clamp(.66rem, .56rem + .3vw, .85rem);
  letter-spacing: .17em; text-transform: uppercase; font-weight: 600;
  color: #fffaf1; white-space: nowrap; text-align: center;
  text-shadow:
    0 0 1px rgba(8,6,4,.98), 0 1px 2px rgba(8,6,4,.98),
    0 0 8px rgba(8,6,4,.92), 0 0 18px rgba(8,6,4,.7);
  /* the row bows: highest at the centre, falling away at both ends, so it
     reads as the span it is annotating rather than a line running off frame */
  transform: translateY(calc(var(--d) * var(--d) * var(--bow, .34rem)
    + var(--odd, 0) * var(--stagger, 0rem)));
  opacity: clamp(0, calc((var(--sc-segp, 1) - var(--at)) / .08), 1);
}
/* the leader line ends in a small dot on the span, per the concept draft */
.labels li::before {
  content: ""; position: absolute; z-index: -1; left: 50%; bottom: 0;
  translate: -50% 50%;
  width: 4px; height: 4px; border-radius: 50%;
  background: rgba(255,250,241,.92); box-shadow: 0 0 6px rgba(8,6,4,.85);
}
.labels li::after {
  content: ""; display: block; width: 1px; margin: .55rem auto 0;
  /* the second rank hangs lower, so its leader is shorter by exactly as much
     and every dot still lands on the same curve */
  height: calc(clamp(2.2rem, 7vh, 5rem) - var(--odd, 0) * var(--stagger, 0rem));
  background: linear-gradient(rgba(246,238,225,.85), rgba(246,238,225,0));
}
/* Each name carries its own soft plate. A band deep enough to hold white text
   over a sunrise sky on its own would darken the whole top of the picture; a
   blurred pill under the words does it locally and stays invisible. */
.labels .lbl { position: relative; display: inline-block; }
.labels .lbl::before {
  content: ""; position: absolute; z-index: -1; inset: -.42rem -.95rem;
  border-radius: 1.4rem; background: rgba(8,6,4,.82); filter: blur(11px);
}

/* The bow for a row short enough to hold one rank. A row of six or more sets
   its own, flatter, from the emitter, because it is also staggered. */
.labels--spans { --bow: .5rem; }    /* under the arch, so a deep bow */
.labels--ridge { --bow: .34rem; }   /* a ridgeline, so a shallower one */

/* ------------------------------------------------ trails to the summit
   The ascent row keeps the band, its scrim and its plates, all of which are the
   only reason white type is readable over this sunrise at all. What it loses is
   the seven parallel leaders that ended in dots in the open sky: the lines are
   now routes that drop from each name, dip onto the mountain and climb to a
   light on the summit, drawn frame by frame in stage.js because both of their
   ends move. See the note there, and the one in build/journey.mjs for why the
   names did not move onto the mountain instead. */

.labels--trails { --bow: .34rem; }

/* the leader and its dot are what the trail replaces */
.labels--trails li::after, .labels--trails li::before { content: none; }

/* Its own layer, because the row is inset to clear the rail and the trails have
   to reach the middle of the frame. Not a [data-copy] block: the stage slides
   those 1.6vh as they fade, and a trail that slides while the peak it points at
   does not would come off the summit exactly when it is most looked at. It
   takes the row's own opacity in stage.js instead. */
.trailslayer {
  position: absolute; inset: 0; z-index: 1;
  pointer-events: none; opacity: 0;
  --pin: 220, 188, 113;                /* gold-hi, as parts, so light can carry alpha */
}
.trails { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; fill: none; }

/* A hairline of warm light reads over the sky and vanishes the moment it
   crosses sunlit snow, which is most of what it crosses on the way down. The
   drop shadow is the same trick the names use with their plates: carry your
   own darkness. */
.trail {
  stroke-width: 1.3px; stroke-linecap: round;
  filter: drop-shadow(0 0 1.5px rgba(8,6,4,.92));
  opacity: clamp(0, calc((var(--sc-segp, 1) - var(--at)) / .08), 1);
}

/* where the paths meet: the same point of light the canal markers use, because
   it is the same idea, a named place in the picture rather than a graphic on
   top of it. Its position is written by stage.js, off the peak itself. */
.summit {
  position: absolute; left: 50.4%; top: 22%; translate: -50% -50%;
  width: 8px; height: 8px; border-radius: 50%;
  background: #fff3d4;
  box-shadow:
    0 0 0 1.5px rgba(8,6,4,.55),        /* a dark seat, or it is lost in the sunrise */
    0 0 7px 1px rgba(var(--pin), 1),
    0 0 22px 7px rgba(var(--pin), .45);
}
/* the bloom the thin air throws around it */
.summit::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 6.5rem; height: 6.5rem; translate: -50% -50%; border-radius: 50%;
  background: radial-gradient(closest-side, rgba(var(--pin), .3), rgba(var(--pin), 0) 70%);
}

/* --------------------------------------------------- pins on the water
   The programme row does not hang out of the sky. Five markers stand IN the
   canal: a point of light on the water, a thin stem, the name above it, and
   the reflection running back toward the camera, which is the part that sells
   it, because every real lamp along this canal already does exactly that.

   They are a CHAIN, not a row, for two reasons. The first is the picture: the
   canal recedes to a vanishing point, so markers on it must recede too, and
   five evenly spaced across the frame would sit on the quay walls at both ends
   where there is no water to stand in. The second is the page: this chapter's
   words own the lower left, which is also the widest, nearest water. Running
   the chain down the right bank keeps every base and every reflection off the
   headline, and gives each name its own horizontal band, so the labels can
   sit closer together than their own width without touching.

   --t is the position along the chain, 0 at the far end, 1 at the near one.
   Everything reads off it: where the pin stands, how tall its stem is, how
   long the reflection runs, and how big the whole marker is. */

.labels--pins {
  --pin: 220, 188, 113;      /* gold-hi, kept as parts so the light can carry alpha */
  /* The chain used to run down the RIGHT BANK, because the headline sat lower
     left and its blurred plate owned the near water: a row across the canal put
     the outer stems straight through the type. The headline has since been
     lifted to cover a fault in the take, which hands the water back, and the
     old position now has the opposite problem, the first marker sitting under
     the raised block.

     So the chain comes down onto the water it was always meant to stand in.
     Measured at 1440x900: the headline block ends at .592 and the dock begins
     at .902, and every marker rides ABOVE its own foot, so the far foot has to
     clear .592 by the height of a label, not merely sit below it. */
  --far-x: 34%;  --far-y: .672;    /* far up the canal, clear of the block */
  --near-x: 90%; --near-y: .862;   /* the near one, in the wide water */
  inset: 0; pointer-events: none;
}
/* No band. This row's top edge lands mid-frame, and a scrim whose top edge
   lands on screen draws a hard horizontal line across the film. The names keep
   their own local plates, and the water is the darkest part of the picture. */
.labels--pins::before { content: none; }

.labels--pins ul { display: block; position: absolute; inset: 0; }

.labels--pins li {
  --t: calc(var(--i) / var(--last, 4));
  position: absolute;
  left: calc(var(--far-x) + (var(--near-x) - var(--far-x)) * var(--t));
  /* The film is cover-fitted, so on any window wider than 16:9 the crop takes
     equal bites out of top and bottom and a fraction of the frame stops being
     a fraction of the screen. --sc-vfill (set in stage.js) is how much taller
     than the viewport the fitted frame is; this maps one space to the other so
     the bases land on water at every window, not only the one they were drawn
     at. */
  --y: calc(var(--far-y) + (var(--near-y) - var(--far-y)) * var(--t));
  top: calc((.5 + (var(--y) - .5) * var(--sc-vfill, 1)) * 100%);
  /* anchored by its FOOT: the pin is the fixed point, the name rides above it */
  translate: -50% -100%;
  transform-origin: 50% 100%;
  scale: calc(.84 + var(--t) * .34);         /* nearer is nearer, so bigger */
  transform: none;                            /* the bow belongs to the rows */
}
.labels--pins .lbl::before { background: rgba(8,6,4,.86); }

/* the stem is light coming OUT of the water, so it is brightest at the base,
   and the near markers stand taller because they are closer */
.labels--pins li::after {
  width: 1px; margin-top: .6rem;
  height: calc(1.7rem + var(--t) * 2.2rem);
  background: linear-gradient(rgba(var(--pin), 0) 0%,
    rgba(var(--pin), .3) 40%, rgba(var(--pin), .95) 100%);
}
/* the bone dot belongs to the rows that hang; here the base is the pin itself */
.labels--pins li::before { content: none; }

.pin {
  position: absolute; left: 50%; bottom: 0; translate: -50% 50%;
  width: 5px; height: 5px; border-radius: 50%;
  background: #ffeaba;
  box-shadow: 0 0 5px 1px rgba(var(--pin), .9), 0 0 15px 4px rgba(var(--pin), .34);
}
/* the bloom the water throws back around the point */
.pin::before {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 4.2rem; height: 1.8rem; translate: -50% -50%; border-radius: 50%;
  background: radial-gradient(closest-side,
    rgba(var(--pin), .32), rgba(var(--pin), .09) 54%, rgba(var(--pin), 0) 100%);
}
/* and the reflection: a lamp streak, widening and softening as it comes back
   toward the camera. The near markers throw a longer one, for the same reason
   the near lamps in the footage do */
.pin::after {
  content: ""; position: absolute; left: 50%; top: 2px;
  width: 1.15rem; height: calc(1.7rem + var(--t) * 1.9rem);
  translate: -50% 0; transform-origin: 50% 0;
  clip-path: polygon(41% 0%, 59% 0%, 100% 100%, 0% 100%);
  background: linear-gradient(rgba(var(--pin), .6),
    rgba(var(--pin), .19) 38%, rgba(var(--pin), 0) 100%);
  filter: blur(1.7px);
  animation: pin-ripple 5.4s ease-in-out calc(var(--i) * -.94s) infinite;
}
@keyframes pin-ripple {
  0%, 100% { transform: scaleX(1); }
  50%      { transform: scaleX(1.2); }
}
}

/* the one permitted statistic, annotated in the scene with a leader line */
.note {
  position: absolute; z-index: 1; right: clamp(1.5rem, 6vw, 6rem); top: clamp(7rem, 22vh, 13rem);
  max-width: 15rem; text-align: left; pointer-events: none;
}
.note::before {
  content: ""; position: absolute; z-index: -1; inset: -1.1rem -1.8rem;
  border-radius: 2rem; background: rgba(8,6,4,.8); filter: blur(22px);
}
.note p {
  font-family: var(--sans); font-size: .8rem; letter-spacing: .06em;
  color: var(--gold-hi); line-height: 1.55;
  text-shadow: 0 1px 12px rgba(8,6,4,.95);
}
.note::after {
  content: ""; position: absolute; left: .2rem; top: 100%;
  width: 1px; height: clamp(2.5rem, 9vh, 6rem);
  background: linear-gradient(rgba(203,166,78,.9), rgba(203,166,78,0));
}

/* -------------------------------------------------------- the CTA dock */

.dock {
  position: fixed; z-index: 30;
  left: calc(var(--gut) + var(--rail-clear, 0rem));
  bottom: clamp(1.4rem, 4.5vh, 2.6rem);
  display: flex; align-items: center; gap: .8rem 1rem; flex-wrap: wrap;
  transition: opacity .3s ease;
}
@media (min-width: 641px)  { .dock { --rail-clear: 2.6rem; } }
@media (min-width: 1401px) { .dock { --rail-clear: 12rem; } }
/* The dock must never grow into the hint's lane in the centre. It carries at
   most two buttons now, but a longer label in a future edit should push the
   second button onto its own line rather than across the middle of the
   screen. `flex-wrap` above already does that; this is what tells it when. */
.dock { max-width: min(38rem, 46vw); }
@media (max-width: 900px) { .dock { max-width: calc(100vw - 2 * var(--gut) - 5rem); } }

/* ---------------------------------------------------------------- rail
   Numbered dots joined by a line; a chapter's name appears once you have
   reached it, the current one in gold. */

.rail {
  position: fixed; left: clamp(.9rem, 2.2vw, 2rem); top: 50%; translate: 0 -50%;
  z-index: 40; pointer-events: auto; isolation: isolate;
}
.rail ol { list-style: none; margin: 0; padding: 0; display: grid; }
.rail li { position: relative; }
.rail li:not(:last-child)::after {
  content: ""; position: absolute; left: calc(.75rem - .5px); top: 1.7rem;
  width: 1px; height: calc(100% - 1.55rem);
  background: rgba(246,238,225,.28);
}
.rail a {
  display: grid; grid-template-columns: 1.5rem 1.1rem auto; align-items: center; gap: .5rem;
  padding: .5rem .4rem .5rem 0; text-decoration: none;
  font-size: .6rem; letter-spacing: .12em; text-transform: uppercase; font-weight: 600;
  color: rgba(246,238,225,.55); transition: color .3s ease;
}
.rail__dot {
  width: .95rem; height: .95rem; border-radius: 50%; justify-self: center;
  border: 1px solid rgba(246,238,225,.55);
  transition: background .3s ease, border-color .3s ease, box-shadow .3s ease;
}
.rail__n { color: rgba(246,238,225,.6); transition: color .3s ease; }
.rail__name { opacity: 0; transform: translateX(-4px); transition: opacity .35s ease, transform .35s ease; }
.rail li.is-past .rail__name, .rail li.is-current .rail__name { opacity: 1; transform: none; }
.rail li.is-past a { color: rgba(246,238,225,.72); }
.rail li.is-current a { color: var(--gold-hi); }
.rail li.is-current .rail__n { color: var(--gold-hi); }
.rail li.is-current .rail__dot {
  background: var(--gold); border-color: var(--gold);
  box-shadow: 0 0 0 3px rgba(203,166,78,.22);
}
.rail a:hover { color: #fdf8f0; }

@media (max-width: 1400px) {
  .rail a { grid-template-columns: 1.5rem; }
  .rail__n, .rail__name { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
}
@media (max-width: 640px) { .rail { display: none; } }

/* ------------------------------------------------------- ring and hint */

.ring {
  position: fixed; right: clamp(1.2rem, 3vw, 2.4rem); bottom: clamp(1.4rem, 4.5vh, 2.6rem);
  z-index: 30; width: clamp(2.4rem, 4vw, 3.2rem); aspect-ratio: 1; pointer-events: none;
}
.ring svg { width: 100%; height: 100%; transform: rotate(-90deg); }
.ring circle { fill: none; stroke-width: 1.6; }
.ring__track { stroke: rgba(246,238,225,.22); }
.ring__arc {
  stroke: var(--gold); stroke-linecap: round;
  stroke-dasharray: 97.39; stroke-dashoffset: 97.39;
}

.hint {
  position: fixed; left: 50%; translate: -50% 0; bottom: clamp(1.2rem, 4vh, 2.2rem);
  z-index: 30; pointer-events: none; text-align: center;
  font-family: var(--sans); font-size: .64rem; letter-spacing: .34em;
  text-transform: uppercase; color: rgba(246,238,225,.75);
  text-shadow: 0 1px 10px rgba(8,6,4,.8);
  transition: opacity .6s ease;
}
/* The box is 11px wide, the LINE is still 1px.

   It has to be, because the box clips (the bead must not appear above or
   below the line) and a 1px box would clip the bead's glow to nothing as
   well. So the line moved from being the element's background to being a 1px
   background stripe centred in a wider one, which renders identically and
   leaves 5px either side for the light to spread into. */
.hint span {
  display: block; position: relative; overflow: hidden;
  width: 11px; height: clamp(1.6rem, 5vh, 2.6rem);
  margin: 0 auto .6rem;
  background: linear-gradient(rgba(246,238,225,0), rgba(246,238,225,.8)) 50% 0 / 1px 100% no-repeat;
}

/* A gold bead falling down the line, once every two and a half seconds.

   The line itself stays exactly as it was: this rides on top of it, so if the
   animation never runs the cue is still the static line it has always been.
   That matters, because this is the only thing on a journey that tells a
   first-time visitor the page moves at all, and a cue that depends on an
   animation to be legible is not a cue.

   Slow and small on purpose. The hint sits over a photograph for the whole of
   chapter one, and anything faster reads as a notification rather than an
   invitation. It stops as soon as the visitor scrolls, because by then it has
   said what it had to say: `data-hide` is set by journey.js on first scroll. */
.hint span::after {
  content: ""; position: absolute; left: 50%; top: 0; margin-left: -.5px;
  width: 1px; height: 55%;
  background: linear-gradient(rgba(203,166,78,0), var(--gold));
  /* A 1px line on a photograph is almost nothing. The glow is what makes the
     bead read at all at normal scale, and it costs nothing: it is the same
     gold, spread, so it reads as the line catching light rather than as a
     second element. */
  box-shadow: 0 0 5px 1px rgba(203,166,78,.55);
  animation: hint-bead 2.5s cubic-bezier(.45, 0, .25, 1) infinite;
}
@keyframes hint-bead {
  0%, 8%    { transform: translateY(-110%); opacity: 0; }
  30%       { opacity: 1; }
  80%, 100% { transform: translateY(240%); opacity: 0; }
}

.hint[data-hide] { opacity: 0; }
.hint[data-hide] span::after { animation-play-state: paused; }
html[data-loading] .hint { opacity: 0; }

/* Asked for less motion: no bead. The line stays, so the cue survives. */
@media (prefers-reduced-motion: reduce) {
  .hint span::after { animation: none; opacity: 0; }
}

/* On a phone there are only two lanes at the bottom, not three: one button is
   already most of the width, so the hint cannot stay beside it. It goes ABOVE
   the dock instead of beside it, clearing the button's own height and the
   dock's offset from the floor. Same fix as the chip, one level down. */
@media (max-width: 700px) {
  .hint { bottom: calc(clamp(1.4rem, 4.5vh, 2.6rem) + 4.4rem); }
}

/* -------------------------------------------------------------- loader
   The loader is part of the show: the first frame of the film is already
   up behind it, the count is the only moving thing, and it lets go early. */

.loader {
  position: fixed; inset: 0; z-index: 80;
  display: grid; place-content: center; justify-items: center; gap: .4rem;
  background: rgba(10,8,6,.55); backdrop-filter: blur(3px);
  opacity: 1; transition: opacity .7s ease, visibility 0s linear .7s;
}
.loader[data-done] { opacity: 0; visibility: hidden; pointer-events: none; }
/* `display: grid` above beats the UA rule for [hidden], so a loader hidden in
   JS stayed on top of the page and swallowed every click on it. */
.loader[hidden] { display: none !important; }
.loader__n {
  font-family: var(--serif); font-weight: 300;
  font-size: clamp(3.5rem, 10vw, 6.5rem); line-height: 1; color: var(--bone);
  font-variant-numeric: tabular-nums;
}
.loader__n i { font-style: normal; font-size: .35em; color: var(--bone-soft); margin-left: .1em; }
.loader__label {
  font-family: var(--sans); font-size: .72rem; letter-spacing: .34em;
  text-transform: uppercase; color: var(--gold-hi);
}

/* The ecosystem page is arrived at out of the homepage's rise into cloud, so
   its loading screen IS that cloud: it thins at 100 into chapter one's fog and
   the page load hides inside the transition. */
.loader--cloud {
  background: radial-gradient(122% 106% at 50% 42%, #fdf8ee 0%, #f6ead2 48%, #e9dcc0 100%);
  backdrop-filter: none;
}
.loader--cloud .loader__n { color: #2a2115; }
.loader--cloud .loader__n i { color: #7d6a45; }
.loader--cloud .loader__label { color: #7a5c1c; }

/* --------------------------------------------------------- the wheel
   One ring, three brass arcs, three names, per the concept draft. */

.wheel {
  position: relative; width: clamp(7.5rem, 12vw, 9.5rem); aspect-ratio: 1;
  margin: 1.7rem 0 .5rem clamp(3.5rem, 7vw, 6rem); justify-self: start;
}
.wheel svg { width: 100%; height: 100%; }
.wheel svg g { stroke: rgba(203,166,78,.85); }
.wheel__tips path { stroke: rgba(203,166,78,.85); stroke-width: 1.5; stroke-linecap: round; }
.wheel__l {
  position: absolute; font-family: var(--serif); font-size: clamp(.85rem, .8rem + .3vw, 1.05rem);
  color: var(--bone); text-shadow: 0 1px 10px rgba(8,6,4,.9);
}
/* placed at the three gaps in the ring: 0deg, 240deg and 120deg clockwise from
   twelve, on the same circle the arcs are drawn on, so each name sits in the
   break its arc leaves rather than near it */
.wheel__l--a { left: 50%;   top: .8%;  translate: -50% -108%; }
.wheel__l--b { left: 7.4%;  top: 74.6%; translate: -104% -50%; }
.wheel__l--c { left: 92.6%; top: 74.6%; translate: 4% -50%; }

/* ------------------------------------------------- the return panel */

.copy--panel { width: min(72rem, calc(100vw - var(--edge-l) - var(--gut))); }
.copy--panel::before { inset: -2.2rem -6rem -8rem; }
.panel {
  display: grid; gap: clamp(1.2rem, 3vw, 2.4rem); width: 100%;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, .9fr);
  grid-template-areas: "say do" "meta meta";
  align-items: end; text-align: left;
}
.panel__say  { grid-area: say; display: grid; gap: .7rem; }
.panel__do   { grid-area: do; }
.panel__meta { grid-area: meta; display: grid; gap: 1.2rem;
  border-top: 1px solid rgba(246,238,225,.2); padding-top: 1.1rem;
  grid-template-columns: auto 1fr; align-items: center; }
.count--film { justify-items: start; }
.reg--compact {
  background: rgba(16,13,9,.55);
  border: 1px solid rgba(246,238,225,.18); border-radius: .7rem;
  padding: clamp(1.1rem, 2.4vw, 1.6rem);
}
/* The form's LAYOUT moved to theme.css on 30 August. It was here, and only
   the journeys load this file, so every form on a paper page had no grid at
   all. What is left here is the one thing that is genuinely film. */
.reg__note { color: var(--bone-soft); }

/* ---- the panel's form only where it can actually be reached -------------
   .copy is  position:absolute; bottom:var(--edge-b)  inside a PINNED stage,
   so it grows UPWARD and anything taller than the band between the bar and
   the dock spills off the TOP of the screen with no scroll that reaches it.

   Measured on the built page: the band is 100vh - 160px. The registration
   form is 649px tall in two columns and 715px stacked, and the panel around
   it 819px / 1109px. So the panel needs a ~980px viewport in two columns and
   ~1270px stacked. It fits almost nowhere: on a 1440x810 laptop the first
   name field sat 35px ABOVE the top of the screen, and on a 375x812 phone all
   three fields did, leaving a consent box and a submit button for a form that
   could not be filled in.

   Trimming cannot fix it, because the form alone is taller than the band. So
   the form is shown only where it provably fits, and everywhere else the
   panel offers the same thing as a link to /summit/registration/ — a paper
   page in normal flow, where the identical form simply scrolls. The dock's
   gold button already goes to the same place. */
.panel__do > form.reg { display: none; }
.panel__cta { display: grid; gap: .8rem; justify-items: start; }
@media (min-width: 62rem) and (min-height: 66rem) {
  .panel__do > form.reg { display: grid; }
  .panel__cta { display: none; }
}
/* On a short viewport even the CTA panel does not fit, because the countdown
   and the key dates sit under it and are 241px between them. Measured: at
   320x568 the panel was 677px against a 424px band. The dates are the part a
   visitor can lose without losing the way in — the dock carries the date, and
   /summit/ carries both — so they go first, and the button stays reachable. */
@media (max-height: 46rem) {
  .panel__meta { display: none; }
}
/* Shorter still — a small phone WITH its browser toolbars showing, which is the
   real case and about 20% shorter than the nominal device height a headless
   check reports. At 320x454 the panel was still 333px against a 313px band
   with the dates already gone, so the standfirst goes too. The tagline and the
   way in survive, and the dock still carries the date and the place. */
@media (max-height: 32rem) {
  .copy--panel .copy__sub { display: none; }
}


/* ------------------------------------------- the invitation to climb */

/* Centred in the viewport, but set LEFT inside its own column. Centred text
   under a centred cloud gave the chapter no edge to read down; ranging it left
   puts the eyebrow, the headline, the line under it and the card on one axis,
   which is what makes it read as an ending with somewhere to go rather than a
   poster. The measure is narrower than the old centred one so the headline
   still breaks into three lines. */
.copy--climb {
  /* Centred. It sat at 53% while the cover was a sprawling ellipse that had to
     be shoved off the chapter rail; the cover is a sized rectangle now, so it
     can be where it belongs and still clear the rail by about 55px. */
  left: 50%; right: auto; bottom: auto; top: 50%;
  /* --ink-dx/dy are set by sizeClimb() in stage.js: they put the block's words
     on the centre line rather than the grid column they are laid out in. */
  translate: calc(-50% + var(--ink-dx, 0px)) calc(-50% + var(--ink-dy, 0px));
  transform: none !important;
  width: min(38rem, 92vw); text-align: left; justify-items: start;
  gap: .9rem;
}
.copy--climb .display { margin-block: .1rem .3rem; }

/* the small bronze line that names what the chapter is about to do */
.climb__eyebrow {
  font-family: var(--serif); font-size: clamp(1.05rem, .95rem + .4vw, 1.3rem);
  /* Body-sized, so it is held to 4.5:1 and not the 3:1 the headline gets, and
     it is the line set furthest out on the thinnest part of the cloud. #7d5f1f
     measured 4.26 and #6b5115 measured 4.38, both short. A deeper bronze is the
     cheapest fix: making it large enough to qualify for 3:1 would put it out of
     proportion with the headline under it. */
  color: #624a11; letter-spacing: .004em; margin: 0;
}
.climb__lede {
  font-family: var(--sans); font-size: clamp(.95rem, .9rem + .2vw, 1.06rem);
  line-height: 1.62; color: #4a4235; max-width: 31rem; margin: 0 0 .35rem;
}
/* The plate moved out to .climbplate, a sibling that lands ahead of the words.
   This one is now only the fallback for a page whose script never ran. */
.copy--climb::before { background: none; }

/* Sized to the block it covers, not to the headline alone. The chapter grew an
   eyebrow, a line of body copy and a card under the actions, which took it from
   22rem to 38rem tall, and a 22rem plate left the card hanging off the bottom of
   the cloud onto open film. Wider than it strictly needs to be as well, so the
   mist drifts out over the rooftops on both sides rather than stopping in mid
   air. Every floor geometry below is a percentage of this box, so both scale
   with it. */
.climbplate {
  position: absolute; z-index: 1; pointer-events: none;
  left: 50%; top: 50%; translate: -50% -50%;
  /* A fallback only. stage.js overwrites both from the block's own measured
     box on every layout; see sizeClimb() there for why. */
  width: min(50rem, 94vw); height: min(44rem, 86vh);
}
.climbplate::before {
  content: ""; position: absolute; pointer-events: none;
  /* THE COVER, and the one thing here that is not drawn.

     A block of words is a rectangle, and the drawn layers are ellipses and
     noise: their falloff always cuts the corners, which on this block is where
     the eyebrow begins and where the card ends. Four rounds of pushing those
     layers outward each moved the thin part somewhere else rather than getting
     rid of it. This is a plain rounded rectangle, sized in pixels from the
     block itself by sizeClimb() in stage.js, blurred to a soft edge. It is
     predictable, it is measurable, and _pw/cover.mjs measures it.

     Everything drawn sits ON TOP of it and is now texture rather than cover. */
  left: 50%; top: 50%; translate: -50% -50%;
  width: var(--cover-w, 44rem); height: var(--cover-h, 30rem);
  /* A blurred rectangle still reads as a rectangle: at 40px of blur its sides
     were straight enough to draw a visible line down the film. A percentage
     radius bows every side, a wider blur softens the whole boundary, and the
     box-shadow adds a second, much fainter falloff outside the first so there
     is no single edge for the eye to find. The shadow is inside the same
     filter, so it is blurred again on top of its own spread. */
  border-radius: 26%;
  background: rgba(251,246,236,.95);
  filter: blur(46px);
  box-shadow: 0 0 130px 70px rgba(251,246,236,.5);
}

.copy--climb .display { color: #221b12; }
.copy--climb .display .em { color: #7a5a14; }
.copy--climb .btn--quiet { color: #382807; }
.copy--climb .btn--quiet:hover { color: #241a03; }

/* --------------------------------------------- the cloud, and the invitation

   Everything below is the last chapter's one job: the film has stopped, the
   words have landed, and the page has to feel like an open door rather than a
   full stop. Three moves, in order of how loud they are.

   1 · The plate breathes and the cloud drifts. The puffs are four ellipses of
       light inside .climbplate, each on its own period between 32 and 54
       seconds, none of them a multiple of another, so the pattern never
       visibly repeats. That is far slower than anything else on the page,
       which is the point: it reads as air, not as an animation asking to be
       watched.

   2 · The headline arrives in two beats, condensing out of the blur rather
       than sliding. The stage sets data-live on a block when it is genuinely
       being read, so the beats fire on arrival at the chapter and not on page
       load, and they reset if the reader scrolls back and returns.

   3 · The gold button beckons: one soft ring of light swelling out of it every
       few seconds, a slow sheen across the face, and an arrow that nudges on
       the same beat. All three stop on hover, where the pointer takes over.

   Every one of them is switched off under prefers-reduced-motion at the foot
   of this file. */

.puff {
  position: absolute; display: block; pointer-events: none;
  background-repeat: no-repeat; background-position: center; background-size: 100% 100%;
  will-change: transform;
}

/* The floor and five layers of drawn cloud, printed by build/clouds.mjs, which
   carries the reasoning: the noise lives in the ALPHA channel and the fill
   stays flat, so the take's sunset burns through wherever the cloud is thin.
   That is where a cloud's colour actually comes from, and it is why three
   earlier rounds that painted light and shade onto white came back looking
   like paper.

   .puff--floor and .puff--floor2 are the contrast floor and are not free like
   the rest: together they never fall below .86 anywhere a word is set, and
   their animations only ever swell them, never shrink them. Everything after
   them is decoration, which can only ADD opacity. That is what makes it safe
   for the decoration to drift: no thin spot of it can wander over a word and
   take the floor with it.

   To re-derive after changing a seed or a frequency:  node build/clouds.mjs
   and paste the six rules below. Do not hand-edit the data URIs. */
.puff--floor { inset: -34% -12%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.003 .007' numOctaves='4' seed='7' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='70' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0018 .0048' numOctaves='5' seed='13' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.7 0 0 0 0.05'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='0.700' intercept='0.3'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0016 .004' numOctaves='3' seed='3' result='sn'/%3E %3CfeColorMatrix in='sn' type='matrix' result='sg' values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E %3CfeComponentTransfer in='sg' result='litc'%3E %3CfeFuncR type='linear' slope='0.336' intercept='0.762'/%3E %3CfeFuncG type='linear' slope='0.336' intercept='0.762'/%3E %3CfeFuncB type='linear' slope='0.336' intercept='0.762'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='shaped' in2='litc' operator='arithmetic' k1='1' k2='0' k3='0' k4='0' result='shaped'/%3E %3CfeGaussianBlur in='shaped' stdDeviation='20'/%3E %3C/filter%3E %3C/defs%3E %3Crect x='50' y='45' width='800' height='470' rx='96' fill='%23fffefb' filter='url(%23f)'/%3E %3C/svg%3E"); }
.puff--floor2 { inset: -30% -12%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.003 .007' numOctaves='4' seed='23' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='70' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0026 .0068' numOctaves='5' seed='41' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.6 0 0 0 0.1'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='0.740' intercept='0.26'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeGaussianBlur in='shaped' stdDeviation='20'/%3E %3C/filter%3E %3C/defs%3E %3Crect x='45' y='40' width='810' height='480' rx='104' fill='%23fffefb' filter='url(%23f)'/%3E %3C/svg%3E"); }
.puff--a { inset: -30% -24%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.003 .007' numOctaves='4' seed='11' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='190' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0018 .0048' numOctaves='5' seed='19' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.5 0 0 0 0'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='1.000' intercept='0'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeGaussianBlur in='shaped' stdDeviation='5'/%3E %3C/filter%3E %3C/defs%3E %3Cellipse cx='450' cy='280' rx='300' ry='165' fill='url(%23g)' filter='url(%23f)'/%3E %3C/svg%3E"); }
.puff--b { inset: -54% -34% 0 -6%; opacity: 0.92;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.003 .007' numOctaves='4' seed='29' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='190' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0018 .0048' numOctaves='5' seed='5' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.5 0 0 0 0'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='1.000' intercept='0'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeGaussianBlur in='shaped' stdDeviation='5'/%3E %3C/filter%3E %3C/defs%3E %3Cellipse cx='450' cy='280' rx='285' ry='132' fill='url(%23g)' filter='url(%23f)'/%3E %3C/svg%3E"); }
.puff--c { inset: 0 -10% -54% -14%; opacity: 0.9;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.003 .007' numOctaves='4' seed='47' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='190' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0018 .0048' numOctaves='5' seed='33' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.5 0 0 0 0'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='1.000' intercept='0'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeGaussianBlur in='shaped' stdDeviation='5'/%3E %3C/filter%3E %3C/defs%3E %3Cellipse cx='450' cy='280' rx='300' ry='142' fill='url(%23g)' filter='url(%23f)'/%3E %3C/svg%3E"); }
.puff--d { inset: -38% -16%; opacity: 0.55;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.005 .011' numOctaves='4' seed='61' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='130' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.005 .012' numOctaves='5' seed='71' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.7 0 0 0 -0.1'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='1.000' intercept='0'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeGaussianBlur in='shaped' stdDeviation='3'/%3E %3C/filter%3E %3C/defs%3E %3Cellipse cx='450' cy='280' rx='300' ry='150' fill='url(%23g)' filter='url(%23f)'/%3E %3C/svg%3E"); }
.puff--e { inset: -18% -44% -26% 12%; opacity: 0.48;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 900 560'%3E %3Cdefs%3E %3CradialGradient id='g'%3E %3Cstop offset='0.28' stop-color='%23fffefb'/%3E %3Cstop offset='1' stop-color='%23fffefb' stop-opacity='0'/%3E %3C/radialGradient%3E %3Cfilter id='f' x='-40%' y='-55%' width='180%' height='210%' color-interpolation-filters='sRGB'%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.006 .012' numOctaves='4' seed='83' result='c'/%3E %3CfeDisplacementMap in='SourceGraphic' in2='c' scale='120' xChannelSelector='R' yChannelSelector='G' result='sil'/%3E %3CfeTurbulence type='fractalNoise' baseFrequency='.0045 .011' numOctaves='5' seed='97' result='m'/%3E %3CfeColorMatrix in='m' type='matrix' result='mn' values='0 0 0 0 1 0 0 0 0 .996 0 0 0 0 .984 1.8 0 0 0 -0.12'/%3E %3CfeComponentTransfer in='mn' result='ma'%3E %3CfeFuncA type='linear' slope='1.000' intercept='0'/%3E %3C/feComponentTransfer%3E %3CfeComposite in='ma' in2='sil' operator='in' result='body'/%3E %3CfeComponentTransfer in='body' result='shaped'%3E %3CfeFuncA type='gamma' amplitude='1.15' exponent='0.9' offset='0'/%3E %3C/feComponentTransfer%3E %3CfeGaussianBlur in='shaped' stdDeviation='3'/%3E %3C/filter%3E %3C/defs%3E %3Cellipse cx='450' cy='280' rx='250' ry='130' fill='url(%23g)' filter='url(%23f)'/%3E %3C/svg%3E"); }

/* The layers sit ON the rim and across the body at five periods between 32 and
   54 seconds, none a multiple of another, so the silhouette never visibly
   repeats. This is far slower than anything else on the page, which is the
   point: it should read as air, not as an animation asking to be watched. */
.puff--floor  { animation: puff-floor 50s ease-in-out infinite; }
.puff--floor2 { animation: puff-floor 61s ease-in-out infinite reverse; }
.puff--a { animation: puff-a 38s ease-in-out infinite; }
.puff--b { animation: puff-b 46s ease-in-out infinite; }
.puff--c { animation: puff-c 32s ease-in-out infinite; }
.puff--d { animation: puff-d 54s ease-in-out infinite; }
.puff--e { animation: puff-e 43s ease-in-out infinite; }

/* never below 1: a floor that shrank could uncover a word it was covering */
@keyframes puff-floor { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.035); } }
@keyframes puff-a { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }    50% { transform: translate3d(4%, -2%, 0) scale(1.07); } }
@keyframes puff-b { 0%, 100% { transform: translate3d(0, 0, 0) scale(1.05); } 50% { transform: translate3d(-5%, 3%, 0) scale(1); } }
@keyframes puff-c { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }    50% { transform: translate3d(-3%, 4%, 0) scale(1.06); } }
@keyframes puff-d { 0%, 100% { transform: translate3d(0, 0, 0) scale(1.04); } 50% { transform: translate3d(6%, -3%, 0) scale(.96); } }
@keyframes puff-e { 0%, 100% { transform: translate3d(0, 0, 0) scale(1); }    50% { transform: translate3d(-7%, -2%, 0) scale(1.08); } }

/* the cream itself swells very slightly, which is what ties the four drifting
   puffs into one body of cloud rather than four separate lights */
.climbplate::before { animation: plate-breathe 26s ease-in-out infinite; will-change: transform; }
@keyframes plate-breathe { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.04); } }

/* None of this runs while the chapter is off screen. The stage marks the block
   live the moment it is genuinely being read, so the cloud, the ring and the
   sheen all start together on arrival instead of turning over unseen for the
   whole of the rest of the page. Paused at 0% is also their rest pose, so a
   page whose script never ran gets the composition rather than nothing. */
.puff, .climbplate::before,
.btn--beckon::before, .btn--beckon::after, .btn__arrow { animation-play-state: paused; }
.climbplate[data-live] .puff,
.climbplate[data-live]::before,
.copy--climb[data-live] .btn--beckon::before,
.copy--climb[data-live] .btn--beckon::after,
.copy--climb[data-live] .btn__arrow { animation-play-state: running; }

/* Beats are inline, so only opacity and filter are animated: transform does
   nothing on a non-replaced inline box, and inline-block would refuse to wrap
   mid sentence, which this headline has to do at every width below 60rem. */
.copy--climb[data-live] .beat--1 { animation: beat-in 1.05s cubic-bezier(.22,.68,.3,1) both; }
.copy--climb[data-live] .beat--2 { animation: beat-in 1.15s cubic-bezier(.22,.68,.3,1) .38s both,
                                              gold-warm 9s ease-in-out 2.2s infinite; }
@keyframes beat-in { from { opacity: 0; filter: blur(9px); } to { opacity: 1; filter: blur(0); } }
@keyframes gold-warm {
  0%, 100% { text-shadow: 0 0 0 rgba(201,164,76,0); }
  50%      { text-shadow: 0 0 22px rgba(201,164,76,.28); }
}

.copy__acts--climb { justify-content: flex-start; }
/* Square-shouldered here, and only here. A pill reads as one more control in a
   row of controls; this is the page's last word and wants to sit like a plate.
   The rest of the site's buttons are untouched. */
.copy--climb .btn--gold {
  border-radius: .28rem;
  background: linear-gradient(180deg, #e0b75d 0%, #cfa244 46%, #b7862f 100%);
  box-shadow: 0 1px 0 rgba(255,255,255,.42) inset, 0 10px 26px rgba(80,53,10,.26);
}
.copy--climb .btn--gold::before { border-radius: .55rem; }

.btn--beckon { position: relative; isolation: isolate; }
.btn--beckon .btn__label { position: relative; z-index: 2; }
/* the ring of light: the same gesture as the cloud behind it, on a human beat */
.btn--beckon::before {
  content: ""; position: absolute; z-index: 0; inset: -.4rem; border-radius: 999px;
  pointer-events: none;
  background: radial-gradient(closest-side, rgba(216,173,82,.55) 0%, rgba(216,173,82,0) 100%);
  opacity: 0; animation: beckon 4.6s ease-out 1.2s infinite;
}
@keyframes beckon {
  0%        { transform: scale(.9);  opacity: 0; }
  16%       { opacity: .8; }
  62%, 100% { transform: scale(1.55); opacity: 0; }
}
/* the sheen rides over the face, under nothing: the label carries z-index 2 */
.btn--beckon::after {
  content: ""; position: absolute; z-index: 1; inset: 0; border-radius: inherit;
  pointer-events: none;
  background-image: linear-gradient(102deg, rgba(255,248,230,0) 38%, rgba(255,250,236,.55) 50%, rgba(255,248,230,0) 62%);
  background-size: 240% 100%; background-repeat: no-repeat; background-position: 140% 0;
  animation: sheen 6.2s ease-in-out 2s infinite;
}
@keyframes sheen { 0% { background-position: 140% 0; } 26%, 100% { background-position: -70% 0; } }

.btn__arrow { position: relative; z-index: 2; font-style: normal; display: inline-block;
  animation: nudge 4.6s ease-in-out 1.2s infinite; }
.btn__arrow::before { content: "\2192"; }
@keyframes nudge { 0%, 66%, 100% { transform: translateX(0); } 78% { transform: translateX(.24em); } }

/* on hover the pointer is the motion, so the page stops volunteering its own */
.btn--beckon:hover {
  transform: translateY(-2px);
  box-shadow: 0 1px 0 rgba(255,255,255,.34) inset, 0 12px 30px rgba(80,53,10,.32);
}
.btn--beckon:hover::before { animation-duration: 2.4s; }
.btn--beckon:hover::after,
.btn--beckon:focus-visible::after { animation: none; }
.btn--beckon:hover .btn__arrow,
.btn--beckon:focus-visible .btn__arrow {
  animation: none; transform: translateX(.24em); transition: transform .2s ease;
}
.btn--beckon:active { transform: translateY(0); }


/* ------------------------------------------------------ the next stop

   A second, quieter door to the same place as the gold button. It is not a
   duplicate for the sake of it: the button is a decision and this is a preview,
   and between them they answer the question the button leaves open, which is
   what is actually on the other side. Its picture is the destination's own
   opening frame, so it can never drift out of date against the page it points
   at, and its ground is the cloud it sits on rather than a card floating over
   it. */

.nextstop {
  display: grid; grid-template-columns: auto 1fr auto; align-items: center;
  gap: clamp(.8rem, 2vw, 1.1rem);
  width: 100%; max-width: 27rem; padding: .7rem .9rem .7rem .7rem;
  text-decoration: none; color: inherit;
  border: 1px solid rgba(138,106,36,.26); border-radius: .5rem;
  background: linear-gradient(180deg, rgba(255,253,247,.88), rgba(252,246,233,.76));
  box-shadow: 0 10px 26px rgba(84,60,20,.1);
  transition: transform .2s ease, border-color .2s ease, box-shadow .2s ease, background .2s ease;
}
.nextstop:hover {
  transform: translateY(-2px); border-color: rgba(138,106,36,.5);
  background: linear-gradient(180deg, rgba(255,254,250,.92), rgba(253,248,238,.78));
  box-shadow: 0 16px 34px rgba(84,60,20,.16);
}
.nextstop:focus-visible { outline: 2px solid var(--gold-deep); outline-offset: 3px; }
.nextstop__shot {
  display: block; width: 7rem; height: 4.75rem; object-fit: cover;
  border-radius: .34rem; background: #2a2119;
}
.nextstop__say { display: grid; gap: .12rem; min-width: 0; }
.nextstop__k {
  font-family: var(--sans); font-size: .68rem; font-weight: 600;
  /* same rule as the eyebrow: small type, so 4.5:1, so not #8a6a24 */
  letter-spacing: .17em; text-transform: uppercase; color: #6b5115;
}
.nextstop__h { font-family: var(--serif); font-size: 1.28rem; color: #221b12; line-height: 1.15; }
.nextstop__p {
  /* the smallest thing set anywhere on this page, so it is held to 4.5:1 over
     the card's own translucent ground; #5a5245 measured 4.19 */
  font-family: var(--sans); font-size: .8rem; line-height: 1.42; color: #453e34;
}
/* the arrow is the card's own, and it steps across on hover with the card */
.nextstop__go {
  width: 1.15rem; height: 1.15rem; flex: 0 0 auto; font-style: normal;
  color: #7d5f1f; transition: transform .2s ease;
}
.nextstop__go::before { content: "\2192"; font-family: var(--sans); font-size: 1.05rem; }
.nextstop:hover .nextstop__go { transform: translateX(.22rem); }

/* --------------------------------------------------- the invitation */

.copy--cards {
  --head: 4.9rem;                      /* the fixed bar it must sit clear of */
  left: 50%; right: auto; bottom: auto;
  top: calc(50% + var(--head) / 2);
  translate: -50% -50%; transform: none !important;
  /* 44rem is the panel's natural height; below that much room it scales to fit
     rather than sliding up under the navigation */
  scale: clamp(.66, calc((100vh - var(--head) - 1.6rem) / 44rem), 1);
  width: min(62rem, 94vw); text-align: center; justify-items: center; gap: .9rem;
}
/* no cream wash here: the cards are the light objects and the defocused
   valley behind them is the point of the chapter */
.copy--cards::before {
  inset: -2.4rem -3rem -2.4rem; border-radius: 3rem;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(8,6,4,.24) 0%, rgba(8,6,4,.18) 56%, rgba(8,6,4,.07) 80%, rgba(8,6,4,0) 100%);
  filter: blur(34px);
}
.alone {
  position: relative; font-family: var(--serif); font-style: italic;
  font-size: clamp(1.05rem, .95rem + .45vw, 1.35rem); color: var(--bone);
  text-shadow: 0 1px 3px rgba(8,6,4,.95), 0 0 18px rgba(8,6,4,.85);
}
.alone::before {
  content: ""; position: absolute; z-index: -1; inset: -.6rem -1.8rem;
  border-radius: 2rem; background: rgba(8,6,4,.7); filter: blur(15px);
}
.cards { display: grid; gap: .7rem; width: 100%; grid-template-columns: repeat(2, minmax(0, 1fr)); }
/* The cards now sit over a dark valley rather than a cream wash, so while a
   card is still fading in, its accents composite toward that valley. A deeper
   bronze keeps every link and focus ring above AA through the whole fade. */
.card {
  --gold-deep: #6a4f16;
  background: linear-gradient(180deg, #f8f3ea, #f0e9dc);
  color: var(--ink); border: 1px solid rgba(138,106,36,.24); border-radius: .5rem;
  padding: clamp(1rem, 2vw, 1.5rem);
  display: grid; gap: .45rem; justify-items: center; text-align: center;
  box-shadow: 0 18px 44px rgba(8,6,4,.28);
}
.card--lead { grid-column: 1 / -1; gap: .3rem; padding-block: clamp(1.1rem, 2.2vw, 1.6rem); }
.card--lead .btn { margin-top: .7rem; }
.card--wide { grid-column: 1 / -1; grid-template-columns: auto 1fr; justify-items: stretch;
  text-align: left; align-items: center; gap: clamp(1rem, 3vw, 2.2rem); }
.card__meta { font-size: .78rem; letter-spacing: .13em; text-transform: uppercase; color: var(--ink-soft); font-weight: 600; }
.card .mark { width: 2.6rem; height: 2.2rem; fill: none; stroke: var(--gold-deep); stroke-width: 1.3; stroke-linecap: round; stroke-linejoin: round; }
.card__go { font-size: .82rem; letter-spacing: .12em; text-transform: uppercase; }
.card label, .card .count__label, .card .count__u, .card .count__when, .card .card__meta { color: var(--ink-soft); }
.card .count__dials li { border-left-color: var(--line); }
.card .count__n { color: var(--ink); }
.card input, .card textarea { background: #fffdf9; border-color: var(--line); color: var(--ink); backdrop-filter: none; }
.card input::placeholder { color: #8a8072; }
.card .field__msg[data-ok] { color: var(--gold-deep); }
.card :focus-visible { outline-color: var(--gold-deep); }
.card .card__go, .card .go { color: var(--gold-deep); border-bottom-color: rgba(106,79,22,.5); }
.stay { display: grid; gap: .2rem; }
.copy__acts--quiet { justify-content: center; position: relative; }
.copy--cards .copy__acts--quiet::before {
  content: ""; position: absolute; z-index: -1; inset: -.55rem -1.7rem;
  border-radius: 2rem; background: rgba(8,6,4,.7); filter: blur(15px);
}
.copy--cards .btn--quiet {
  position: relative; color: var(--bone); text-shadow: 0 1px 10px rgba(8,6,4,.9);
}

/* ----------------------------------------------------- the cloudline */

.cloudline {
  position: fixed; inset: 0; z-index: 90; pointer-events: none;
  background: radial-gradient(120% 104% at 50% 44%, #fdf7ea 0%, #f5e9d2 52%, #ecdcbc 100%);
  opacity: 0; visibility: hidden;
  transition: opacity .3s linear, visibility 0s linear .3s;
}
.cloudline[data-on],
html[data-arriving] .cloudline { opacity: 1; visibility: visible; transition: opacity .26s linear, visibility 0s linear 0s; }
/* leaving, the cloud closes IN over the page; arriving, it is simply already
   there and fades off, so only the outgoing state carries the bloom */
.cloudline[data-on] { animation: cloud-close .34s ease-out both; }
html[data-arriving] .cloudline { animation: none; }
@keyframes cloud-close { from { transform: scale(1.22); } to { transform: scale(1); } }

.copylayer { transition: opacity .34s ease; }
.copylayer[data-wrapping] { opacity: 0; transition: none; }

/* ------------------------------------------------------------ narrower */

@media (max-width: 900px) {
  .film { --edge-b: clamp(7.5rem, 18vh, 11rem); }
  /* wrapped chips need a gap wider than the plates they carry, or two names
     that never touch as boxes still read as one run */
  .labels ul { display: flex; flex-wrap: wrap; justify-content: center; gap: .55rem 2.4rem; }
  .labels li { transform: none !important; opacity: 1 !important; }
  .labels li::after, .labels li::before { display: none; }
  /* a narrow window has no canal left to run a chain down: the markers go back
     to the wrapped chips every other row uses at this width */
  .labels--pins { inset: auto; top: clamp(5.5rem, 13vh, 9rem);
    left: var(--label-inset); right: var(--label-inset); }
  .labels--pins::before { content: ""; }
  .labels--pins ul { position: static; }
  .labels--pins li { position: relative; left: auto; top: auto;
    translate: none; scale: none; }
  .pin { display: none; }
  /* the row wraps to chips at this width, so its names no longer sit anywhere a
     trail could leave from, and cover-fit has cropped most of the mountain away
     besides. The drawing goes; the names stay. */
  .trailslayer { display: none; }

  .note { right: var(--gut); top: 6.2rem; max-width: 12rem; }
  .panel { grid-template-columns: 1fr; grid-template-areas: "say" "do" "meta"; }
  .panel__meta { grid-template-columns: 1fr; }
  .copy--cards { --head: 4.5rem; width: min(62rem, 96vw); gap: .55rem;
    scale: clamp(.58, calc((100vh - var(--head) - 2.4rem) / 52rem), 1); }
  .copy--cards::before { inset: -1.2rem -1.4rem -1.2rem; }
  .cards { grid-template-columns: 1fr; gap: .5rem; }
  .card { padding: .8rem .9rem; gap: .3rem; }
  .card--lead .btn { margin-top: .5rem; }
  .card--wide { grid-template-columns: 1fr; }
  .card--wide .count__dials li { padding-inline: .5rem; }
  .wheel { display: none; }
  .dock .btn--ghost { display: none; }
  /* The rail is not on screen at this width, so the reason the cloud leans
     right on the desktop is gone with it: centre the whole composition and let
     the mist spread both ways again, or the top of the block stands off the
     cloud on open sky. */
  .copy--climb, .climbplate { left: 50%; }
  /* Nearly the full screen. The block is 31rem tall here against a 44rem plate
     on the desktop, but the plate is capped by the viewport, and at 86vh the
     eyebrow at the top of the block was standing on the cloud's own falloff. */
  .climbplate { width: 98vw; height: 98vh; }
  /* Wider insets, not narrower. These are percentages of the plate, and the
     plate is a third of its desktop width here, so the same percentage buys a
     third of the cover: at -14% the floor's plateau reached 178px against a
     180px half-measure of text and the first two lines stood on the falloff. */
  /* No inset table here any more. It existed because the plate was a fixed
     box, so the same percentage bought a third of the cover at 390px that it
     bought at 1440px. The plate is now a ratio of the block itself, so the
     geometry is the same at every width and the desktop insets are correct
     here too. */
  /* The bronze is a desktop luxury. A phone meets this chapter over the duller
     part of the take, where the cloud's own floor is all that holds the line
     up, and the eyebrow graded 3.25:1 there against the 4.5 small type needs.
     Deeper here only; the desktop keeps #624a11. */
  .climb__eyebrow { color: #46340a; }
  .copy__acts--climb { flex-direction: column; align-items: flex-start; gap: .6rem; }
  .copy--climb { width: min(38rem, 92vw); gap: .75rem; }
  .climb__lede { max-width: none; }
  .nextstop { max-width: none; padding: .6rem .7rem .6rem .6rem; }
  .nextstop__shot { width: 5.4rem; height: 3.9rem; }
  .nextstop__h { font-size: 1.12rem; }
  .nextstop__p { font-size: .76rem; }
}

@media (prefers-reduced-motion: reduce) {
  .labels li { opacity: 1; }
  .pin::after { animation: none; }
  .cloudline, .copylayer, .loader { transition: none; }
  .rail__name { transition: none; }
  /* the whole last chapter goes still: cloud, beats, ring, sheen and arrow */
  .puff, .climbplate::before, .cloudline[data-on],
  .copy--climb[data-live] .beat--1, .copy--climb[data-live] .beat--2,
  .btn--beckon::before, .btn--beckon::after, .btn__arrow { animation: none !important; }
  .copy--climb .beat { opacity: 1; filter: none; }
  .btn--beckon::before, .btn--beckon::after { opacity: 0; }
  .nextstop, .nextstop__go { transition: none; }
}

/* ------------------------------------------------------- the footer

   The journey pages end in a real footer, so the loop now turns past one
   rather than straight back into itself. Three things follow from that.

   The stage is `position: fixed; z-index: 0`, so the footer has to be given
   a stacking context of its own or the film paints straight over it. It also
   has to carry its own opaque ground: `main.film` is `pointer-events: none`,
   but the canvas behind it is not transparent.

   And the floating chrome, the dock, the ring and the scroll hint, is fixed
   at z-index 30. It would otherwise sit on top of the footer's own links for
   the whole length of it. `data-atfoot` on the root takes it out of the way
   as soon as the footer breaks the bottom of the viewport.

   The chapter rail was left out of that hand-off and sits OVER the footer at
   z-index 40, so the chapter names printed through the footer's brand block.
   It leaves with the rest of the floating chrome.

   The copy layer was wrong at the foot too, in the opposite direction: it sits
   UNDER the footer at z-index 2, so the last chapter's headline was cut in half
   along the footer's own top edge. That one is NOT fixed here. It is driven by
   stage.js, on each block's own opacity, for a reason given in full there. */

.ground-film .foot {
  position: relative; z-index: 20;
  background: var(--film);
  border-top: 1px solid rgba(246,238,225,.14);
}

html[data-atfoot] .dock,
html[data-atfoot] .ring,
html[data-atfoot] .hint,
html[data-atfoot] .rail { opacity: 0; pointer-events: none; }

/* The hint already animates its own opacity; the dock has a transition of its
   own. The ring does not, so give it one, and let anyone who asked for less
   motion have the change instantly instead. */
.ring, .rail { transition: opacity .3s ease; }

@media (prefers-reduced-motion: reduce) {
  .dock, .ring, .hint, .rail { transition: none; }
}

/* ────────────────────────────────────────────────────────────── mends
   Light laid over a fault in the take, not a patch drawn on top of it. The
   Bruges journey materialises a colonnaded building out of open sky across
   frames 206 to 232 and changes the hour while it does it. A warm haze
   centred on that spot, rising and falling with the fault, gives the eye
   something to be looking at while the picture rearranges itself behind it.

   It reads as evening mist catching the lamplight because that is what the
   rest of the take already does, which is the whole reason it works: the
   cover is made of the scene's own material.

   Positioned in FRAME space and converted to screen space with --sc-vfill,
   like every other piece of chrome that points at something in the picture.
   Without it the haze sits on the water at 1440x810 and over the roofline at
   1920x900, and a haze that misses is worse than no haze. */

.mend {
  /* The landscape geometry by default; the narrow query below swaps in the
     portrait one. This indirection exists because the two sets arrive as
     INLINE custom properties, and an inline declaration beats any stylesheet
     rule, so a media query could never override `--mx` if the element set it
     directly. It sets `--mx-l` and `--mx-p` instead and the cascade picks. */
  --mx: var(--mx-l); --my: var(--my-l); --mr: var(--mr-l);
  position: fixed; z-index: 1;           /* over the canvas, under every word */
  pointer-events: none; opacity: 0;
  left: calc(var(--mx) * 100%);
  top: calc((.5 + (var(--my) - .5) * var(--sc-vfill, 1)) * 100%);
  width: calc(var(--mr) * 100vw);
  aspect-ratio: 1;
  translate: -50% -50%;
  background: radial-gradient(closest-side,
    rgba(255, 214, 148, .40),
    rgba(255, 198, 122, .22) 42%,
    rgba(214, 160, 96, .10) 68%,
    transparent 100%);
  filter: blur(clamp(14px, 2.4vw, 34px));
  mix-blend-mode: screen;
}

/* Reduced motion keeps the cover, because it is hiding a defect rather than
   performing: the still frames it shows are drawn from the same take. */
@media (prefers-reduced-motion: reduce) { .mend { transition: none; } }

/* ── the programme headline, lifted over a fault in the take ──────────────
   The take materialises a colonnaded building out of open sky across frames
   206 to 232 and changes the hour while it does it. Regeneration was tried and
   every take breaks somewhere, so the fix is to have something real in front of
   it at that moment rather than to repair the picture.

   This block is already on screen for the whole fault, from f203, and its box
   already spans the x the building sits at. It was simply 190px too low. Lifted,
   the founders' own line covers the moment, and it is the line about the five
   programmes standing over the five programme pins, which is where it wants to
   be anyway.

   Anchored from the TOP for this one chapter, because --edge-b is measured off
   the dock and would put the block back where it was on any screen of a
   different height. */
.copy--overfault {
  bottom: auto;
  top: calc((.5 + (.40 - .5) * var(--sc-vfill, 1)) * 100%);
}
/* The scrim has to travel with it: it is anchored to the block, but its shape
   was cut for a block sitting on the near water. Higher up it needs less below
   and more above, or it reads as a smear under the type rather than as depth
   behind it. */
.copy--overfault::before { inset: -3.4rem -7rem -4.4rem -8rem; }

/* ------------------------------------------------ the fault, narrow

   The note that used to sit here said the building was off the side of the
   frame on a phone and there was nothing to cover. That was wrong, and it is
   worth saying why rather than quietly deleting it: the mobile tier is not a
   crop of the wide frame, it is a 3:4 crop of the same 1920x1080 master, and
   the side it keeps is the LEFT one. The left is where the building arrives.
   Checked frame by frame in `assets/p1/w810`: f200 and f206 are open sky over
   a plain sunlit wall, and by f224 there is a gothic facade with pointed
   arches standing in the same place, under a sky that has gone from gold to
   dusk. It is more obvious on a phone than on a desktop, not less.

   So the phone needs a cover too, and it cannot be the headline. Lifting the
   headline works on a wide screen because the block is one or two lines in the
   middle of a wide frame. Here it is three lines and would land across the
   canal, on top of the row of names, with the button under it.

   The names go instead. They are already the right shape for the job: five
   short chips with their own plates, and a scrim that is built to have no hard
   edge. All this does is put them where the fault is rather than at the top of
   the frame with every other row, and swap their scrim from top-anchored to
   one that is densest in the middle, because the block is now in open frame on
   both sides and a gradient that starts at full strength would draw the line
   the scrim exists to avoid.

   Same breakpoint as the headline's, deliberately: exactly one of the two
   covers the fault at any width, and they can never both be doing it. */
@media (max-width: 56rem) {
  .copy--overfault { top: auto; bottom: var(--edge-b); }
  .copy--overfault::before { inset: -2.2rem -7rem -7rem -8rem; }

  /* Where the building stands in the 3:4 crop, measured off the frames rather
     than guessed: its mass runs from about a fifth of the way down to about
     two thirds. The row sits at 38%, which puts the chips across the gothic
     upper storeys, the part that actually changes.

     The scrim still starts ABOVE the viewport and still fades to nothing at
     the bottom, for the reason the base rule gives: an edge that lands on
     screen draws a line across the film. That rule does not stop applying
     because the block moved down. A veil that begins mid-frame reads as a
     grey blob on the sky, which was the first thing tried here and was worse
     than the fault it was hiding. Anchored in vh so it tracks the block, which
     is now positioned as a share of the viewport rather than in rem. */
  .labels--pins { top: 44%; }
  .labels--pins::before {
    content: ""; top: -50vh; height: 82vh;
    background: linear-gradient(180deg,
      rgba(8,6,4,.70) 0%, rgba(8,6,4,.66) 28%, rgba(8,6,4,.62) 48%,
      rgba(8,6,4,.52) 64%, rgba(8,6,4,.26) 82%, rgba(8,6,4,0) 100%);
  }

  /* The frame-locked cover, re-aimed and re-tuned for the portrait crop.

     Same element and the same driver in stage.js, which is the point: it is
     welded to FRAMES 206/222/232, so it peaks exactly as the building arrives
     and is gone either side of it, which no scroll-driven or copy-driven cover
     can promise. What changes is only what it is made of. On the wide frame it
     is a small warm haze on `screen`, reading as light; at phone size that
     same haze is 129px across in a frame where the building is 200px wide, and
     lightening a picture that is on its way to dusk fights the grade besides.

     So here it is a soft dark veil at normal blend, wide enough to hold the
     whole facade, and weak enough on its own to pass unnoticed. It is not
     doing the job by itself: the names above are the cover, and this takes the
     edge off the instant of the change. */
  .mend {
    --mx: var(--mx-p); --my: var(--my-p); --mr: var(--mr-p);
    mix-blend-mode: normal;
    filter: blur(clamp(30px, 11vw, 64px));
    background: radial-gradient(closest-side,
      rgba(8,6,4,.50),
      rgba(8,6,4,.40) 44%,
      rgba(8,6,4,.18) 72%,
      transparent 100%);
  }
}

/* The lifted headline is positioned as a FRACTION of the viewport but its
   height is in rem, so on a short screen it occupies a much larger share and
   its bottom edge comes down into the water the markers now stand in. Measured:
   fine to 900 tall, one marker caught at 810 and 768, two at 620. The chain
   goes down to meet it rather than the type going up, because the type is
   covering a fault in the take and has to stay where the fault is. */
@media (max-height: 52rem) {
  .labels--pins { --far-y: .715; --near-y: .875; }
}
@media (max-height: 44rem) {
  .labels--pins { --far-y: .755; --near-y: .895; }
}

/* ---- two blocks that end just below the fold on extreme viewports -------
   Found by the reachability pass in geom.mjs, not by eye. Both are pinned, so
   "just below the fold" means unreachable, not merely unseen.

   The cards reach their scale floor BEFORE they fit: at 320x568 the clamp is
   already pinned at .58 and "Begin again" still ended 18px past the bottom.
   The floor exists so the type never becomes unreadable, and .5 is still
   legible at this size, where the alternative is a button nobody can press.

   The climb is centred rather than bottom-anchored, so on a short, wide window
   it grows past both edges: at 1440x600 its closing link ended 4px over. Its
   gap is the only part that can give without moving the words off the centre
   line that sizeClimb() puts them on. */
@media (max-height: 40rem) {
  /* NOT bottom-anchored: this block sets bottom:auto and centres itself on
     top:calc(50% + var(--head)/2). So it can only be made to fit by scaling,
     never by moving its bottom edge — giving it a bottom as well as a top
     stretches the box and throws the buttons far below the fold, which is
     what one attempt at this did. The headroom subtracted here is 3.2rem
     rather than the 2.4rem of the width-based rule: that is the 5px the
     block was still over by at 320x568, converted into scale. */
  .copy--cards { scale: clamp(.5, calc((100vh - var(--head, 4.5rem) - 3.2rem) / 52rem), 1); }
}
@media (max-height: 38rem) and (min-width: 62rem) {
  .copy--climb { gap: .6rem; }
}

/* Smallest screen the site claims to support, WITH browser toolbars showing:
   320x454, not the 320x568 a headless check reports. Both of these blocks still
   ended below the fold there — the cards by 39px because their clamp was
   already sitting on its floor, and the climb by 45px. The climb is centred and
   sized by sizeClimb(), so it cannot be moved; it can only be scaled, and
   `scale` composes with the `translate` that rule already uses rather than
   fighting the `transform: none` it needs. */
@media (max-height: 34rem) {
  /* the climb runs out of room first, and on a NARROW screen too, so this one
     cannot sit behind the min-width the wide-window rule above uses */
  .copy--climb { gap: .5rem; scale: .86; }
}
@media (max-height: 30rem) {
  .copy--cards { scale: clamp(.4, calc((100vh - var(--head, 4.5rem) - 3.2rem) / 52rem), 1); }
}
