/* GET ROOTED — design tokens, typography, and the grain that sits over
   everything. Shared by the landing page and the deck. */

/* ---------------------------------------------------------------- *
 * Fonts. Paths are relative to this file; the build step rewrites
 * them into data: URIs so the output is a single portable document.
 * latin-ext carries ă â î ș ț — do not drop it, the Romanian
 * translation depends on it.
 * ---------------------------------------------------------------- */
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300 700;
  font-display: block;
  src: url('fonts/fraunces-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 300 700;
  font-display: block;
  src: url('fonts/fraunces-latinext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url('fonts/inter-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 300 600;
  font-display: swap;
  src: url('fonts/inter-latinext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

@font-face {
  font-family: 'Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('fonts/mono-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'Mono';
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url('fonts/mono-latinext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF,
    U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020,
    U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* ---------------------------------------------------------------- *
 * Tokens
 * ---------------------------------------------------------------- */
:root {
  /* Forest, not soil. The references sit around hue 109° — moss and fern —
     with warm amber only ever appearing as glow, never as a ground tone. */
  --forest-900: #0d1410;
  --forest-800: #16211a;
  --forest-700: #1e2b22;
  --bark:     #3a3226;
  --clay:     #8a6947;
  --moss:     #5f6f45;
  --fern:     #8ba05e;
  --dusk:     #2b3d52;
  --ochre:    #e8a24a;
  --ember:    #ff8c3a;
  --bone:     #f2ece0;
  /* Higher alphas than a flat-background design would need: body copy now
     sits directly on photographs, where 66% white reads as washed out. */
  --bone-dim: rgba(242, 236, 224, 0.88);
  --bone-faint: rgba(242, 236, 224, 0.58);
  --hairline: rgba(242, 236, 224, 0.22);

  --display: 'Fraunces', 'Iowan Old Style', Georgia, 'Times New Roman', serif;
  --body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --mono: 'Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;

  /* Fluid type — one scale, used by both documents. */
  --step--1: clamp(0.75rem, 0.72rem + 0.15vw, 0.84rem);
  --step-0:  clamp(1rem, 0.95rem + 0.24vw, 1.16rem);
  --step-1:  clamp(1.2rem, 1.1rem + 0.5vw, 1.55rem);
  --step-2:  clamp(1.5rem, 1.28rem + 1.1vw, 2.3rem);
  --step-3:  clamp(2rem, 1.5rem + 2.4vw, 3.6rem);
  --step-4:  clamp(2.8rem, 1.7rem + 5vw, 6rem);
  --step-5:  clamp(3.4rem, 1.2rem + 10vw, 11rem);

  --gutter: clamp(1.25rem, 4vw, 4.5rem);
  /* 34ch measured 44 characters in Inter 300 — caption width, and
     narrower than the grid columns it was placed in, so a declared
     5-column span rendered as 3.6. */
  --measure: 46ch;

  /* Pace. Every chapter used to breathe identically, which gave the page
     length but no rhythm. */
  --pace-s: clamp(3rem, 7vh, 5.5rem);
  --pace-m: clamp(7rem, 20vh, 16rem);
  --pace-l: clamp(13rem, 36vh, 28rem);

  /* One mono voice, three trackings — it had nine. */
  --track-lg: 0.34em;
  --track: 0.2em;
  --track-sm: 0.08em;

  /* The accent travels with the night. Sections retint this. */
  --accent: var(--ochre);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* Film grain, generated in-document so nothing is fetched. */
  --grain-src: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)' opacity='0.5'/%3E%3C/svg%3E");

  color-scheme: dark;
}

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
}

body {
  margin: 0;
  background: var(--forest-900);
  color: var(--bone);
  font-family: var(--body);
  font-size: var(--step-0);
  font-weight: 300;
  line-height: 1.62;
  letter-spacing: 0.005em;
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  /* clip, not hidden. `overflow-x: hidden` silently forces `overflow-y:
     auto`, which turns the body into a scroll container and stops
     `position: sticky` from ever pinning to the viewport. `clip` trims the
     same overflow without creating one. */
  overflow-x: hidden;
  overflow-x: clip;
}

h1, h2, h3 {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  font-variation-settings: 'SOFT' 40, 'WONK' 1, 'opsz' 100;
  letter-spacing: -0.022em;
  line-height: 0.94;
  text-wrap: balance;
}

p { margin: 0; text-wrap: pretty; }

/* Body-sized text sits directly on photographs. A soft shadow buys real
   legibility over busy foliage without dimming the picture the way more
   scrim would — the whole point of this redesign was to stop doing that. */
p, .label, .clock__value, .clock__zone, .phase__body, .topnav a, .deck-count, .deck-hint {
  text-shadow:
    0 1px 14px rgba(6, 10, 8, 0.9),
    0 0 4px rgba(6, 10, 8, 0.7);
}
h1, h2, h3, blockquote span {
  text-shadow: 0 2px 28px rgba(6, 10, 8, 0.72);
}

a { color: inherit; }

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

::selection { background: var(--clay); color: var(--bone); }

/* ---------------------------------------------------------------- *
 * Shared pieces
 * ---------------------------------------------------------------- */

/* Small caps label — the mono voice used for every piece of metadata. */
.label {
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--bone-dim);
}

.rule {
  height: 1px;
  background: var(--hairline);
  border: 0;
  margin: 0;
}

/* ---------------------------------------------------------------- *
 * The gate
 *
 * Nothing behind this is reachable until it is crossed. Two jobs: it
 * hides the seconds it takes a few megabytes of WebP to decode, and the
 * click that dismisses it is a genuine user gesture — which is the only
 * thing a browser will accept as permission to start audio.
 * ---------------------------------------------------------------- */
.gate {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: grid;
  place-items: center;
  padding: var(--gutter);
  background: var(--forest-900);
  transition: opacity 1.1s var(--ease-out), visibility 0s linear 1.1s;
}
.gate__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: 1.1rem;
  max-width: 32rem;
}

.gate__mark {
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.42em;
  text-indent: 0.42em;
  text-transform: uppercase;
  color: var(--bone-dim);
  opacity: 0;
  animation: gate-in 1.1s var(--ease-out) 0.15s forwards;
}
.gate__tagline {
  font-family: var(--display);
  font-style: italic;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 40;
  font-size: var(--step-2);
  font-weight: 300;
  line-height: 1.15;
  color: var(--bone);
  max-width: 18ch;
  opacity: 0;
  animation: gate-in 1.2s var(--ease-out) 0.35s forwards;
}
@keyframes gate-in {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: none; }
}

/* The button is the taproot: the drawing grows down out of the word. */
.gate__enter {
  appearance: none;
  position: relative;
  margin-top: 0.8rem;
  padding: 0 0 5.4rem;
  border: 0;
  background: none;
  color: var(--ochre);
  font-family: var(--mono);
  font-size: var(--step-0);
  font-weight: 500;
  letter-spacing: 0.26em;
  text-indent: 0.26em;
  text-transform: uppercase;
  cursor: pointer;
  opacity: 0;
  animation: gate-in 1.2s var(--ease-out) 0.6s forwards;
  transition: color 0.4s var(--ease);
}
.gate__enter[disabled] { color: var(--bone-faint); cursor: default; }
.gate__enter:not([disabled]):hover { color: var(--bone); }

.gate__roots {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 15rem;
  height: 5.25rem;
  transform: translateX(-50%);
  overflow: visible;
  pointer-events: none;
}
.gate__roots path {
  stroke-dasharray: 130;
  stroke-dashoffset: 130;
  opacity: 0.75;
}
/* Roots creep outward once the forest is ready to be entered. */
.gate__enter:not([disabled]) .gate__roots path {
  animation: root-grow 2.4s var(--ease-out) forwards;
}
.gate__enter:not([disabled]) .gate__roots path:nth-child(2)  { animation-delay: 0.10s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(3)  { animation-delay: 0.16s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(4)  { animation-delay: 0.30s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(5)  { animation-delay: 0.36s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(6)  { animation-delay: 0.52s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(7)  { animation-delay: 0.58s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(8)  { animation-delay: 0.24s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(9)  { animation-delay: 0.70s; }
.gate__enter:not([disabled]) .gate__roots path:nth-child(10) { animation-delay: 0.76s; }
@keyframes root-grow { to { stroke-dashoffset: 0; } }

/* Hovering pushes them a little further into the soil. */
.gate__enter:not([disabled]):hover .gate__roots { transform: translateX(-50%) scaleY(1.06); }
.gate__roots { transition: transform 0.7s var(--ease-out); transform-origin: 50% 0; }

.gate__loading { display: none; }
.gate__enter[disabled] .gate__enterLabel { display: none; }
.gate__enter[disabled] .gate__loading { display: inline; }

/* Sound consent, asked once, before anything can make a noise. */
.gate__sound {
  display: flex;
  align-items: flex-start;
  gap: 0.7rem;
  margin-top: 1.4rem;
  padding: 0.8rem 1.1rem;
  border: 1px solid var(--hairline);
  border-radius: 3px;
  text-align: left;
  cursor: pointer;
  opacity: 0;
  animation: gate-in 1.2s var(--ease-out) 0.85s forwards;
  transition: border-color 0.35s var(--ease);
}
.gate__sound:hover { border-color: var(--bone-faint); }
.gate__sound input { position: absolute; opacity: 0; pointer-events: none; }

.gate__box {
  flex: 0 0 auto;
  width: 1.05rem;
  height: 1.05rem;
  margin-top: 0.15rem;
  border: 1px solid var(--bone-faint);
  border-radius: 2px;
  position: relative;
  transition: border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.gate__box::after {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--forest-900);
  clip-path: polygon(16% 52%, 40% 74%, 84% 24%, 92% 34%, 41% 88%, 8% 60%);
  opacity: 0;
  transition: opacity 0.2s var(--ease);
}
.gate__sound input:checked ~ .gate__box {
  background: var(--ochre);
  border-color: var(--ochre);
}
.gate__sound input:checked ~ .gate__box::after { opacity: 1; }
.gate__sound input:focus-visible ~ .gate__box { outline: 2px solid var(--ochre); outline-offset: 3px; }

.gate__soundText {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bone-dim);
}
.gate__soundText small {
  font-size: 0.82em;
  letter-spacing: 0.1em;
  text-transform: none;
  color: var(--bone-faint);
}

html[data-entered] .gate {
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .gate__mark, .gate__tagline, .gate__enter, .gate__sound { animation: none; opacity: 1; }
  .gate__roots path { stroke-dashoffset: 0; animation: none; }
}

/* ---------------------------------------------------------------- *
 * The plate layer — one photographic backdrop per chapter.
 * ---------------------------------------------------------------- */
.plates {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--forest-900);
  pointer-events: none;
}

.plate {
  position: absolute;
  inset: 0;
  opacity: 0;
  transform: scale(1.08);
  /* Opacity alone controls visibility. Toggling `visibility` on a layer
     that is also `will-change`-promoted can leave Chrome showing a stale
     composited frame — the plate stays blank at opacity 1. */
  /* will-change is applied per plate in plates.js, only while visible. */
  /* No transition. The cross-fade is computed from scroll position every
     frame, so a transition would only lag behind it — and an unfinished
     one leaves the opening plate stuck at zero. The deck, whose slides
     change discretely, adds its own transition in deck.css. */
}
/* Barely touched. These photographs are the entire point of the redesign —
   darkening them here is what made the first attempt look like mud.
   Legibility is handled by the scrim, locally. */
.plate {
  background-size: cover;
  background-position: center 50%;
  background-repeat: no-repeat;
  filter: saturate(1.02) brightness(0.96);
}
/* Per-plate veil, set from media.js — see the note on `dim` there. */
.plate::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(9, 14, 11, var(--dim, 0));
}

/* The scrim. Photographs are busy and bright in places; this is what
   guarantees the copy stays readable over all of them. Tuned per side
   because the layout is left-weighted. */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    /* Left column carries the display type, so it keeps the most weight,
       but the right column holds body copy too — hence a floor of ~0.35
       across the whole frame rather than a ramp to nothing. */
    linear-gradient(100deg,
      rgba(9, 14, 11, 0.82) 0%,
      rgba(9, 14, 11, 0.58) 32%,
      rgba(9, 14, 11, 0.36) 62%,
      rgba(9, 14, 11, 0.40) 100%),
    /* Top and bottom bands, for the fixed chrome. */
    linear-gradient(to bottom,
      rgba(9, 14, 11, 0.55) 0%,
      rgba(9, 14, 11, 0.06) 20%,
      rgba(9, 14, 11, 0.06) 72%,
      rgba(9, 14, 11, 0.60) 100%);
}

/* The WebGL glow, over the plate and under the content.
 *
 * The canvas must ADD light to the page rather than composite over it.
 * Additive rendering accumulates alpha as well as colour, so a plain
 * transparent canvas ends up nearly opaque wherever roots overlap — and
 * an opaque canvas of dim amber hides the photograph completely. Blending
 * the whole canvas means its black areas contribute nothing no matter
 * what alpha they carry. */
.stage {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 1;
  pointer-events: none;
  /* No blend mode. The glow materials bound their own alpha (see the
     CustomBlending in scene.js), so ordinary compositing already leaves
     the photograph visible — and one less blended full-screen layer is
     one less thing for the compositor to get wrong. */
}

/* The beat.
 *
 * Fires the instant one backdrop actually overtakes the last, so a
 * chapter change lands as a moment rather than a wash. Deliberately its
 * own layer: the plates themselves must never carry a transition, since
 * their opacity is rewritten every frame from scroll position. */
.beat {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(ellipse at 50% 42%,
    rgba(232, 162, 74, 0.16) 0%, transparent 62%);
}
.beat.is-firing { animation: beat-pulse 560ms cubic-bezier(0.2, 0.9, 0.3, 1); }
/* A frame drum's attack is under 20ms. The previous curve peaked at 32%
   of a 900ms animation — a 288ms swell, which reads as a glow, not a hit. */
@keyframes beat-pulse {
  0%   { opacity: 0;    transform: scale(1.00); }
  7%   { opacity: 1;    transform: scale(1.03); }
  26%  { opacity: 0.42; transform: scale(1.06); }
  100% { opacity: 0;    transform: scale(1.12); }
}

@media (prefers-reduced-motion: reduce) { .beat { display: none; } }

/* Paper grain over the entire page — the thing that makes a WebGL render
   stop looking like a WebGL render. */
.grain {
  position: fixed;
  /* The keyframes below shift by at most 3% of this box, so anything
     over ~3.2% of overscan is wasted. At -50% the element was 2x the
     viewport in each axis — 4x the area — as a permanently promoted,
     soft-light-blended layer re-compositing several times a second. */
  inset: -4%;
  z-index: 2;
  pointer-events: none;
  /* soft-light, not overlay. Overlay behaves like multiply wherever the
     backdrop is darker than 50% grey, so over a scrimmed night photograph
     it compounds with the scrim and crushes the image to black. */
  opacity: 0.34;
  mix-blend-mode: soft-light;
  background-image: var(--grain-src);
  background-size: 220px 220px;
  will-change: transform;
  animation: grain-shift 900ms steps(4, end) infinite;
}

@keyframes grain-shift {
  0%   { transform: translate3d(0, 0, 0); }
  25%  { transform: translate3d(-3%, 2%, 0); }
  50%  { transform: translate3d(2%, -3%, 0); }
  75%  { transform: translate3d(-2%, -2%, 0); }
  100% { transform: translate3d(0, 0, 0); }
}

/* Ambient sound toggle. Sits beside the language button and reads as
   off until it is switched on — sound that arrives uninvited is worse
   than no sound at all. */
.sound {
  appearance: none;
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  border: 1px solid var(--hairline);
  background: rgba(9, 14, 11, 0.66);
  /* No backdrop-filter: it sits over a canvas that repaints every
     frame, so it would re-blur 60 times a second for the whole session.
     A flat tint over an already-scrimmed scene looks the same. */
  color: var(--bone-dim);
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 0.5em 0.9em;
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.sound:hover { color: var(--bone); border-color: var(--bone-faint); }
.sound[aria-pressed='true'] { color: var(--ochre); border-color: rgba(232, 162, 74, 0.45); }

.sound__bars {
  display: flex;
  align-items: flex-end;
  gap: 2px;
  height: 0.7em;
}
.sound__bars i {
  display: block;
  width: 2px;
  height: 100%;
  background: currentColor;
  transform: scaleY(0.32);
  transform-origin: bottom;
}
.sound[aria-pressed='true'] .sound__bars i {
  animation: sound-bar 1.1s ease-in-out infinite;
}
.sound[aria-pressed='true'] .sound__bars i:nth-child(2) { animation-delay: 0.22s; }
.sound[aria-pressed='true'] .sound__bars i:nth-child(3) { animation-delay: 0.44s; }

/* transform, not height — height animates layout, and this runs
   forever while sound is on. transform-origin is already set. */
@keyframes sound-bar {
  0%, 100% { transform: scaleY(0.30); }
  50%      { transform: scaleY(1); }
}

@media (max-width: 700px) { .sound__label { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .sound[aria-pressed='true'] .sound__bars i { animation: none; transform: scaleY(0.7); }
}

/* Language toggle */
.lang {
  appearance: none;
  border: 1px solid var(--hairline);
  background: rgba(12, 9, 7, 0.4);
  /* No backdrop-filter: it sits over a canvas that repaints every
     frame, so it would re-blur 60 times a second for the whole session.
     A flat tint over an already-scrimmed scene looks the same. */
  color: var(--bone-dim);
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.16em;
  padding: 0.5em 0.85em;
  border-radius: 999px;
  cursor: pointer;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease), background 0.3s var(--ease);
}
.lang:hover { color: var(--bone); border-color: var(--bone-faint); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .grain { animation: none; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}


/* ================================================================== *
   Rules are roots
   ------------------------------------------------------------------
   Every <hr class="rule"> is replaced at runtime by a grown SVG root
   (roots.js). It still occupies exactly the space a 1px rule did, so
   nothing in the surrounding grid moves; the drawing simply overflows
   its own box. Without JS the plain hairline stays.
   ================================================================== */

.rule--root {
  display: block;
  position: relative;
  height: 1px;
  background: none;
  border: 0;
}

.root-rule {
  position: absolute;
  left: 0;
  right: 0;
  /* The spine sits on the baseline the hairline occupied; the offshoots
     hang off it, so the box is centred on that line and overflows. */
  top: 50%;
  height: 3.6rem;
  transform: translateY(-50%);
  overflow: visible;
  pointer-events: none;
}

.root-rule path {
  fill: none;
  stroke: var(--hairline);
  stroke-linecap: round;
  /* The box is stretched to the rule's width, so without this a hairline
     becomes a wedge. */
  vector-effect: non-scaling-stroke;
  stroke-width: 1;
}
.root-rule__branch  { stroke: rgba(238, 224, 198, 0.22); stroke-width: 0.9; }
.root-rule__rootlet { stroke: rgba(238, 224, 198, 0.14); stroke-width: 0.75; }

/* These sit on photographs, not on a flat panel. At plain --hairline the
   offshoots vanished into the picture and the whole thing read as the
   straight rule it replaced. */
.root-rule__spine { stroke: rgba(242, 236, 224, 0.32); }

/* It grows when its section arrives, from the label outward. */
.root-rule path {
  stroke-dasharray: var(--len, 1200);
  stroke-dashoffset: var(--len, 1200);
}
.is-in .root-rule path,
.reveal.is-in .root-rule path {
  animation: root-grow 1.5s var(--ease-out) forwards;
}
.is-in .root-rule__branch  { animation-duration: 1.1s; animation-delay: 0.5s; }
.is-in .root-rule__rootlet { animation-duration: 0.9s; animation-delay: 0.9s; }

@keyframes root-grow { to { stroke-dashoffset: 0; } }

@media (prefers-reduced-motion: reduce) {
  .root-rule path { stroke-dashoffset: 0; animation: none; }
}

/* The forking rule hangs below its line, reaching down toward the four
   movements it introduces. It must never intercept a click on the way. */
.rule--fork { overflow: visible; }
.rule--fork .root-rule {
  top: 0;
  height: 15rem;
  transform: translateY(-1.1rem);
  z-index: 0;
  /* The one root that has to be legible as a diagram — it stands for the
     four movements it reaches down to. */
  filter: drop-shadow(0 0 6px rgba(232, 162, 74, 0.3));
}
.rule--fork .root-rule__spine  { stroke: rgba(240, 214, 166, 0.5); stroke-width: 1.3; }
.rule--fork .root-rule__branch { stroke: rgba(233, 191, 124, 0.42); stroke-width: 1.15; }
.rule--fork .root-rule__rootlet { stroke: rgba(214, 165, 96, 0.26); stroke-width: 0.85; }
@media (max-width: 720px) {
  .rule--fork .root-rule { height: 8rem; }
}


/* The page answers when you touch it — visibly, not only audibly. The
   drum was gated behind sound being on, which meant most visitors got no
   feedback at all on a page whose whole conceit is that it responds. */
.tap {
  position: fixed;
  z-index: 9;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border-radius: 50%;
  border: 1px solid rgba(232, 162, 74, 0.9);
  opacity: 0;
  pointer-events: none;
}
.tap.is-firing { animation: tap-ring 620ms cubic-bezier(0.12, 0.7, 0.2, 1); }
@keyframes tap-ring {
  0%   { opacity: 0.95; transform: scale(0.35); }
  100% { opacity: 0;    transform: scale(7); }
}
@media (prefers-reduced-motion: reduce) { .tap { display: none; } }

/* Registered so they do not inherit — a --par write on a card would
   otherwise invalidate style for the image, title and body inside it —
   and so --rise can be transitioned at all. */
@property --rise { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --par  { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --tilt { syntax: '<angle>';  inherits: false; initial-value: 0deg; }
@property --push { syntax: '<length>'; inherits: false; initial-value: 0px; }
@property --near { syntax: '<number>'; inherits: false; initial-value: 1; }

/* GET ROOTED — landing page.
   The document scrolls; the canvas behind it descends in step. */

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 99;
  padding: 0.75rem 1.25rem;
  background: var(--forest-800);
  border: 1px solid var(--hairline);
}
.skip:focus { left: 1rem; top: 1rem; }

/* ---------------------------------------------------------------- *
 * Chrome
 * ---------------------------------------------------------------- */
.topbar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: clamp(1rem, 2.2vw, 1.9rem) var(--gutter);
  mix-blend-mode: normal;
  /* Retracts when you scroll down, returns the moment you scroll up.
     Without this the header sits on top of whatever chapter happens to
     be passing under it — the vision principles collided with the nav. */
  transition: transform 0.55s var(--ease-out), opacity 0.4s var(--ease);
  will-change: transform;
}

/* A scrim only once the hero is behind you, so the hero itself stays
   completely clean. */
.topbar::before {
  content: '';
  position: absolute;
  inset: -1px 0 auto 0;
  height: 200%;
  background: linear-gradient(180deg, rgba(6, 10, 8, 0.72), transparent);
  opacity: 0;
  transition: opacity 0.6s var(--ease);
  pointer-events: none;
  z-index: -1;
}
[data-past-hero] .topbar::before { opacity: 1; }

[data-nav="hidden"] .topbar {
  transform: translateY(-115%);
  /* Off-screen is not enough: without these the nav links and toggles
     stay in the tab order and focus jumps to something nobody can see.
     Fading as well as sliding also made the return read as a dissolve
     rather than a slide, so the opacity change is gone. */
  visibility: hidden;
  pointer-events: none;
}

/* Tabbing into it always brings it back. */
.topbar:focus-within {
  transform: none;
  visibility: visible;
  opacity: 1;
}

.wordmark {
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.3em;
  text-transform: uppercase;
  color: var(--bone);
  text-decoration: none;
}

.topnav {
  display: flex;
  gap: clamp(1rem, 2.4vw, 2.4rem);
  margin-left: auto;
  margin-right: clamp(1rem, 2.4vw, 2.4rem);
}
.topnav a {
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--bone-dim);
  text-decoration: none;
  padding-bottom: 2px;
  border-bottom: 1px solid transparent;
  transition: color 0.35s var(--ease), border-color 0.35s var(--ease);
}
.topnav a:hover, .topnav a[aria-current='true'] {
  color: var(--bone);
  border-bottom-color: var(--ochre);
}
@media (max-width: 700px) { .topnav { display: none; } }

/* Depth read-out. The page's most literal spatial cue: it tells you how
   far underground the camera currently is. */
/* The night, as a timeline.
 *
 * It sits over photographs, so it carries its own backdrop rather than
 * relying on the scrim, and every label is full-contrast: the previous
 * version put the phase name at 58% alpha in the smallest size in the
 * scale and it simply could not be read. */
.clock {
  position: fixed;
  left: var(--gutter);
  bottom: clamp(1.25rem, 3vw, 2.5rem);
  z-index: 10;
  display: grid;
  grid-template-columns: auto auto;
  grid-template-areas: 'now phases';
  align-items: center;
  gap: 0 clamp(0.9rem, 1.6vw, 1.5rem);
  padding: 0.85rem 1.15rem 0.85rem 0;
  /* Deliberately not a card. A bordered, blurred panel over full-bleed
     chapters cuts a hard rectangle through whatever copy is passing
     underneath — it sliced the third vision principle in half. A soft
     corner scrim lets type fade under it instead of colliding, and drops
     a full-screen backdrop-filter at the same time. */
  pointer-events: none;
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 0.8s var(--ease), transform 0.8s var(--ease);
}
.clock.is-ready { opacity: 1; transform: none; }

.clock::before {
  content: '';
  position: fixed;
  left: 0;
  bottom: 0;
  width: min(46rem, 70vw);
  height: 15rem;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(80% 100% at 0% 100%, rgba(6, 10, 8, 0.9), rgba(6, 10, 8, 0.55) 42%, transparent 72%);
}

.clock__now {
  grid-area: now;
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
  padding-right: clamp(0.9rem, 1.6vw, 1.5rem);
  border-right: 1px solid var(--hairline);
}
.clock__value {
  font-family: var(--mono);
  font-size: var(--step-2);
  font-weight: 500;
  letter-spacing: -0.01em;
  color: var(--bone);
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.clock__zone {
  font-family: var(--mono);
  font-size: var(--step--1);
  font-weight: 500;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ochre);
  line-height: 1;
}

/* The four phases of the night, so you can see the whole arc and where in
   it you currently are — not just a number with no frame of reference. */
.clock__phases {
  grid-area: phases;
  display: flex;
  gap: clamp(0.7rem, 1.3vw, 1.2rem);
  margin: 0;
  padding: 0;
  list-style: none;
}
.clock__phase {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--bone-faint);
  white-space: nowrap;
  transition: color 0.5s var(--ease);
}
.clock__phase.is-on { color: var(--bone); }
.clock__tick {
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--bone-faint);
  transition: background 0.5s var(--ease), box-shadow 0.5s var(--ease);
}
.clock__phase.is-on .clock__tick {
  background: var(--ochre);
  box-shadow: 0 0 10px 2px rgba(232, 162, 74, 0.55);
}

/* Progress hairline along the bottom edge of the panel. */
.clock__track {
  grid-column: 1 / -1;
  position: relative;
  height: 2px;
  margin-top: 0.85rem;
  background: rgba(242, 236, 224, 0.14);
  overflow: hidden;
}
.clock__thumb {
  position: absolute;
  inset: 0 auto 0 0;
  width: 100%;
  transform-origin: left;
  transform: scaleX(0);
  background: linear-gradient(to right, rgba(232, 162, 74, 0.35), var(--ochre));
}

/* On a phone the four phase names will not fit; the clock and the current
   phase still do, and that is the part that was missing entirely before —
   the old build hid this whole component below 700px. */
@media (max-width: 860px) {
  .clock__phases { display: none; }
  .clock {
    grid-template-columns: auto;
    grid-template-areas: 'now';
    padding: 0.7rem 0.95rem;
  }
  .clock__now { border-right: 0; padding-right: 0; }
  .clock__value { font-size: var(--step-1); }

  /* The clock is fixed over full-bleed chapters, so on a narrow screen it
     landed directly on top of body copy — the manifesto paragraph and the
     third vision principle both had it sitting in the middle of a line.
     Chapters keep clear of it, and anything that still passes beneath is
     dimmed by a scrim rather than colliding at full contrast. */
  .chapter { padding-bottom: clamp(9rem, 24vh, 15rem); }
}

.clock::after {
  content: '';
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  height: 9rem;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(0deg, rgba(6, 10, 8, 0.8), transparent);
  transition: opacity 0.8s var(--ease);
}
.clock.is-ready::after { opacity: 1; }

@media (min-width: 861px) {
  /* On a wide screen the clock is a small object in a corner and the
     scrim would be a band across the whole page for no reason. */
  .clock::after { display: none; }
}

/* ---------------------------------------------------------------- *
 * Layout
 * ---------------------------------------------------------------- */
main {
  position: relative;
  z-index: 3;
}

.chapter {
  position: relative;
  padding: clamp(7rem, 20vh, 16rem) var(--gutter);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  column-gap: clamp(1rem, 2vw, 2rem);
  align-content: center;
}

/* Reveal.
   `transform` is NEVER transitioned. It carries two things: the reveal
   offset (--rise, animated) and the scroll-linked drift (--par, which
   must be instant). Transitioning the transform meant --par was rewritten
   into a 1.1s ease 60 times a second, so every drifting element trailed
   the scroll by about a second — depth that lags does not read as depth,
   it reads as mush. Animating --rise instead separates them cleanly.

   The previous attempt at this used a transitionend listener to drop the
   transition afterwards; it never fired, because `once: true` was spent
   on the opacity event, which always lands first. */
.reveal {
  --rise: 22px;
  opacity: 0;
  transform: translate3d(0, calc(var(--rise) + var(--par, 0px)), 0);
  transition:
    opacity var(--t, 1.1s) var(--reveal-ease, var(--ease-out)),
    --rise var(--t, 1.1s) var(--reveal-ease, var(--ease-out));
  transition-delay: var(--delay, 0ms);
}
.is-in .reveal, .reveal.is-in { opacity: 1; --rise: 0px; }

/* Three tempos, not one. A mono label and a display line arriving on the
   same 1.1s curve is what made every section read identically. */
.section-head        { --t: 460ms; --rise: 10px; }
.hero__meta > *,
.intro__body,
.djs__body,
.outro__body         { --t: 720ms; --rise: 16px; --reveal-ease: cubic-bezier(0.33, 0.9, 0.28, 1); }
.intro__lead,
.djs__quote,
.outro__line,
.scroll-well__hook   { --t: 1150ms; --rise: 30px; }

@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; }
}

/* ---------------------------------------------------------------- *
 * Hero
 * ---------------------------------------------------------------- */
.hero {
  min-height: 100vh;
  min-height: 100svh;
  padding-top: clamp(7rem, 16vh, 12rem);
  padding-bottom: clamp(4rem, 8vh, 7rem);
  align-content: space-between;
  row-gap: 3rem;
}

.hero__type { grid-column: 1 / -1; }

.hero h1 {
  font-size: var(--step-5);
  font-variation-settings: 'SOFT' 30, 'WONK' 1, 'opsz' 144;
  font-weight: 300;
  line-height: 0.84;
  letter-spacing: -0.035em;
  margin: 0;
}
.hero h1 .line { display: block; overflow: hidden; padding-bottom: 0.06em; }
.hero h1 .line--indent { margin-left: 0.12em; }
.hero h1 .line i {
  display: block;
  font-style: inherit;
  transform: translateY(105%);
  transition: transform 1.5s var(--ease-out);
}
.hero h1 .line:nth-child(2) i { transition-delay: 0.14s; }
/* Held until the opening curtain lifts, so the wordmark rises into a
   photograph that is already there rather than into a black rectangle. */
html[data-entered] .hero h1 .line i { transform: translateY(0); }

@media (prefers-reduced-motion: reduce) {
  .hero h1 .line i { transform: none; transition: none; }
}

.hero__tagline {
  grid-column: 1 / -1;
  margin-top: clamp(1.2rem, 3vh, 2.4rem);
  font-family: var(--display);
  font-style: italic;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 40;
  font-size: var(--step-2);
  font-weight: 300;
  line-height: 1.14;
  color: var(--bone-dim);
  max-width: 22ch;
}

.hero__meta {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.6rem clamp(1.5rem, 5vw, 5rem);
  padding-top: 1.1rem;
  border-top: 1px solid var(--hairline);
}
.hero__meta > * { flex: 0 1 auto; }
.hero__meta .place-note { color: var(--bone-faint); text-transform: none; letter-spacing: 0.06em; }
.hero__scroll { margin-left: auto; display: flex; align-items: center; gap: 0.6rem; }
.hero__scroll::after {
  content: '';
  width: 1.6rem;
  height: 1px;
  background: var(--ochre);
  transform-origin: left;
  animation: draw 2.6s var(--ease) infinite;
}
@keyframes draw {
  0%, 100% { transform: scaleX(0.2); opacity: 0.4; }
  50%      { transform: scaleX(1); opacity: 1; }
}
@media (max-width: 700px) { .hero__scroll { margin-left: 0; width: 100%; } }

/* ---------------------------------------------------------------- *
 * Section headers
 * ---------------------------------------------------------------- */
.section-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  gap: 1.2rem;
  margin-bottom: clamp(3rem, 9vh, 7rem);
}
.section-head .rule { flex: 1; align-self: center; }
.section-head__kicker {
  font-family: var(--display);
  font-style: italic;
  font-size: var(--step-0);
  color: var(--bone-faint);
  flex: 0 0 auto;
}
@media (max-width: 900px) { .section-head__kicker { display: none; } }

/* ---------------------------------------------------------------- *
 * Intro
 * ---------------------------------------------------------------- */
.intro__lead {
  grid-column: 2 / span 10;
  font-family: var(--display);
  font-size: var(--step-3);
  font-weight: 300;
  font-variation-settings: 'SOFT' 50, 'WONK' 1, 'opsz' 90;
  line-height: 1.08;
  letter-spacing: -0.02em;
  text-wrap: balance;
}
.intro__body {
  grid-column: 7 / span 5;
  margin-top: clamp(2rem, 5vh, 3.5rem);
  color: var(--bone-dim);
  max-width: var(--measure);
}
@media (max-width: 900px) {
  .intro__lead { grid-column: 1 / -1; }
  .intro__body { grid-column: 1 / -1; }
}

/* ---------------------------------------------------------------- *
 * Vision — three principles, alternating across the grid
 * ---------------------------------------------------------------- */
.principle {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  padding: clamp(2.5rem, 7vh, 5rem) 0;
  align-items: start;
}
/* Separators BETWEEN principles only. A border on the first stacked a
   second line directly under the section head's own rule, which read as
   an accident rather than as structure. */
.principle + .principle { border-top: 1px solid var(--hairline); }
.principle:last-of-type { border-bottom: 1px solid var(--hairline); }

.principle__num {
  grid-column: 1 / span 2;
  font-family: var(--display);
  font-size: var(--step-2);
  font-weight: 300;
  font-variation-settings: 'SOFT' 60, 'WONK' 1, 'opsz' 60;
  font-style: italic;
  color: var(--ochre);
  line-height: 1;
}
.principle__title {
  grid-column: 3 / span 5;
  font-size: var(--step-3);
  font-weight: 300;
  font-variation-settings: 'SOFT' 45, 'WONK' 1, 'opsz' 80;
}
.principle__body {
  grid-column: 9 / span 4;
  color: var(--bone-dim);
  max-width: var(--measure);
}
@media (max-width: 900px) {
  .principle { display: block; }
  .principle__num { display: block; margin-bottom: 0.6rem; }
  .principle__title { margin-bottom: 1.1rem; }
  .principle__body { max-width: none; }
}

/* ---------------------------------------------------------------- *
 * Experience — a rail of four cards, tilted in real perspective
 * ---------------------------------------------------------------- */
/* The experience rail.
 *
 * A tall track gives the section extra scroll length; the viewport inside
 * it sticks, and the cards are translated sideways from the page's own
 * scroll position. Four movements, travelled through rather than swiped —
 * which is the beat the WebGL version had and the static grid lost. */
.rail-track {
  grid-column: 1 / -1;
  position: relative;
  /* Scroll length. Pinned time is this minus one viewport, and the
     traverse is spread across exactly that — so any extra here is a dead
     screen at the end where the cards have already arrived and nothing
     further happens. Sized so the last card lands as the section ends. */
  height: 250vh;
  margin-block: clamp(-4rem, -6vh, -2rem) 0;
}

.rail-sticky {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(1.5rem, 4vh, 3rem);
  overflow: hidden;
  margin-inline: calc(var(--gutter) * -1);
  padding-inline: var(--gutter);
}

/* This chapter ends on the sticky viewport, which already carries a
   screen's worth of its own breathing room — the usual chapter padding
   on top of that reads as a hole in the page. */
#experience { padding-bottom: clamp(2rem, 4vh, 3.5rem); }

.rail {
  display: flex;
  gap: clamp(1rem, 2.5vw, 2.25rem);
  perspective: 1500px;
  perspective-origin: 50% 45%;
  will-change: transform;
}

.phase {
  /* Wide enough that four never fit at once — the row has to overflow or
     there is no distance for the scroll to travel through. */
  flex: 0 0 clamp(17rem, 36vw, 31rem);
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  gap: 1rem;
  padding: clamp(1.6rem, 3vw, 2.4rem);
  min-height: clamp(19rem, 42vh, 27rem);
  border: 1px solid var(--hairline);
  transform-style: preserve-3d;
  transform: rotateY(var(--tilt, 0deg)) translateZ(var(--push, 0px)) scale(var(--near, 1));
  transition: border-color 0.5s var(--ease);
  will-change: transform;
}
.phase.is-near { border-color: var(--bone-faint); }

.phase__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
  filter: saturate(0.9) brightness(0.66);
  transform: scale(1.06);
  transition: filter 0.6s var(--ease), transform 0.6s var(--ease);
}
/* The card nearest the centre lifts out of the row — it is the one being
   read, so it gets its photograph back. */
.phase.is-near .phase__img { filter: saturate(1) brightness(0.86); transform: scale(1.01); }

/* Gradient over the photograph, weighted to the bottom where the title
   and body sit. Without it the card text competes with forest detail. */
.phase::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top,
    rgba(9, 14, 11, 0.94) 0%,
    rgba(9, 14, 11, 0.72) 38%,
    rgba(9, 14, 11, 0.30) 70%,
    rgba(9, 14, 11, 0.22) 100%);
}
.phase > *:not(.phase__img) { position: relative; z-index: 2; }

.phase__num {
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.24em;
  color: var(--ochre);
}
/* The copy performs the traverse too, not just the photograph: it sits
   back and dim until its card reaches the middle, then settles. */
.phase__title, .phase__body {
  opacity: 0.5;
  transform: translate3d(0, 7px, 0);
  transition: opacity 0.55s var(--ease), transform 0.55s var(--ease);
}
.phase.is-near .phase__title,
.phase.is-near .phase__body { opacity: 1; transform: none; }

.phase__title {
  font-size: var(--step-2);
  font-weight: 300;
  font-variation-settings: 'SOFT' 45, 'WONK' 1, 'opsz' 60;
  margin-top: auto;
}
.phase__body {
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.08em;
  color: var(--bone-dim);
  line-height: 1.7;
}

.rail-hint {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  border-top: 1px solid var(--hairline);
  padding-top: 1rem;
}
.rail-progress { display: flex; gap: 0.4rem; }
.rail-progress span {
  width: clamp(1.6rem, 3vw, 2.6rem);
  height: 2px;
  background: var(--hairline);
  transition: background 0.45s var(--ease);
}
.rail-progress span.is-on { background: var(--ochre); }

/* Without JS, or with reduced motion, the track collapses back to an
   ordinary horizontally-scrollable row. */
@media (prefers-reduced-motion: reduce) {
  .rail-track { height: auto; margin-block: 0; }
  .rail-sticky { position: static; height: auto; overflow-x: auto; }
  .rail { transform: none !important; }
}

/* ---------------------------------------------------------------- *
 * For the DJs
 * ---------------------------------------------------------------- */
.djs__body {
  grid-column: 1 / span 6;
  font-size: var(--step-1);
  color: var(--bone-dim);
  max-width: 40ch;
}
.djs__quote {
  grid-column: 7 / span 6;
  display: flex;
  flex-direction: column;
  gap: clamp(0.4rem, 1.4vh, 1rem);
  margin: 0;
  padding-left: clamp(1rem, 3vw, 3rem);
  border-left: 1px solid var(--hairline);
}
.djs__quote span {
  font-family: var(--display);
  font-style: italic;
  font-size: var(--step-3);
  font-weight: 300;
  font-variation-settings: 'SOFT' 70, 'WONK' 1, 'opsz' 90;
  line-height: 1.02;
  letter-spacing: -0.02em;
  color: var(--bone);
}
.djs__quote span:last-child { color: var(--ochre); }
@media (max-width: 900px) {
  .djs__body, .djs__quote { grid-column: 1 / -1; }
  .djs__quote { margin-top: 2.5rem; padding-left: 1.25rem; }
}

/* ---------------------------------------------------------------- *
 * Outro
 * ---------------------------------------------------------------- */
.outro {
  min-height: 92vh;
  min-height: 92svh;
  align-content: center;
  justify-items: center;
  text-align: center;
}
.outro__line {
  grid-column: 1 / -1;
  font-size: var(--step-4);
  font-weight: 300;
  font-variation-settings: 'SOFT' 30, 'WONK' 1, 'opsz' 144;
  letter-spacing: -0.03em;
}
.outro__body {
  grid-column: 1 / -1;
  margin-top: 1.4rem;
  color: var(--bone-dim);
}
.outro__lockup {
  grid-column: 1 / -1;
  margin-top: clamp(3rem, 8vh, 6rem);
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem clamp(1.5rem, 4vw, 3rem);
}

.site-foot {
  position: relative;
  z-index: 3;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.75rem 2rem;
  padding: 1.6rem var(--gutter) clamp(1.6rem, 4vh, 3rem);
  border-top: 1px solid var(--hairline);
  background: linear-gradient(to top, rgba(9, 14, 11, 0.85), transparent);
}

/* ================================================================== *
   The manifesto — a scroll you unroll
   ------------------------------------------------------------------
   A <details> so it works, and is announced, with no JavaScript. The
   height animation needs JS (a <details> snaps open natively), so
   site.js intercepts the toggle and keeps the element open until the
   closing animation has finished.

   The paper is cream on a page that is otherwise entirely dark — it is
   the one bright object on the site, which is the point: you found
   something. It also matches the printed manifesto.
   ================================================================== */

.scroll-well {
  grid-column: 1 / -1;   /* the chapter is a 12-column grid */
  display: grid;
  justify-items: center;
  gap: clamp(0.75rem, 2vh, 1.4rem);
  padding: 0;
}

/* This chapter is one object, not a column of copy. The standard chapter
   padding left the rod floating in a screen of empty dark. */
#manifesto { padding-block: clamp(5rem, 14vh, 10rem); }

.scroll-well__label { color: var(--bone-faint); }

.scroll-well__hook {
  font-family: var(--display);
  font-weight: 300;
  font-size: var(--step-3);
  line-height: 1.06;
  letter-spacing: -0.02em;
  text-align: center;
  text-wrap: balance;
  max-width: 18ch;
  margin: 0 0 clamp(1.25rem, 4vh, 2.5rem);
}

.scroll {
  position: relative;
  width: min(62ch, 100%);
}

/* ---- the roll ---------------------------------------------------- */

.scroll__roll {
  display: grid;
  justify-items: center;
  gap: 0.9rem;
  cursor: pointer;
  list-style: none;
  padding: 0.35rem 0 0;
  position: relative;
  z-index: 3; /* the rod sits over the paper's tucked top edge */
}
.scroll__roll::-webkit-details-marker { display: none; }

/* A lit cylinder: dark at the edges, catching light along the middle. */
.scroll__roll-body {
  display: block;
  position: relative;
  width: 100%;
  height: 30px;
  border-radius: 15px / 15px;
  background:
    linear-gradient(180deg,
      #241d15 0%, #4b3c27 16%, #8a6f47 40%, #a98a5c 50%,
      #7d6440 62%, #3d3121 84%, #1d1710 100%);
  box-shadow:
    0 14px 30px rgba(0, 0, 0, 0.5),
    inset 0 1px 0 rgba(255, 225, 175, 0.28),
    inset 0 -1px 0 rgba(0, 0, 0, 0.5);
  transition: transform 0.5s var(--ease-out), box-shadow 0.5s var(--ease);
  animation: rod-breathe 5.5s var(--ease) infinite;
}

/* Closed, this is the only interactive object on the screen. A slow
   breath is what tells you so without a label shouting it. */
@keyframes rod-breathe {
  0%, 100% { box-shadow: 0 14px 30px rgba(0,0,0,0.5), 0 0 0 rgba(232,162,74,0), inset 0 1px 0 rgba(255,225,175,0.28), inset 0 -1px 0 rgba(0,0,0,0.5); }
  50%      { box-shadow: 0 14px 30px rgba(0,0,0,0.5), 0 0 26px rgba(232,162,74,0.26), inset 0 1px 0 rgba(255,225,175,0.42), inset 0 -1px 0 rgba(0,0,0,0.5); }
}
.scroll[open] .scroll__roll-body { animation: none; }
@media (prefers-reduced-motion: reduce) {
  .scroll__roll-body { animation: none; }
}

/* End caps, so it reads as a rod rather than a bar. */
.scroll__roll-body::before,
.scroll__roll-body::after {
  content: '';
  position: absolute;
  top: 0;
  width: 13px;
  height: 30px;
  border-radius: 50%;
  background: linear-gradient(180deg, #5c4a30, #241d15);
  box-shadow: inset 0 1px 0 rgba(255, 225, 175, 0.22);
}
.scroll__roll-body::before { left: -6px; }
.scroll__roll-body::after { right: -6px; }

.scroll__roll:hover .scroll__roll-body,
.scroll__roll:focus-visible .scroll__roll-body {
  transform: scaleY(1.09);
  box-shadow:
    0 18px 40px rgba(0, 0, 0, 0.55),
    0 0 34px rgba(232, 162, 74, 0.22),
    inset 0 1px 0 rgba(255, 225, 175, 0.4);
}
.scroll__roll:focus-visible { outline: 1px solid var(--ochre); outline-offset: 10px; }

.scroll__hint {
  /* Under the rod, and out of the flow, so the paper can still tuck
     straight under the rod when it unrolls. */
  position: absolute;
  top: calc(100% + 0.55rem);
  left: 0;
  right: 0;
  text-align: center;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--bone-faint);
  transition: color 0.4s var(--ease);
}
.scroll__roll:hover .scroll__hint { color: var(--ochre); }

/* ---- the paper --------------------------------------------------- */

.scroll__sheet {
  position: relative;
  overflow: hidden;
  height: 0;
}

.scroll__paper {
  position: relative;
  margin-top: -14px; /* tuck the top edge under the rod */
  padding: clamp(2.75rem, 7vw, 4.5rem) clamp(1.75rem, 6vw, 4rem) clamp(2.5rem, 6vw, 4rem);
  color: #2a2318;
  text-align: center;
  background:
    radial-gradient(120% 60% at 50% 0%, rgba(120, 92, 48, 0.16), transparent 60%),
    radial-gradient(120% 60% at 50% 100%, rgba(120, 92, 48, 0.16), transparent 60%),
    linear-gradient(178deg, #efe6d2 0%, #e6dbc2 45%, #ded1b4 100%);
  box-shadow:
    inset 0 14px 22px -14px rgba(60, 44, 22, 0.65),
    inset 0 -14px 22px -14px rgba(60, 44, 22, 0.5),
    0 30px 60px rgba(0, 0, 0, 0.45);
}

/* The same grain as the rest of the site, so the paper belongs to the
   photograph rather than sitting on top of it. */
.scroll__paper::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: var(--grain-src);
  opacity: 0.5;
  mix-blend-mode: multiply;
  pointer-events: none;
}


.scroll__para {
  font-size: var(--step-0);
  line-height: 1.72;
  max-width: 46ch;
  margin: 0 auto clamp(1.1rem, 3vh, 1.75rem);
  color: #3a3125;
  text-wrap: pretty;
}

.scroll__sign {
  font-family: var(--display);
  font-size: var(--step-1);
  letter-spacing: 0.02em;
  margin: clamp(1.5rem, 4vh, 2.5rem) 0 0;
  color: #6b5330;
}

/* The bottom rod, which travels down as the paper is revealed. */
.scroll__foot {
  display: block;
  width: 100%;
  height: 26px;
  border-radius: 13px;
  background: linear-gradient(180deg,
    #241d15 0%, #4b3c27 18%, #86693f 46%, #6d5637 66%, #1d1710 100%);
  box-shadow: 0 16px 34px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 225, 175, 0.22);
}

/* ---- roots down both edges --------------------------------------- */

.scroll__vine {
  position: absolute;
  /* Starts under the rod and follows the sheet down, because .scroll's
     own height grows with it. */
  top: 4.6rem;
  bottom: 0;
  width: 8.5rem;
  pointer-events: none;
  z-index: 2;
  filter: drop-shadow(0 0 7px rgba(232, 162, 74, 0.4));
  opacity: 0;
  transition: opacity 0.5s var(--ease);
}
.scroll.is-open .scroll__vine { opacity: 1; }
.scroll__vine--l { left: -5.2rem; }
.scroll__vine--r { right: -5.2rem; transform: scaleX(-1); }

/* There is no room outside the paper on a phone — the vines were being
   clipped away to nothing and reported 153px of (clipped) overflow. */
@media (max-width: 720px) {
  .scroll__vine { width: 4.5rem; opacity: 0.55; }
  .scroll__vine--l { left: -1rem; }
  .scroll__vine--r { right: -1rem; }
}
/* The base rule centres itself on its line with a translate; a vine
   fills its box instead, so that has to be undone or it hangs half a
   sheet too high. */
.scroll__vine .root-rule { position: absolute; inset: 0; height: 100%; transform: none; }
/* These are the roots holding the scroll, not a hairline — they carry
   real weight and a warm glow, unlike the rules elsewhere. */
.scroll__vine path { stroke: rgba(233, 191, 124, 0.8); stroke-width: 1.9; }
.scroll__vine .root-rule__branch  { stroke: rgba(226, 178, 108, 0.55); stroke-width: 1.35; }
.scroll__vine .root-rule__rootlet { stroke: rgba(214, 165, 96, 0.34); stroke-width: 0.95; }

/* ---- open / closed ----------------------------------------------- */

/* Open, the space under the rod belongs to the paper — so the label that
   closes it moves above the rod rather than disappearing. */
.scroll[open] .scroll__hint {
  color: var(--ochre);
  top: auto;
  bottom: calc(100% + 0.55rem);
}

/* Paper contents settle in behind the unrolling, not with it. */
.scroll__paper > * {
  opacity: 0;
  transform: translateY(0.9rem);
  transition: opacity 0.7s var(--ease-out), transform 0.7s var(--ease-out);
}
.scroll.is-open .scroll__paper > * { opacity: 1; transform: none; }
.scroll.is-open .scroll__para:nth-of-type(1) { transition-delay: 0.44s; }
.scroll.is-open .scroll__para:nth-of-type(2) { transition-delay: 0.56s; }
.scroll.is-open .scroll__sign { transition-delay: 0.7s; }

@media (prefers-reduced-motion: reduce) {
  /* NOT `transition: none` — an !important author rule outranks the
     inline style JS sets, so no transition runs, transitionend never
     fires, and the open/close state machine deadlocks with the scroll
     stuck open and the header dimmed for the rest of the session. */
  .scroll__sheet { transition-duration: 1ms !important; }
  .scroll__paper > * { transition-duration: 1ms; transform: none; }
}

/* The vines belong to the scroll's own state. Without this they would
   draw as soon as the section revealed, while still rolled up. */
.scroll__vine .root-rule path { animation: none; stroke-dashoffset: var(--len, 1200); }
.scroll.is-open .scroll__vine .root-rule path {
  animation: root-grow 1.8s var(--ease-out) forwards;
}
.scroll.is-open .scroll__vine--r .root-rule path { animation-delay: 0.18s; }

/* Reading the manifesto: everything else steps back. */
[data-reading] .clock { opacity: 0.18; }
[data-reading] .topbar { opacity: 0.3; }
[data-reading] .clock::before { opacity: 0; }
[data-reading] .clock,
[data-reading] .topbar { transition: opacity 0.7s var(--ease); }


/* The bottom rod is a physical object landing. This is the only overshoot
   on the site — springs anywhere else would break the register. */
.scroll.is-open .scroll__foot {
  animation: rod-settle 620ms cubic-bezier(0.20, 1.50, 0.40, 1) 980ms both;
}
@keyframes rod-settle { from { transform: translateY(-9px); } to { transform: none; } }
@media (prefers-reduced-motion: reduce) { .scroll.is-open .scroll__foot { animation: none; } }

/* The hero's scroll hint loops forever, eight screens after anyone has
   stopped looking at it. */
[data-scrolled] .hero__scroll { opacity: 0; transition: opacity 500ms var(--ease); }
[data-scrolled] .hero__scroll::after { animation: none; }

/* Directional wipe rather than a colour fade. */
.topnav a { position: relative; }
.topnav a::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -2px;
  height: 1px;
  background: var(--ochre);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 320ms var(--ease-out);
}
.topnav a:hover::after, .topnav a:focus-visible::after { transform: scaleX(1); transform-origin: left; }

/* Nothing on this page gave when pressed. */
.gate__enter:active, .sound:active, .lang:active { transform: translateY(1px) scale(0.985); transition-duration: 90ms; }
.scroll__roll:active .scroll__roll-body { transform: translateY(2px) scaleY(1.03); }

/* The four cards had no hover state at all. */
.phase:hover::after { opacity: 0.82; }
.phase::after { transition: opacity 300ms var(--ease); }


/* ================================================================== *
   Pace, palette and composition
   ================================================================== */

/* Chapters no longer all breathe the same. The hero already ends on air,
   so #intro does not need another 16rem before it — that pairing was up
   to 23rem of nothing at the exact moment the page asks you to scroll. */
.chapter   { padding-block: var(--pace-m); }
#intro     { padding-top: var(--pace-s); }
#manifesto { padding-block: var(--pace-l) var(--pace-s); }
#vision    { padding-top: var(--pace-s); }
.outro     { padding-top: var(--pace-l); }

/* The palette travels with the night. Every accent on the page was the
   same warm amber from 17:20 to 06:10, on a site whose entire premise is
   that the light changes. :focus-visible deliberately stays literal
   --ochre so the focus ring never changes meaning. */
#manifesto, #vision { --accent: #9db6c9; }
#experience         { --accent: #7f97ad; }
#djs                { --accent: var(--ember); }
.outro              { --accent: #cfd8dd; }

.principle__num,
.phase__num,
.rail-progress span.is-on,
.djs__quote span:last-child { color: var(--accent); }

/* Asymmetry. The intro was two full-width bands; now the body hangs off
   the right and sits to the baseline of the lead. */
.intro__lead { grid-column: 1 / span 8; }
.intro__body { grid-column: 10 / span 3; align-self: end; }

/* The three principles said "alternating" in a comment and did not.
   Three identical rows between full-width hairlines read as a table. */
.principle:nth-of-type(2) .principle__num   { grid-column: 12 / span 1; text-align: right; }
.principle:nth-of-type(2) .principle__title { grid-column: 6 / span 6;  text-align: right; }
.principle:nth-of-type(2) .principle__body  { grid-column: 2 / span 4;  margin-left: auto; }
.principle:nth-of-type(3) .principle__title { grid-column: 4 / span 5; }
.principle:nth-of-type(3) .principle__body  { grid-column: 9 / span 4; margin-top: clamp(3rem, 7vh, 6rem); }
.principle + .principle, .principle:last-of-type { border: 0; }

/* The roman numerals were the best unused asset here — set smaller than
   the heading they number. Now they are the separator. */
.principle__num {
  /* step-5 is the hero's size — at that scale the third numeral ran into
     the clock and the first was cut by the header. */
  font-size: var(--step-4);
  font-weight: 100;
  line-height: 0.8;
  color: rgba(157, 182, 201, 0.16);
  -webkit-text-stroke: 1.4px var(--accent);
}
/* The last principle has the clock in its corner. */
.principle:last-of-type { padding-bottom: clamp(4rem, 12vh, 8rem); }

/* Optical sizing. The subset defaults to opsz 9 — a caption cut — so any
   display element that does not set it was rendering caption letterforms
   at 20-60px. */
.scroll-well__hook   { font-variation-settings: 'opsz' 48; }
.scroll__sign        { font-variation-settings: 'opsz' 24; }
.section-head__kicker { font-variation-settings: 'opsz' 18; }

/* The two facts anyone came for were set at 13px, the same size as
   "scroll to begin". */
.hero__meta > [data-i18n="dates"],
.hero__meta > [data-i18n="place"] {
  font-size: var(--step-1);
  letter-spacing: 0.12em;
  color: var(--bone);
}

/* Labels inherited body leading — at 12px uppercase with wide tracking
   that is over twice the cap height, and it shows on every wrap. */
.label { line-height: 1.3; }

/* The vines started clipping at ~840px, not 720 — the space they need is
   exactly the gutter, so tie them to it and drop the breakpoint. */
.scroll__vine--l { left: calc(var(--gutter) * -0.9); }
.scroll__vine--r { right: calc(var(--gutter) * -0.9); }

/* Kickers are content, not decoration — hiding them left #vision opening
   on a bare label. */
.section-head { flex-wrap: wrap; }
@media (max-width: 900px) {
  .section-head__kicker { display: block; flex-basis: 100%; margin-top: 0.5rem; }
}

@media (max-width: 900px) {
  /* Stacked, the numeral sits directly above its title and a step-4
     glyph lands straight on the clock. */
  .principle__num { font-size: var(--step-3); -webkit-text-stroke-width: 1.1px; }
  .principle:nth-of-type(2) .principle__title,
  .principle:nth-of-type(2) .principle__num { text-align: left; }
  .principle:nth-of-type(2) .principle__body { margin-left: 0; }
  .principle:nth-of-type(3) .principle__body { margin-top: 0; }
}
