/*
 * site.css
 *
 * Single-theme on purpose. This page is a night scene lit by an additive
 * renderer; a light mode would mean a different composition, not a different
 * palette, so every colour is stated explicitly rather than inherited from the
 * host, and nothing is left to `prefers-color-scheme`.
 */

:root {
  --ink:        #05070d;   /* the canvas clear colour, matched exactly */
  --ink-deep:   #03040a;
  --paper:      #e8eef6;
  --muted:      #8092ad;
  --faint:      #4a5a75;
  --teal:       #39d6d0;
  --violet:     #a78bfa;
  --amber:      #f5b445;
  --line:       rgba(126, 152, 190, 0.16);

  --display: "Instrument Serif", "Iowan Old Style", Georgia, serif;
  --sans: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace;

  --gutter: clamp(1.25rem, 4vw, 3.5rem);
  --measure: 34rem;
}

*, *::before, *::after { box-sizing: border-box; }

/* The UA rule for `hidden` is `[hidden] { display: none }` — a plain
   type-and-attribute selector that any class setting `display` outranks. So
   `.hud { display: grid }` quietly beat `hud.hidden = true`, and the telemetry
   panel stayed on screen in the no-WebGL fallback. This restores the attribute
   to meaning what it says. */
[hidden] { display: none !important; }

html { scroll-behavior: auto; }   /* the rig owns scroll; never fight it */

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper);
  font-family: var(--sans);
  font-size: clamp(1rem, 0.95rem + 0.2vw, 1.0625rem);
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a { color: var(--teal); text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
a:hover { color: var(--paper); }

:where(a, button):focus-visible {
  outline: 2px solid var(--teal);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip {
  position: fixed;
  top: 0.75rem; left: 0.75rem;
  z-index: 100;
  padding: 0.6rem 1rem;
  background: var(--paper);
  color: var(--ink);
  font: 500 0.8rem/1 var(--mono);
  border-radius: 4px;
  transform: translateY(-160%);
  transition: transform 160ms ease;
}
.skip:focus { transform: none; }

/* ------------------------------------------------------------- the scene */

#gl {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  /* The canvas element is sized by CSS; its drawing buffer is sized in JS to
     the CSS box times a capped DPR. Setting width/height here in pixels would
     silently stretch the render. */
}

/* Corner falloff. Cheaper and steadier than a post-processing pass, and it is
   what stops the additive glow from reaching the screen edges and flattening
   the whole frame into a bright rectangle. */
.vignette {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(115% 88% at 50% 46%, rgba(5, 7, 13, 0) 38%, rgba(5, 7, 13, 0.55) 78%, rgba(3, 4, 10, 0.94) 100%);
}

/* A very faint horizontal texture. Additive rendering onto a flat dark field
   produces visible banding in the gradients; a 1px repeating overlay breaks it
   up for far less cost than dithering in the shader. */
.scanlines {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.35;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.014) 0px,
    rgba(255, 255, 255, 0.014) 1px,
    rgba(0, 0, 0, 0) 1px,
    rgba(0, 0, 0, 0) 3px
  );
}

/* Legibility scrim.
   Flying through the weight bundles puts bright filaments behind the copy, and
   white text on that is genuinely hard to read. Darkening the side the text
   lives on costs nothing and keeps the scene untouched everywhere else — much
   better than putting a panel behind the words, which would wall the reader
   off from the thing they are supposed to be inside. */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background: linear-gradient(
    to right,
    rgba(5, 7, 13, 0.88) 0%,
    rgba(5, 7, 13, 0.74) 20%,
    rgba(5, 7, 13, 0.34) 42%,
    rgba(5, 7, 13, 0) 62%
  );
}

/* ---------------------------------------------------------------- track */

/* The only job of this element is to be tall. Its height is the length of the
   flight: everything else reads scroll position as a fraction of it. */
.track { height: 900vh; position: relative; z-index: 2; pointer-events: none; }

/* -------------------------------------------------------------- masthead */

.masthead {
  position: fixed;
  z-index: 5;
  left: var(--gutter);
  bottom: clamp(3rem, 12vh, 8rem);
  max-width: min(var(--measure), calc(100vw - 2 * var(--gutter)));
  will-change: opacity, transform;
}

.eyebrow {
  margin: 0 0 1.1rem;
  font: 400 0.7rem/1 var(--mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--teal);
}

.masthead h1 {
  margin: 0 0 1.1rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(3rem, 9vw, 6.5rem);
  line-height: 0.94;
  letter-spacing: -0.015em;
  text-wrap: balance;
}
.masthead h1 em {
  font-style: italic;
  /* The one place the palette's three hues appear together, and the only
     gradient on the page. Spending it on the title keeps it an event. */
  background: linear-gradient(96deg, var(--teal) 4%, var(--violet) 54%, var(--amber) 96%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.standfirst {
  margin: 0;
  max-width: 30rem;
  color: var(--muted);
}

.cue {
  margin: 2.4rem 0 0;
  display: flex;
  align-items: center;
  gap: 0.8rem;
  font: 400 0.68rem/1 var(--mono);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--faint);
}
.cue-line {
  display: block;
  width: 3.5rem;
  height: 1px;
  background: linear-gradient(to right, var(--faint), transparent);
  animation: cue 2.6s ease-in-out infinite;
}
@keyframes cue {
  0%, 100% { transform: translateX(0); opacity: 0.4; }
  50%      { transform: translateX(0.55rem); opacity: 1; }
}

/* -------------------------------------------------------------- chapters */

.stage {
  position: fixed;
  inset: 0;
  z-index: 4;
  pointer-events: none;
}

.chapter {
  position: absolute;
  left: var(--gutter);
  top: 50%;
  width: min(var(--measure), calc(100vw - 2 * var(--gutter)));
  /* Composed from two custom properties so the scroll rig can drive the drift
     without ever clobbering the centring translate — a transform written from
     two places is the classic way scroll animation loses one of them. */
  transform: translateY(calc(-50% + var(--shift, 0px)));
  opacity: 0;
  filter: blur(var(--blur, 0px));
  will-change: opacity, transform, filter;
}

.chapter .eyebrow { color: var(--violet); }

.chapter h2 {
  margin: 0 0 0.9rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2rem, 4.4vw, 3.25rem);
  line-height: 1.06;
  letter-spacing: -0.01em;
  text-wrap: balance;
}

.chapter p {
  margin: 0;
  max-width: 29rem;
  color: #c3d0e2;
}

/* A hairline to the left of each chapter, tinted to that stage of the pass. */
.chapter::before {
  content: "";
  position: absolute;
  left: -1.15rem;
  top: 0.35rem;
  bottom: 0.35rem;
  width: 1px;
  background: linear-gradient(to bottom, var(--teal), transparent 85%);
  opacity: 0.5;
}

/* ------------------------------------------------------------------- HUD */

.hud {
  position: fixed;
  transition: opacity 200ms ease;
  z-index: 6;
  right: var(--gutter);
  top: var(--gutter);
  display: grid;
  gap: 0.28rem;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: rgba(5, 8, 16, 0.55);
  backdrop-filter: blur(9px);
  font: 400 0.68rem/1.35 var(--mono);
  color: var(--faint);
  letter-spacing: 0.04em;
}
.hud-row { display: grid; grid-template-columns: 5.4rem auto; gap: 0.9rem; }
.hud-row b {
  font-weight: 500;
  color: var(--paper);
  font-variant-numeric: tabular-nums;
  justify-self: end;
}
.hud .bars { letter-spacing: 0.14em; color: var(--teal); }

/* ------------------------------------------------------------------ rail */

.rail {
  position: fixed;
  transition: opacity 200ms ease;
  z-index: 6;
  right: calc(var(--gutter) * 0.55);
  top: 32%;
  bottom: 12%;
  width: 2px;
  background: var(--line);
  border-radius: 2px;
}
.rail-fill {
  position: absolute;
  inset: 0 0 auto 0;
  height: 0%;
  background: linear-gradient(to bottom, var(--teal), var(--violet));
  border-radius: 2px;
}
.rail-tick {
  position: absolute;
  left: -3px;
  width: 8px;
  height: 1px;
  background: var(--faint);
}
.rail-tick[data-on="1"] { background: var(--teal); box-shadow: 0 0 8px var(--teal); }

/* ---------------------------------------------------------------- reader */

/* The full text in normal document order. Visually hidden while the flight is
   running — but present for screen readers, for search, and for anyone whose
   browser could not start WebGL, in which case JS unhides it. */
.reader {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}
.reader.shown {
  position: relative;
  z-index: 7;
  width: auto; height: auto;
  margin: 0 auto;
  padding: clamp(4rem, 12vh, 8rem) var(--gutter);
  max-width: 42rem;
  overflow: visible;
  clip: auto;
  clip-path: none;
  white-space: normal;
}
.reader.shown .reader-heading { display: none; }
.reader.shown section { margin: 0 0 3.2rem; }
.reader.shown h3 {
  margin: 0 0 0.7rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.7rem, 3.6vw, 2.4rem);
  line-height: 1.1;
}
.reader.shown p { margin: 0; color: #c3d0e2; }

/*
 * The no-WebGL layout.
 *
 * Driven by one class on <body> rather than by JS writing inline styles onto
 * half a dozen elements. The first attempt did it the other way and forgot the
 * overlays: the scrim is a near-opaque gradient sitting at z-index 3, so it
 * carried on painting over the fallback article and greyed the whole thing out.
 */
body.no-gl .vignette,
body.no-gl .scanlines,
body.no-gl .scrim,
body.no-gl .cue,
/* "Every dot and every line below is drawn by a shader" is a promise the page
   cannot keep in this mode, and the notice underneath already says so. */
body.no-gl .standfirst { display: none; }

body.no-gl .masthead {
  position: static;
  left: auto;
  bottom: auto;
  max-width: 42rem;
  margin: 0 auto;
  padding: clamp(3rem, 10vh, 6rem) var(--gutter) 0;
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
}

.fallback { position: relative; z-index: 7; padding: var(--gutter) var(--gutter) 0; }
.fallback-note {
  max-width: 42rem;
  margin: 0 auto;
  padding: 0.85rem 1rem;
  border: 1px solid var(--line);
  border-left: 2px solid var(--amber);
  border-radius: 3px;
  font: 400 0.85rem/1.5 var(--mono);
  color: var(--muted);
}

/* ----------------------------------------------------------------- outro */

.outro {
  position: relative;
  z-index: 5;
  background: linear-gradient(to bottom, rgba(5, 7, 13, 0) 0%, var(--ink) 22%, var(--ink-deep) 100%);
  padding: 26vh var(--gutter) clamp(4rem, 10vh, 7rem);
}
.outro-inner { max-width: 46rem; margin: 0 auto; }
.outro h2 {
  margin: 0 0 2.2rem;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.2rem, 5vw, 3.4rem);
  line-height: 1.05;
}

.specs {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  gap: 1.6rem 2rem;
  margin: 0 0 2.6rem;
  padding: 1.8rem 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.specs b {
  display: block;
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2rem, 4vw, 2.8rem);
  line-height: 1;
  color: var(--teal);
  font-variant-numeric: tabular-nums;
}
.specs span {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.86rem;
  line-height: 1.5;
  color: var(--muted);
}

.outro-body { margin: 0 0 2.4rem; color: #c3d0e2; }
.outro-body code {
  font: 400 0.88em/1 var(--mono);
  color: var(--amber);
  background: rgba(245, 180, 69, 0.08);
  padding: 0.15em 0.35em;
  border-radius: 3px;
}

.links { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0 0 2.6rem; }
.btn {
  display: inline-block;
  padding: 0.72rem 1.35rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  font: 500 0.82rem/1 var(--mono);
  letter-spacing: 0.04em;
  color: var(--paper);
  text-decoration: none;
  transition: border-color 160ms ease, background 160ms ease, color 160ms ease;
}
.btn:hover { border-color: var(--teal); color: var(--teal); }
.btn.primary { background: var(--teal); border-color: var(--teal); color: var(--ink); }
.btn.primary:hover { background: var(--paper); border-color: var(--paper); color: var(--ink); }

.sig { margin: 0; font: 400 0.78rem/1.6 var(--mono); color: var(--faint); }

/* ---------------------------------------------------------------- mobile */

@media (max-width: 780px) {
  /* The copy sits at the bottom on narrow screens, so the scrim has to move
     with it. */
  .scrim {
    background: linear-gradient(
      to top,
      rgba(5, 7, 13, 0.92) 0%,
      rgba(5, 7, 13, 0.80) 30%,
      rgba(5, 7, 13, 0.34) 52%,
      rgba(5, 7, 13, 0) 74%
    );
  }
  .hud { display: none; }          /* seven rows of telemetry over the copy is noise */
  .rail { right: 0.5rem; top: 26%; }
  .masthead { bottom: clamp(2rem, 8vh, 5rem); }
  .chapter { top: auto; bottom: 12vh; transform: translateY(var(--shift, 0px)); }
  .chapter::before { display: none; }
}

@media (max-height: 520px) {
  .cue { display: none; }
}

/* -------------------------------------------------------- reduced motion */

/* The flight itself is driven by the reader's own scrolling, so it stays —
   taking it away would leave a still image and no way to see the piece. What
   goes is everything that moves on its own: the ambient drift in the shaders
   (handled in JS), the scroll smoothing, and the animated cue. */
@media (prefers-reduced-motion: reduce) {
  .cue-line { animation: none; }
  .chapter { filter: none !important; transition: none !important; }
  * { scroll-behavior: auto !important; }
}
