/* duguid.org.uk — "Vault"
   Text layer. The canvas behind it is drawn by assets/glsl/*.
   Everything here is compositor-only: opacity, transform, filter. Nothing that
   touches layout is animated, because the station sync runs every frame. */

/* Self-hosted, and subset to the exact characters this page uses — all three
   faces together come to about 50 kB. Rebuild with tools/build-fonts.py after
   changing any copy, or a character will silently fall back.

   STIX Two Text is the text companion to STIX Two Math, so the prose and the
   equations are drawn by the same hand. That is the whole reason for it: a
   formula set in a different face from the sentence around it always looks
   pasted in. Both are OFL. */

@font-face {
  font-family: "STIX Two Text";
  src: url("/assets/fonts/stix-two-text-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: "STIX Two Text";
  src: url("/assets/fonts/stix-two-text-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: block;
}
@font-face {
  font-family: "IBM Plex Mono";
  src: url("/assets/fonts/ibm-plex-mono-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Ink. The shader supplies the colour of the world; the type stays close to
     paper-white so it reads against every station's grade. */
  --ink:        #f4efe6;
  --ink-dim:    #b9b1a4;
  --ink-faint:  #7d766c;
  --accent:     #e8b661;

  --measure:    38rem;
  --pad:        clamp(1.5rem, 5vw, 4rem);

  --face-text:  "STIX Two Text", "Iowan Old Style", "Palatino Linotype", Palatino,
                Georgia, "Times New Roman", serif;
  --face-mono:  "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  color-scheme: dark;
}

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

html, body {
  height: 100%;
  margin: 0;
  background: #05060a;
  color: var(--ink);
  font-family: var(--face-text);
  font-size: 16px;
  line-height: 1.55;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
}

/* The document never scrolls: descent is virtual. The no-gl fallback below
   hands scrolling back to the browser. */
body {
  overflow: hidden;
  overscroll-behavior: none;
  /* Not `none`: that took the browser's pinch-to-zoom with it, and the type is
     the only content on the page — a low-vision reader must be able to magnify
     it (WCAG 1.4.4). `pinch-zoom` keeps the gesture; single-finger panning is
     still ours to drive the descent (scroll.js bows out on a second finger). */
  touch-action: pinch-zoom;
}

/* ── the world ─────────────────────────────────────────────────────────── */

#vault {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
}

/* The contrast floor. The graph lattice and the Euler chamber are bright
   enough that body copy over them would otherwise fail AA, and legibility
   cannot depend on where the camera happens to be pointing.

   A scrim down the side the text lives on, rather than a plate behind each
   station: a plate is a dark oval that looks like a mistake the moment the
   world behind it turns pale, whereas a directional gradient reads as
   deliberate at any brightness — it is how a title sequence would do it. */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(100deg,
      rgb(2 3 6 / .80) 0%,
      rgb(2 3 6 / .66) 22%,
      rgb(2 3 6 / .34) 46%,
      rgb(2 3 6 / .05) 68%,
      rgb(2 3 6 / 0) 82%),
    linear-gradient(to top, rgb(2 3 6 / .45), rgb(2 3 6 / 0) 32%);
}

/* Film grain. One inline SVG turbulence, no image file. This is the cheapest
   thing that stops a flat gradient looking flat. */
.grain {
  position: fixed;
  inset: -50%;
  z-index: 1;
  pointer-events: none;
  opacity: .16;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── the text layer ────────────────────────────────────────────────────── */

#stations {
  position: fixed;
  inset: 0;
  z-index: 2;
  pointer-events: none;
}

.station {
  position: absolute;
  top: 50%;
  left: 50%;
  width: min(var(--measure), calc(100vw - 2 * var(--pad)));
  margin: 0;
  padding: 0;

  /* --z   signed distance to this station, normalised. >0 = still below us.
     --near proximity, 0..1, peaking as we pass through it.
     Both are written every frame by stations.js. */
  --z: 2;
  --near: 0;

  /* Faded, never hidden: `visibility: hidden` would drop the text out of the
     accessibility tree and make the contact link unfocusable. See
     stations.js. */
  opacity: var(--near);
  transform:
    translate3d(-50%, calc(-50% + var(--z) * 38vh), 0)
    scale(calc(0.92 + var(--near) * 0.08));
  filter: blur(calc((1 - var(--near)) * 7px));
  will-change: opacity, transform, filter;
}

/* Faded out entirely: stop paying the compositor to blur it. Set by
   stations.js; opacity and the accessibility tree are untouched. */
.station.far { filter: none; will-change: auto; }

/* A little shadow under each glyph, for the thin strokes a scrim alone will
   not save. */
.station .title,
.station .formula,
.station .lede,
.station .notes,
.station .era { text-shadow: 0 1px 14px rgb(2 3 6 / .7), 0 0 3px rgb(2 3 6 / .5); }

.station > * { margin: 0 0 .85rem; }
.station > :last-child { margin-bottom: 0; }

.era {
  font-family: var(--face-mono);
  font-size: clamp(.66rem, 1.4vw, .78rem);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink);
  margin-bottom: 1.1rem;
}

.title {
  font-size: clamp(3rem, 11vw, 7.5rem);
  font-weight: 400;
  line-height: .92;
  letter-spacing: -.025em;
  margin-bottom: 1.2rem;
}

.lede {
  font-size: clamp(1.05rem, 2.4vw, 1.3rem);
  line-height: 1.45;
  color: var(--ink);
  max-width: 30ch;
}

.formula {
  font-size: clamp(1.6rem, 5vw, 2.8rem);
  line-height: 1.15;
  letter-spacing: .01em;
  color: var(--ink);
  margin: 1.1rem 0 1.3rem;
}
.formula--xl { font-size: clamp(2.2rem, 7.5vw, 4.4rem); color: #fff; }
.formula sup { font-size: .58em; vertical-align: super; line-height: 0; }
.formula i { font-style: italic; }

.notes {
  list-style: none;
  padding: 0;
  font-size: clamp(.86rem, 1.8vw, .98rem);
  color: var(--ink-dim);
  line-height: 1.5;
}
.notes li {
  padding: .3rem 0 .3rem 0;
  border-top: 1px solid rgb(244 239 230 / .12);
}
.notes li:first-child { border-top: 0; }

.k {
  display: inline-block;
  min-width: 9.5rem;
  color: var(--ink);
  font-variant-caps: small-caps;
  letter-spacing: .04em;
}
.num { font-family: var(--face-mono); font-size: .92em; color: var(--ink); }

.aside {
  font-size: clamp(.8rem, 1.6vw, .9rem);
  color: var(--ink-faint);
  font-style: italic;
  margin-top: 1.1rem;
}

.station--euler .title { color: #fffaf0; }

/* ── colophon ──────────────────────────────────────────────────────────── */

.colophon { text-align: left; }
.colophon .name {
  font-family: var(--face-mono);
  font-size: clamp(.9rem, 2vw, 1.05rem);
  letter-spacing: .12em;
  color: var(--ink);
}
.colophon .contact { font-size: 1.05rem; }
.colophon a {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid rgb(232 182 97 / .4);
  pointer-events: auto;
}
.colophon a:hover, .colophon a:focus-visible { border-bottom-color: var(--accent); }

/* ── the one affordance ────────────────────────────────────────────────── */
/* There is no scrollbar, so there is no other way to know the page moves.
   It leaves permanently on the first input and does not come back. */

.hint {
  position: fixed;
  left: 50%;
  bottom: 2.2rem;
  z-index: 3;
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .7rem;
  margin: 0;
  pointer-events: none;
  transition: opacity .8s ease;
}
.hint__word {
  font-family: var(--face-mono);
  font-size: .64rem;
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--ink-faint);
  padding-left: .34em;
}
.hint__rule {
  width: 1px;
  height: 2.6rem;
  background: linear-gradient(to bottom, rgb(244 239 230 / .38), rgb(244 239 230 / 0));
}
body.has-moved .hint { opacity: 0; }

/* ── focus ─────────────────────────────────────────────────────────────── */

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ── fallback: no WebGL2 ───────────────────────────────────────────────── */
/* Must look deliberate rather than broken: an ordinary, handsome long page. */

html.no-gl body {
  overflow: auto;
  touch-action: auto;
  background:
    radial-gradient(120% 80% at 50% 0%, #16171f 0%, #05060a 60%),
    #05060a;
}
html.no-gl #vault,
html.no-gl .scrim,
html.no-gl .hint { display: none; }
html.no-gl #stations {
  position: static;
  pointer-events: auto;
  max-width: var(--measure);
  margin: 0 auto;
  padding: 18vh var(--pad) 22vh;
}
html.no-gl .station {
  position: static;
  width: auto;
  opacity: 1;
  transform: none;
  filter: none;
  margin-bottom: 22vh;
  padding-bottom: 6vh;
  border-bottom: 1px solid rgb(244 239 230 / .1);
}
html.no-gl .station--last { border-bottom: 0; }

/* ── reduced motion ────────────────────────────────────────────────────── */
/* The shader still runs — it simply stops drifting, and the type stops
   sliding. A still frame of this world is still worth looking at. */

@media (prefers-reduced-motion: reduce) {
  .station {
    transform: translate3d(-50%, calc(-50% + var(--z) * 26vh), 0);
    filter: none;
  }
  .hint { transition: none; }
}

@media (prefers-contrast: more) {
  .station { filter: none; }
  .notes { color: var(--ink); }
  /* A plate behind each station was tried and rejected (see .scrim): it reads
     as a mistake the moment the world behind it turns pale. The rule that used
     to sit here styled `.station::before`, but no rule ever gives that
     pseudo-element a `content`, so it painted nothing — a reader who asked for
     more contrast got the blur removed and no darker backdrop at all. So deepen
     and widen the same directional scrim instead: a real, stronger wash behind
     the text in any orientation, everywhere the camera might point. `body`
     lifts its specificity above the base and narrow-screen `.scrim` rules. */
  body .scrim {
    background:
      linear-gradient(to top, rgb(1 2 4 / .84), rgb(1 2 4 / .58)),
      linear-gradient(100deg, rgb(1 2 4 / .52) 0%, rgb(1 2 4 / .30) 100%);
  }
}

@media print {
  @page { size: A4; margin: 16mm 14mm; }
  html, body { background: #fff; color: #000; overflow: visible; height: auto; }
  #vault, .scrim, .grain, .hint { display: none !important; }
  #stations { position: static; }
  .station {
    position: static; width: auto; opacity: 1; transform: none; filter: none;
    page-break-inside: avoid; margin-bottom: 2rem;
  }
  .era, .k, .num, .accent { color: #000; }
  .notes { color: #222; }
}

/* ── narrow screens ────────────────────────────────────────────────────── */
/* A phone is not a small desktop. The two-column note rows collapse (a
   9.5rem label beside a value leaves nothing for the value at 390px), and the
   scrim turns bottom-weighted, because in portrait the text spans the full
   width and a scrim down one side no longer sits behind anything. */

@media (max-width: 46rem) {
  :root { --pad: 1.35rem; }

  .scrim {
    background:
      linear-gradient(to top,
        rgb(2 3 6 / .90) 0%,
        rgb(2 3 6 / .80) 40%,
        rgb(2 3 6 / .62) 75%,
        rgb(2 3 6 / .50) 100%);
  }

  .station {
    width: calc(100vw - 2 * var(--pad));
    transform: translate3d(-50%, calc(-50% + var(--z) * 30vh), 0)
               scale(calc(0.95 + var(--near) * 0.05));
    filter: blur(calc((1 - var(--near)) * 4px));
  }

  .title { font-size: clamp(2.6rem, 14vw, 4.4rem); margin-bottom: .9rem; }
  .formula { font-size: clamp(1.5rem, 8vw, 2.2rem); }
  .formula--xl { font-size: clamp(2rem, 11vw, 3rem); }
  .lede { font-size: 1.02rem; max-width: none; }

  .k {
    display: block;
    min-width: 0;
    margin-bottom: .05rem;
  }
  .notes { font-size: .88rem; }
  .notes li { padding: .5rem 0; }

  .hint { bottom: 1.4rem; }
}

/* Landscape on a phone: almost no vertical room, so give the type less of it. */
@media (max-height: 30rem) and (orientation: landscape) {
  .title { font-size: clamp(2rem, 7vh, 3rem); margin-bottom: .5rem; }
  .station > * { margin-bottom: .5rem; }
  .aside, .hint { display: none; }
}
