/* ─────────────────────────────────────────────────────────────────
   Homepage — scroll-driven sequence.

   Act 1 art direction sets the page chrome: off-white, quiet,
   monospace. The grade inside the canvas travels away from this as
   the sequence progresses; the DOM chrome stays out of the way.
   ───────────────────────────────────────────────────────────────── */

:root {
  --paper: #f4f4f2;
  --sky: #b9d6ff;      /* the sky gradient's midtone, so the loader and
                          any frame before the sky builds meet it */
  --ink: #1b1b19;
  --ink-muted: #6f6f6a;
  --ink-faint: #a3a39d;
  --rule: rgba(27, 27, 25, 0.14);
  --accent: #e8dc38;

  --font-ui: 'Instrument Sans', -apple-system, 'Segoe UI', sans-serif;
  --font-display: 'Instrument Serif', Georgia, serif;
  --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;

  --ease-out: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  color-scheme: light;
}

* { box-sizing: border-box; margin: 0; padding: 0; border: 0 solid; }

html { background: var(--sky); -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font-ui);
  color: var(--ink);
  background: var(--sky);
  overscroll-behavior-y: none;
  -webkit-font-smoothing: antialiased;
}

.mono { font-family: var(--font-mono); font-size: 0.72rem; letter-spacing: 0.02em; }

/* ─── skip ────────────────────────────────────────────────────────
   Quiet, but a real control — visible on focus and always the first
   stop for a keyboard user. */
.skip {
  position: fixed;
  top: 1rem;
  left: 1rem;
  z-index: 200;
  padding: 0.6rem 0.9rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 0.72rem;
  text-decoration: none;
  opacity: 0;
  pointer-events: none;
  transform: translateY(-0.4rem);
  transition: opacity 200ms var(--ease-out), transform 300ms var(--ease-soft);
}
.skip:focus-visible { opacity: 1; pointer-events: auto; transform: none; outline: 2px solid var(--ink); outline-offset: 2px; }
/* also surfaces on hover near the top-left, without shouting */
body:hover .skip { opacity: 0.55; pointer-events: auto; transform: none; }
body:hover .skip:hover { opacity: 1; }

/* ─── loader ──────────────────────────────────────────────────── */
/* ─── the hold ────────────────────────────────────────────────────
   There is no loader card any more. The page still waits — for the
   globe's map to be on its material and the cloud plates to be decoded
   — but what it shows while waiting is nothing at all, over the sky
   colour that html and body already carry. So the page opens on plain
   sky and the sequence arrives into it at full strength.

   Fading these five rather than one wrapper because there is no wrapper:
   the backdrop, the in-front cloud plates, the pinned stage, the label
   pills and the scroll cue are all siblings on the body, which is what
   lets the front clouds blend against the page instead of against a
   layer of their own. */
.backdrop,
.backdrop-cloud--front,
.pin,
.pills,
.cue {
  /* Hidden until the page is ready, then straight to full — no
     transition. The entrance is the pull-back, and a fade underneath it
     only makes the move look like the page still arriving. */
  opacity: 0;
}
/* The warm pass. 0.004, not 0 — the same number and the same reason as
   the cabin overlay: a browser skips painting what is fully transparent,
   and the paint is the entire point. At four thousandths it is painted
   and it is not visible, so the sequence can be swept through its whole
   range under the loader and every raster it needs is already done by
   the time anyone can scroll. */
body.is-warming .backdrop,
body.is-warming .backdrop-cloud--front,
body.is-warming .pin,
body.is-warming .pills { opacity: 0.004; }

body.is-ready .backdrop,
body.is-ready .backdrop-cloud--front,
body.is-ready .pin,
body.is-ready .pills { opacity: 1; }
/* The cue also fades on scroll progress, which the sequence writes into
   --cue. Multiplying here rather than letting it write `opacity`
   directly, because an inline opacity would beat the hold above. */
body.is-ready .cue { opacity: var(--cue, 1); }

/* The progress rule. Held back for a beat first: on a warm load the
   whole hold is over in about a second, and a bar that appears and
   vanishes inside that is a flicker rather than information. It only
   shows if the page is still waiting after `holdShowMs`. */
.hold {
  position: fixed;
  left: 50%;
  bottom: 3.2rem;
  transform: translateX(-50%);
  z-index: 160;
  width: min(38vw, 220px);
  height: 1px;
  background: rgba(27, 27, 25, 0.14);
  overflow: hidden;
  opacity: 0;
  transition: opacity 400ms var(--ease-out);
  pointer-events: none;
}
body.is-holding .hold { opacity: 1; }
body.is-ready .hold { opacity: 0; }
.hold i {
  display: block;
  height: 100%;
  background: var(--ink);
  /* Not zero: an empty bar reads as stalled. */
  transform: scaleX(0.06);
  transform-origin: left;
  transition: transform 420ms var(--ease-out);
}

/* No JS, no hold: the document is the page and it should just be there. */
html:not(.has-js) .backdrop,
body:not(.has-js) .backdrop { opacity: 1; }

/* ─── the sky ─────────────────────────────────────────────────────
   A composed 2D layer BEHIND the canvas: a gradient with cloud plates
   over it in mix-blend-mode: overlay. Overlay is the point — it keeps
   the backdrop's hue and moves only its lightness, so a plate sits in
   the sky instead of on top of it. `isolation` keeps that blending
   inside this layer rather than letting it reach the page.

   Fixed, so it holds still while the pinned sequence scrolls over it.
   Built by home/backdrop.js; tuned in dev/clouds.html. */
.backdrop {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  isolation: isolate;
  pointer-events: none;
}
.backdrop-sky { position: absolute; inset: 0; }
.backdrop-cloud {
  position: absolute;
  left: 0;
  top: 0;
  height: auto;
  transform-origin: 50% 50%;
  will-change: transform, opacity;
}

/* The few clouds that pass IN FRONT of the canvas. They sit directly on
   the body, with no wrapper and no z-index of their own: `mix-blend-mode`
   blends against the parent stacking context's backdrop, and any
   fixed-position wrapper with a z-index would make that an empty layer
   instead of the scene. Document order is what puts them above the
   pinned stage.

   Declared after the rule above, not before it: same specificity, so
   source order is what decides, and placed first it lost its `position`
   and the clouds scrolled away with the page.

   z-index 1 puts them level with the pinned stage and, being later in
   the document, above it — while the document itself at 2 still covers
   them. A z-index on the element is safe here: it makes a stacking
   context for its own descendants, but the element still blends against
   its PARENT's backdrop, which is the root and holds the canvas. Only an
   isolating ancestor would break that, which is what the wrapper did. */
.backdrop-cloud--front { position: fixed; z-index: 1; }

/* ─── the pinned sequence ─────────────────────────────────────── */
/* Isolating on purpose. The cabin wall, the note and the flash carry
   z-indexes 4-7 that are meant to order themselves WITHIN the stage;
   without a stacking context here they compete in the root one and
   paint straight over the document below. */
.pin { position: relative; z-index: 1; height: 700vh; }   /* height is set by JS from config */

.stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
}

.gl { display: block; width: 100%; height: 100%; }

/* Bloom over the moment the camera passes through the skin. */
.flash {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  background:
    radial-gradient(58% 46% at 52% 46%, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.55) 46%, rgba(255, 255, 255, 0) 78%);
  mix-blend-mode: screen;
}

.note-slot { position: absolute; inset: 0; pointer-events: none; }

/* ─── cabin ───────────────────────────────────────────────────────
   The wall sits over the canvas with the window punched out of it, so
   the sky the 3D scene is drawing shows through the aperture. */
.cabin {
  position: absolute;
  inset: 0;
  z-index: 4;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Film and surface grain over the whole cabin composition. One static
   tile of fractal noise, composited — never animated: moving grain is
   video noise, and what we want is the texture of a matte surface and
   the sensor under a photograph of one. Sits above the glass and below
   the note, so the plastics and the view get it and the collage stays
   clean. */
.cabin-grain {
  position: absolute;
  inset: 0;
  /* 6, not 5: it is appended after the blind layer, so it needs to be on
     the same level to cover it — a blind with no grain on it is the one
     smooth surface left in the frame. The note is created later still
     and stays on top. */
  z-index: 6;
  display: block;
  pointer-events: none;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 180px 180px;
}

/* No `will-change` on either of these, and the reason is measured.

   `clip-path` is not a compositor-animatable property, so declaring
   will-change on it promotes a full-viewport layer carrying six blended,
   blurred children and then re-rasters the whole promoted subtree every
   time the path changes — which is every frame. Sweeping the crossing on
   a cold load, the worst frame went 235ms -> 79ms just by dropping it.
   The hint was buying nothing and costing the spikes that tear the
   composite.

   And the children are the size of the stage rather than 140% of it.
   They are clipped to the aperture regardless, so the extra 20% a side
   was roughly twice the area being blended and blurred for no pixels
   anyone can see. */
.cabin-glass {
  position: absolute;
  inset: 0;
  z-index: 5;
  pointer-events: none;
}
.cabin-glass i {
  position: absolute;
  inset: 0;
  display: block;
}

/* Low sun catching the pane.

   Positioned off --sx/--sy, which the cabin writes each frame from the
   aperture. It used to be `at 62% 28%` of a layer inset -20% of the
   whole stage, so the highlight sat at roughly (0.57, 0.19) of the
   VIEWPORT — above the top of the window, on the wall, outside the clip.
   It has been painting almost entirely off the pane. */
.cabin-glass__flare {
  background:
    radial-gradient(var(--sr, 22vh) calc(var(--sr, 22vh) * 0.74) at var(--sx, 62%) var(--sy, 28%),
      rgba(255,253,246,1), rgba(255,250,235,0.42) 44%, rgba(255,255,255,0) 72%);
  mix-blend-mode: screen;
}

/* The sun on the pane. A low sun outside does not only make a bright
   patch — it throws a defined shaft across the acrylic, because the
   pane is not optically clean: every wipe mark and every micro-scratch
   scatters along the same axis. The shaft is what says the light has a
   direction, and it is the thing every reference photograph has that
   we did not.

   Masked from the sun's own position rather than laid across the whole
   pane, so it reads as a shaft from a source instead of a stripe on a
   picture — brightest at the highlight, gone by the far corner. That
   also keeps it in the upper half, where the sky is still saturated
   blue; a screen blend over the near-white lower half would do nothing
   at all, which is the mistake this layer stack has made twice. */
.cabin-glass__sun {
  background: linear-gradient(118deg,
    rgba(255,255,255,0) 34%,
    rgba(255,240,212,0.18) 42%,
    rgba(255,248,232,0.58) 47.5%,
    rgba(255,253,246,0.92) 50%,
    rgba(255,248,232,0.58) 52.5%,
    rgba(255,240,212,0.18) 58%,
    rgba(255,255,255,0) 66%);
  -webkit-mask-image: radial-gradient(var(--sm, 60vh) var(--sm, 60vh) at var(--sx, 62%) var(--sy, 28%),
    #000 0%, rgba(0,0,0,0.60) 40%, rgba(0,0,0,0.12) 74%, transparent 100%);
  mask-image: radial-gradient(var(--sm, 60vh) var(--sm, 60vh) at var(--sx, 62%) var(--sy, 28%),
    #000 0%, rgba(0,0,0,0.60) 40%, rgba(0,0,0,0.12) 74%, transparent 100%);
  mix-blend-mode: screen;
  /* No blur. A Gaussian over a smooth gradient is only a smoother
     gradient: convolved against its own 4px sigma this layer moves by
     at most 4.3/255 on alpha, and 0.017/255 in the mean, before a
     `screen` blend that can only lighten. What it cost was a
     full-viewport render surface re-rastered every frame, because
     cabin.js rewrites --sx/--sy/--sm on this element continuously.
     Measured: cabin-act p90 33.3ms -> 17.4ms across the three layers. */
}

/* the rainbow streak — dispersion across the pane, not a decal */
/* Dispersion, not a filter. Spanning 34%-70% of the pane this was a
   pastel wash across the entire view — pink at one corner, green at the
   other — which reads as a cheap lens effect, because a full-field
   rainbow is not a thing acrylic does. Real prismatic fringing is a
   narrow band thrown off one edge near the sun, and it is faint. */
.cabin-glass__chroma {
  background: linear-gradient(104deg,
    rgba(255,255,255,0) 47%,
    rgba(255,140,140,0.30) 50.5%,
    rgba(255,220,140,0.28) 52.5%,
    rgba(160,240,175,0.26) 54.5%,
    rgba(130,195,255,0.28) 56.5%,
    rgba(200,160,255,0.24) 58.5%,
    rgba(255,255,255,0) 62%);
  mix-blend-mode: screen;
  /* Same trade as __sun, and the stops here are wider still: max
     3.3/255 on alpha against its own 9px sigma. The fringe stays a
     fringe. */
}

/* Milk on the pane: the outer one is scratched and the gap between the
   panes hazes, so a cabin window is never optically clear. NOT screen —
   the sky behind it is already near-white in the lower half, and screen
   only lightens, so a white veil in screen over a white sky is exactly
   nothing. This is a real veil, thickest at the edges where a pane
   collects the most wear. */
.cabin-glass__haze {
  background:
    /* The cabin, reflected. You are looking at the inner dust cover
       from a foot away and it returns the room behind you — the ceiling
       line and the lit window band, soft and wide and low in contrast.
       This is the layer that says there is glass here at all. */
    linear-gradient(196deg,
      rgba(236,233,227,0.28) 0%,
      rgba(236,233,227,0.09) 17%,
      rgba(255,255,255,0) 33%),
    /* Fresnel. Reflectivity climbs steeply toward grazing incidence, so
       the rim of the pane returns far more of the room than its centre
       does — and the room is dark and warm, so the rim darkens. Getting
       this backwards (a bright edge veil) is what made the pane read as
       a light source instead of a sheet of acrylic. */
    radial-gradient(76% 64% at 50% 46%,
      rgba(58,55,52,0) 44%,
      rgba(58,55,52,0.11) 76%,
      rgba(50,48,46,0.26) 100%),
    /* And the film of condensation that collects in the corners. */
    radial-gradient(98% 94% at 50% 50%,
      rgba(226,236,246,0) 60%,
      rgba(226,236,246,0.22) 100%);
}

/* The cabin, reflected in the pane.

   Not the Fresnel veil in __haze — that is the rim darkening. This is
   the room itself: the seat back beside you as a soft dark mass low on
   one side, and the lit ceiling as a diffuse band across the top. Both
   are placed against the PANE via --px/--py/--pw/--ph, because the
   layer's own box is the stage plus a fifth, and a percentage in it
   lands somewhere else entirely.

   Normal blend, deliberately: the dominant half of a cabin reflection
   is dark, and screen cannot darken anything. Low, because the failure
   mode here is not "too subtle" — it is a smear that reads as a dirty
   monitor. */
.cabin-glass__reflect {
  background:
    radial-gradient(58% 34% at calc(var(--px) + var(--pw) * 0.34) calc(var(--py) + var(--ph) * 0.10),
      rgba(238,234,226,0.16), rgba(238,234,226,0.05) 46%, rgba(255,255,255,0) 78%),
    radial-gradient(26% 40% at calc(var(--px) + var(--pw) * 0.02) calc(var(--py) + var(--ph) * 0.78),
      rgba(34,32,30,0.13), rgba(34,32,30,0.04) 52%, rgba(0,0,0,0) 82%);
  /* This one's blur was provably doing nothing: the radials are already
     58% and 26% wide with soft stops, so a 10px sigma moves the layer by
     0.09/255 at its worst pixel — a quarter of one 8-bit level. It was
     paying for a full-viewport surface to change nothing at all. */
}

/* Fine scratches, concentric with the way a pane gets wiped. Almost
   subliminal — at any opacity you can name outright it reads as dirt on
   the screen rather than wear on the glass. */
.cabin-glass__wear {
  background:
    repeating-linear-gradient(72deg,
      rgba(255,255,255,0) 0 17px,
      rgba(255,255,255,0.05) 17px 18px,
      rgba(255,255,255,0) 18px 41px),
    repeating-linear-gradient(-58deg,
      rgba(255,255,255,0) 0 23px,
      rgba(255,255,255,0.035) 23px 24px,
      rgba(255,255,255,0) 24px 63px);
  mix-blend-mode: screen;
  opacity: 0.5;
}

/* ─── the note ────────────────────────────────────────────────────
   Paper stuck to the inside of the glass. Deliberately uneven: every
   piece has its own tone, angle and shadow, because the moment they
   match it stops reading as handmade.

   The container carries the hover state, not the pieces — their
   transforms are written inline per frame by the assembly animation,
   so a CSS hover transform on them would simply lose. */
.note {
  position: absolute;
  z-index: 6;
  display: block;
  width: 0;
  height: 0;
  font-size: calc(var(--aw, 400px) * 0.058);
  color: var(--ink);
  text-decoration: none;
  pointer-events: auto;
  transform: translate3d(0, 0, 0);
  transition: transform 260ms var(--ease-soft);

  /* Blue, not black.

     These were rgba(28,26,22) — a warm near-black — and the collage
     hangs in front of a bright sky. What a shadow actually is here is
     the sky with the direct light taken out of it: darker, and BLUER,
     because skylight is what is left. A neutral black ink does the
     opposite, and measurably so. Against the sky behind the card:

       black at 0.26   luminance 0.78x, blue-red +120 -> +87
       blue  at 0.24   luminance 0.87x, blue-red +120 -> +115

     So the old one both dug twice as deep and took a third of the blue
     out with it, which is the grey smudge under every piece. */
  --sh-strip: 0 0.10em 0.5em rgba(52,86,152,0.24), 0 0.02em 0.1em rgba(52,86,152,0.16);
  --sh-card:  0 0.08em 0.42em rgba(52,86,152,0.20), 0 0.015em 0.08em rgba(52,86,152,0.14);
  --sh-die:   0 0.06em 0.26em rgba(52,86,152,0.26);
}
.note:hover, .note:focus-visible {
  transform: translate3d(0, -0.055em, 0);
  --sh-strip: 0 0.20em 0.85em rgba(52,86,152,0.30), 0 0.03em 0.14em rgba(52,86,152,0.20);
  --sh-card:  0 0.17em 0.70em rgba(52,86,152,0.27), 0 0.02em 0.10em rgba(52,86,152,0.18);
  --sh-die:   0 0.12em 0.46em rgba(52,86,152,0.32);
}
.note:focus-visible { outline: 2px solid var(--ink); outline-offset: 0.5em; border-radius: 2px; }

.note__piece {
  position: absolute;
  left: 0;
  top: 0;
  display: block;
  will-change: transform, opacity;
}

/* torn strip — the tear is a real irregular edge, not a rounded corner */
.note__strip {
  /* 12.4em fitted "everything I kept". The headline is longer now and
     measured 224px against 210px of content width — short by 0.71em —
     so it broke onto a second line that fell straight through the torn
     bottom edge of the paper. Widened with a little to spare. */
  width: 13.7em;
  padding: 0.7em 0.9em 1.05em;
  background:
    linear-gradient(163deg, #fbf8f0 0%, #f4efe2 58%, #efe9da 100%);
  box-shadow: var(--sh-strip);
  clip-path: polygon(
    0% 3%, 4% 0%, 19% 2%, 33% 0%, 48% 3%, 63% 0%, 79% 2%, 92% 0%, 100% 3%,
    100% 88%, 93% 92%, 86% 88%, 77% 93%, 68% 89%, 58% 94%, 48% 90%,
    38% 95%, 29% 90%, 20% 94%, 11% 89%, 4% 93%, 0% 89%
  );
}
.note__hand {
  display: block;
  font-family: 'Caveat', 'Bradley Hand', cursive;
  font-weight: 600;
  font-size: 1.5em;
  line-height: 1.04;
  letter-spacing: 0.005em;
  color: #23211c;
}

/* the card underneath */
.note__card {
  width: 9.4em;
  padding: 0.62em 0.72em 0.68em;
  background: linear-gradient(172deg, #fdfcf8, #f7f4ec);
  border-radius: 0.06em;
  box-shadow: var(--sh-card);
}
/* One line now that the item count is gone, so it simply sits left. */
.note__card-row { display: flex; align-items: baseline; gap: 0.5em; }
.note__card-row b {
  font-family: 'Caveat', cursive;
  font-weight: 700;
  font-size: 1.15em;
  color: #23211c;
}
.note__rule { display: block; height: 1px; margin: 0.42em 0 0.34em; background: rgba(35,33,28,0.16); }
.note__card-sub {
  display: block;
  font-family: 'Caveat', cursive;
  font-size: 0.72em;
  line-height: 1.12;
  color: #4a463d;
}

/* tape — slightly frosted, and it peels at one corner on hover */
.note__tape {
  position: absolute;
  width: 3.1em;
  height: 0.86em;
  background: linear-gradient(180deg, rgba(255,253,244,0.72), rgba(238,232,214,0.62));
  box-shadow: 0 0.02em 0.08em rgba(52,86,152,0.14);
  transition: transform 300ms var(--ease-soft);
}
.note__tape--l { top: -0.34em; left: -0.7em; transform: rotate(-24deg); transform-origin: 100% 50%; }
.note__tape--r { bottom: -0.3em; right: -0.8em; transform: rotate(-16deg); transform-origin: 0% 50%; }
.note:hover .note__tape--l { transform: rotate(-31deg) translate(-0.1em, -0.09em); }
.note:hover .note__tape--r { transform: rotate(-10deg) translate(0.09em, 0.07em); }

/* die-cuts — each its own colour, angle and size */
.note__die { display: grid; place-items: center; }
.note__die svg { width: 100%; height: 100%; display: block; }
.note__die--plane { width: 1.7em; height: 1.7em; color: #2f5d86; filter: drop-shadow(var(--sh-die)); }
.note__die--star  { width: 1.15em; height: 1.15em; color: var(--accent); filter: drop-shadow(var(--sh-die)); }
.note__die--chip {
  padding: 0.16em 0.44em;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: var(--sh-die);
  font-family: 'Caveat', cursive;
  font-weight: 700;
  font-size: 0.78em;
  color: #2a2716;
}

/* quiet affordance — lowercase, no eyebrow, no appended arrow */
/* ─── the way in ──────────────────────────────────────────────────
   The window is the gateway, so it asks the one question the gallery's
   own intro used to ask and no longer gets the chance to: sound, or
   not. Two real links, so cmd-click and keyboard both work and a JS
   failure still lands somewhere sensible. */
/* Below the collage, not inside it. At 6.3em these sat on top of the
   card — 5,592 square pixels of overlap, measured — so the two links
   that are the entire point of the screen were printed over
   handwriting. And at 0.3em they rendered about ten pixels tall, which
   is too small for the primary action on the page whatever it sits on. */
.note__enter {
  position: absolute;
  left: 50%;
  /* In the NOTE's em, not its own. With the type size set here, `top`
     was measured in a third of the unit it reads as, so the number
     needed to be ~16 to mean "below the card" — arithmetic nobody
     wants to redo when the collage moves. The card ends at 6.35em.

     8.6 rather than 7: at 7 the gap measured 11px, which is closer than
     the lines inside the card are to each other, so the two links read
     as another row of the card rather than as the thing you do next.
     They are a separate decision and want separate air. */
  top: 8.6em;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 0.28em;
  white-space: nowrap;
}
.note__go {
  font-family: var(--font-mono);
  font-size: 0.42em;
  letter-spacing: 0.05em;
  /* Sized for a finger, not just an eye: this lands the hit area at
     ~44px, which is the floor for a touch target. */
  padding: 0.88em 1.25em;
  border: 1px solid rgba(38,36,31,0.28);
  border-radius: 2em;
  color: rgba(38,36,31,0.78);
  text-decoration: none;
  background: rgba(255,255,255,0.5);
  transition: color 180ms var(--ease-out), border-color 180ms var(--ease-out),
              background-color 180ms var(--ease-out);
}
.note__go:hover,
.note__go:focus-visible {
  color: rgba(38,36,31,1);
  border-color: rgba(38,36,31,0.55);
  background: rgba(255,255,255,0.85);
}
/* Legible against a pane that runs from deep blue at the top to near
   white at the bottom. At 0.55 with no ground behind it this failed the
   only place it actually sits — over bright cloud. The halo carries it
   without giving it a button's weight, which is the whole point of the
   quiet one. */
.note__go--quiet {
  border-color: transparent;
  background: none;
  color: rgba(38,36,31,0.78);
  text-shadow: 0 0 6px rgba(255,255,255,0.75), 0 0 2px rgba(255,255,255,0.9);
}
.note__go--quiet:hover,
.note__go--quiet:focus-visible { background: none; border-color: transparent; }

/* ─── floating label pills ────────────────────────────────────────
   Quiet and monospace. Positioned in screen space from projected 3D
   anchors, so they parallax with the globe rather than sitting flat
   on top of it. Decorative — the document below carries the content. */
.pills { position: fixed; inset: 0; z-index: 3; pointer-events: none; }

.pill {
  position: absolute;
  top: 0;
  left: 0;
  padding: 0.3rem 0.62rem;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.92);
  /* Same reason as the collage: these sit on sky too. */
  box-shadow: 0 1px 2px rgba(52, 86, 152, 0.10);
  color: var(--ink-muted);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  letter-spacing: 0.01em;
  white-space: nowrap;
  will-change: transform, opacity;
}

/* ─── the document ────────────────────────────────────────────────
   Visually quiet at the end of the scroll, but always present in the
   DOM and always focusable. */
.doc {
  position: relative;
  z-index: 2;
  max-width: 44rem;
  margin: 0 auto;
  padding: 6rem 1.5rem 8rem;
  background: var(--paper);
}
.doc__title { font-family: var(--font-display); font-size: clamp(2.6rem, 7vw, 4rem); line-height: 1; }
.doc__by { margin-top: 0.5rem; color: var(--ink-muted); }
.doc__lede { margin-top: 2rem; font-size: 1.05rem; line-height: 1.65; color: var(--ink-muted); max-width: 34rem; }

.doc__enter {
  display: inline-block;
  margin-top: 2.4rem;
  padding: 0.85rem 1.4rem;
  border: 1px solid var(--ink);
  border-radius: 4px;
  color: var(--ink);
  text-decoration: none;
  transition: background 200ms var(--ease-out), color 200ms var(--ease-out);
}
.doc__enter:hover { background: var(--ink); color: var(--paper); }
.doc__enter:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.doc__note { margin-top: 3rem; color: var(--ink-faint); max-width: 30rem; line-height: 1.6; }

/* ─── scroll affordance ───────────────────────────────────────────
   Quiet, lowercase, in the Act 1 voice — no all-caps eyebrow, no arrow
   glyph. The dot falls down the rail continuously so it reads as an
   invitation rather than a label. Opacity is driven by p, so it is
   there at the top and gone the moment you move. */
.cue {
  position: fixed;
  left: 50%;
  bottom: 3.2rem;
  transform: translateX(-50%);
  /* Above the in-front cloud plates, which sit at 1. */
  z-index: 4;
  display: grid;
  justify-items: center;
  gap: 0.7rem;
  pointer-events: none;
  will-change: opacity;
}
/* Louder than it was, and only louder — the form is unchanged.

   At 0.68rem in muted ink this sat at about 4.5:1 against the cloud it
   lands on, which is a contrast a caption can live at and an invitation
   cannot: it read as one more small grey mark in a frame that already
   has seven of them, and the labels around the globe are larger than it
   was. Full ink takes it to roughly 16:1.

   The rail's top end was 0.22 alpha over white — a hairline you had to
   already know about to find. Lifted, still a hairline.

   Deliberately NOT touched: the rail's 2.6rem height and the dot, since
   the keyframe falls exactly that far and resizing either is a change to
   the motion rather than to how much of it you can see. Nor
   `scrollCue.out`, so it still leaves the moment you move. */
.cue__word {
  font-family: var(--font-mono);
  font-size: 0.82rem;
  letter-spacing: 0.14em;
  color: var(--ink);
}
.cue__rail {
  position: relative;
  display: block;
  width: 1px;
  height: 2.6rem;
  background: linear-gradient(to bottom, rgba(27,27,25,0.48), rgba(27,27,25,0.12));
  overflow: hidden;
}
.cue__rail i {
  position: absolute;
  left: -1px;
  top: 0;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--ink);
  animation: cueFall 2.4s cubic-bezier(0.45, 0, 0.25, 1) infinite;
}
@keyframes cueFall {
  0%   { transform: translateY(-3px); opacity: 0; }
  22%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { transform: translateY(2.6rem); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .cue__rail i { animation: none; opacity: 0.7; }
}

/* ─── reduced motion ──────────────────────────────────────────────
   Not the sequence with the motion removed — a different page. The
   pinned section collapses to a single screen holding one still frame
   of the destination composition, and the document follows it as
   ordinary content. Nothing animates, nothing hijacks the scroll. */
body.is-reduced .pin { height: 100svh; }
body.is-reduced .stage { position: relative; }
body.is-reduced .cue,
body.is-reduced .flash { display: none; }
body.is-reduced .note { transition: none; }
body.is-reduced .doc { padding-top: 4rem; }

/* Degradation step: the blend-mode glass layer is the most expensive
   thing on top of the canvas, so it is the last thing dropped. */
body.no-glass .cabin-glass { display: none; }
body.no-glass .cabin-grain { display: none; }
body.no-glass .cabin-glass__chroma { filter: none; }
body.no-glass .cabin-glass__sun { filter: none; }

/* ─── debug HUD ───────────────────────────────────────────────── */
.hud {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 180;
  width: 16.5rem;
  padding: 0.85rem;
  border: 1px solid var(--rule);
  border-radius: 6px;
  background: rgba(244, 244, 242, 0.93);
  backdrop-filter: blur(10px);
  font-family: var(--font-mono);
  font-size: 0.68rem;
  color: var(--ink);
  display: none;
}
.hud.is-open { display: block; }

.hud__row { display: flex; align-items: baseline; justify-content: space-between; }
.hud__p b { font-size: 1.5rem; font-weight: 500; font-variant-numeric: tabular-nums; }
.hud__p span { color: var(--ink-muted); }

.hud__scrub { width: 100%; margin: 0.6rem 0 0.2rem; accent-color: var(--ink); }

.hud__acts { position: relative; height: 4px; margin-bottom: 0.7rem; background: var(--rule); }
.hud__acts i { position: absolute; top: -2px; width: 1px; height: 8px; background: var(--ink); opacity: 0.5; }

.hud__grid { display: grid; grid-template-columns: auto 1fr; gap: 0.15rem 0.7rem; margin-bottom: 0.7rem; }
.hud__grid dt { color: var(--ink-faint); }
.hud__grid dd { text-align: right; font-variant-numeric: tabular-nums; }

.hud__keys { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.hud__keys button {
  padding: 0.25rem 0.4rem;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: transparent;
  font: inherit;
  color: var(--ink);
  cursor: pointer;
}
.hud__keys button:hover { background: var(--ink); color: var(--paper); }
.hud__keys button.is-off { opacity: 0.35; }

.hud__hint { margin-top: 0.6rem; color: var(--ink-faint); line-height: 1.4; }

@media (max-width: 820px) {
  .hud { left: 1rem; right: 1rem; width: auto; }
}


/* ─── the document, once the sequence can run ─────────────────────
   The flight ends at the window and the window is the way in, so there
   is nothing below it any more. The document is not deleted though —
   it is what a screen reader, a crawler and a no-JS visitor get, and
   removing it to tidy the scroll would take all of that with it. It is
   taken out of the visual flow and left in the accessibility tree.

   Without JS the class is never added and the page is the document,
   which is the whole point of it. */
body.has-js .doc {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
