/* ═══════════════════════════════════════════════════════════════════════════
   Ephemeris — the solar system, computed

   The design thesis, so it can be argued with:

   · IT IS AN INSTRUMENT, NOT A POSTER. Space sites default to a black page, a
     glowing planet and a lot of glass panels with blur behind them. This one is
     built as an observatory logbook: hairlines, corner ticks, tabular figures,
     a live readout strip pinned to the foot of the screen. There is not one
     rounded rectangle with a background fill in this file.

   · THE ACCENT IS THE PLANET. There is no house colour. --accent is rewritten
     as you travel — rust at Mars, cyan at Uranus, cream at Saturn — and every
     rule, folio, active state and readout label follows it. The page changes
     temperature as you cross the solar system, which is one variable doing the
     work of an entire theming system.

   · MONOSPACE ONLY WHERE FIGURES ARE TABULAR. Wide-tracked uppercase mono
     sprayed over every label is the loudest tic in machine-made interfaces.
     Here it appears in exactly three places, all of them numeric: the readout
     strip, the data tables and the folios. The prose is a humanist sans and the
     display is a serif.

   · THE GROUND IS NOT BLACK. #05060b — ink with a blue cast. True black kills
     the bloom falloff and makes the vignette a visible ring; a near-black with
     a hue gives the render somewhere to sit.

   · NOTHING IS CENTRED EXCEPT THE THINGS THAT SHOULD BE. Each dossier takes one
     side of the screen and tells the camera to put the planet on the other. The
     asymmetry is functional: it is how the writing and the subject stop
     fighting for the middle.
   ═══════════════════════════════════════════════════════════════════════════ */

:root {
  --ink:        #05060b;
  --ink-2:      #0a0c14;
  --paper:      #ece8e0;   /* type on ink, so "paper" is the foreground here */
  --paper-2:    #a7a49e;
  --paper-3:    #6d6b67;
  --rule:       rgba(236, 232, 224, 0.16);
  --rule-soft:  rgba(236, 232, 224, 0.075);

  /* Rewritten by ui.js as the tour advances. The default is the Sun. */
  --accent:     #ffb547;
  --accent-dim: rgba(255, 181, 71, 0.34);

  --pad: clamp(1.15rem, 4.2vw, 4rem);
  --maxw: 88rem;
  --readout-h: 3.1rem;

  --display: "Instrument Serif", Georgia, "Times New Roman", serif;
  --sans: "IBM Plex Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --ease: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-io: cubic-bezier(0.76, 0, 0.24, 1);
  --tint: 0.9s var(--ease);
}

/* ── Reset ──────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }

html {
  background: var(--ink);
  -webkit-text-size-adjust: 100%;
  scroll-behavior: auto;
  /* clip, not hidden, and on html rather than body. `overflow-x: hidden` makes
     the element a scroll container even on the axis that is hidden, which is
     enough to stop a scroll library seeing the document scroll at all. `clip`
     does the same visual job without creating one. Nothing here overflows
     sideways anyway; this is a guard, not a fix for a layout. */
  overflow-x: clip;
}
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }

body {
  font-family: var(--sans);
  font-weight: 300;
  background: var(--ink);
  color: var(--paper);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg { display: block; max-width: 100%; }
button, input { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }

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

::selection { background: var(--accent); color: var(--ink); }

/* ── The canvas sits behind everything and never scrolls ────────────────── */
#scene-canvas {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  z-index: 0;
  /* Fades in with the intro rather than snapping on under the loader. */
  opacity: 0;
  transition: opacity 1.2s var(--ease);
}
body.is-ready #scene-canvas { opacity: 1; }
html.no-webgl #scene-canvas { display: none; }

main { position: relative; z-index: 2; }

/* Sits between the render and the writing. Both halves are always present and
   only their opacity moves, so the change of side is a cross-fade rather than a
   repaint — and because it is fixed, it never acquires an edge.
 *
 * The fade is driven by GSAP rather than a CSS transition, deliberately. A
 * transition is scheduled by the compositor; on a frame where the WebGL loop
 * has the main thread for a long time it can sit in "running" without
 * progressing, and the scrim stays invisible over a fully lit planet with the
 * type unreadable on top of it. Measured, not theorised. Putting it on the same
 * ticker as everything else means it advances exactly when the scene does. */
.scrim { position: fixed; inset: 0; z-index: 1; pointer-events: none; }
.scrim i { position: absolute; inset: 0; opacity: 0; }
/* The stops are placed against the layout, not by eye. A dossier occupies the
   outer 41 per cent of the width, so the scrim has to still be near full
   strength at the 41 per cent mark and gone by 66, or the far edge of the data
   table ends up as grey text on a lit cloud deck. */
.scrim__l {
  background: linear-gradient(to right, rgba(5, 6, 11, 0.9) 0%,
    rgba(5, 6, 11, 0.86) 34%, rgba(5, 6, 11, 0.46) 50%, rgba(5, 6, 11, 0) 66%);
}
.scrim__r {
  background: linear-gradient(to left, rgba(5, 6, 11, 0.9) 0%,
    rgba(5, 6, 11, 0.86) 34%, rgba(5, 6, 11, 0.46) 50%, rgba(5, 6, 11, 0) 66%);
}
/* data-scrim is still written to the root element: it is the readable record of
   which side is live, and the mobile rules and print styles key off it. */

/* ═══ LOADER ══════════════════════════════════════════════════════════════
   The percentage is real. It counts build steps — nine spheres, nine shader
   programs, a star field — because there is nothing else to count: this page
   downloads no assets at all beyond three libraries and two fonts. */

.loader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  place-items: center;
  background: var(--ink);
  padding: var(--pad);
}
.loader.is-gone { pointer-events: none; }
.loader__inner { text-align: center; max-width: 34rem; }

.loader__kicker {
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-3);
  margin-bottom: 1.6rem;
}
.loader__word {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(3.4rem, 12vw, 7rem);
  line-height: 0.92;
  letter-spacing: -0.01em;
  color: var(--paper);
}
.loader__meter {
  font-family: var(--mono);
  margin-top: 2.2rem;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}
.loader__pct { font-size: 1.5rem; }
.loader__unit { font-size: 0.7rem; opacity: 0.6; margin-left: 0.15em; }

.loader__track {
  height: 1px;
  background: var(--rule-soft);
  margin: 0.9rem 0 1.1rem;
  overflow: hidden;
}
.loader__bar {
  height: 100%;
  width: 0%;
  background: var(--accent);
  transform-origin: left;
}
.loader__status {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  color: var(--paper-3);
  min-height: 1.2em;
}

.enter {
  position: absolute;
  bottom: clamp(2rem, 8vh, 5rem);
  left: 50%;
  transform: translateX(-50%);
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--accent);
  padding: 0.45rem 0.1rem;
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper);
  cursor: pointer;
  transition: gap 0.4s var(--ease), color 0.4s var(--ease);
}
.enter:hover { gap: 1.2rem; color: var(--accent); }
.enter svg {
  width: 1.05rem; height: 1.05rem;
  fill: none; stroke: currentColor; stroke-width: 1.2;
}
.enter[hidden] { display: none; }

/* ── No WebGL ───────────────────────────────────────────────────────────── */
.fallback {
  position: relative;
  z-index: 2;
  margin: 0 auto;
  max-width: 44rem;
  padding: 8rem var(--pad) 0;
}
.fallback__note {
  font-size: 0.9rem;
  color: var(--paper-2);
  border-left: 1px solid var(--accent);
  padding-left: 1.1rem;
}

/* ═══ CHROME ══════════════════════════════════════════════════════════════ */

/* Corner ticks. Four elements, and they do more to set the register than any
   amount of frosted glass: they say the viewport is an aperture. */
.bezel { position: fixed; inset: 0; z-index: 6; pointer-events: none; }
.bezel i {
  position: absolute;
  width: 12px; height: 12px;
  border: 0 solid var(--accent-dim);
  filter: drop-shadow(0 0 3px rgba(5, 6, 11, 0.9));
  transition: border-color var(--tint);
}
.bezel i:nth-child(1) { top: 14px;    left: 14px;  border-top-width: 1px; border-left-width: 1px; }
.bezel i:nth-child(2) { top: 14px;    right: 14px; border-top-width: 1px; border-right-width: 1px; }
.bezel i:nth-child(3) { bottom: calc(var(--readout-h) + 14px); left: 14px;  border-bottom-width: 1px; border-left-width: 1px; }
.bezel i:nth-child(4) { bottom: calc(var(--readout-h) + 14px); right: 14px; border-bottom-width: 1px; border-right-width: 1px; }

.mast {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 8;
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  padding: clamp(1.6rem, 3vw, 2.4rem) var(--pad);
  pointer-events: none;
}
.mast__mark { pointer-events: auto; display: block; }

/* Overlay chrome sits on whatever the render happens to be doing, and at the
   Sun that is a full-frame corona. A shadow keeps hairline type legible over
   anything without putting a panel behind it — which is what the rest of this
   stylesheet spends its time avoiding. Two radii: a tight one for edge contrast
   and a wide one to darken the ground behind the glyph. */
.mast__mark, .rail__item, .hero__cue, .orrery__hint {
  text-shadow: 0 0 2px rgba(5, 6, 11, 0.9), 0 0 10px rgba(5, 6, 11, 0.85);
}
.mast__name {
  display: block;
  font-family: var(--display);
  font-size: 1.32rem;
  line-height: 1;
  letter-spacing: 0.01em;
}
.mast__sub {
  display: block;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper-3);
  margin-top: 0.4rem;
}

/* ── Body rail ──────────────────────────────────────────────────────────────
   A tick per body with the name set beside it. The tick lengthens and takes the
   accent when its section is live, which is the whole of the state: no pill, no
   fill, no underline animation. */
.rail {
  pointer-events: auto;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.42rem;
  margin-top: 0.2rem;
}
.rail__item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  background: none;
  border: 0;
  padding: 0.1rem 0;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--paper-3);
  transition: color 0.4s var(--ease);
}
.rail__item span { transition: opacity 0.4s var(--ease); opacity: 0.55; }
.rail__item i {
  display: block;
  width: 14px; height: 1px;
  background: currentColor;
  transition: width 0.5s var(--ease), background 0.4s var(--ease);
}
.rail__item:hover { color: var(--paper); }
.rail__item:hover span { opacity: 1; }
.rail__item.is-live { color: var(--accent); }
.rail__item.is-live span { opacity: 1; }
.rail__item.is-live i { width: 30px; }

/* ═══ READOUT ═════════════════════════════════════════════════════════════
   Every figure here comes out of the same Kepler solve that positions the
   planet, one frame earlier. Tabular numerals and a fixed field order, so the
   strip never reflows while it ticks — a readout that jitters reads as broken
   however correct the numbers are. */
.readout {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 8;
  height: var(--readout-h);
  display: flex;
  align-items: stretch;
  border-top: 1px solid var(--rule-soft);
  background: linear-gradient(to top, rgba(5, 6, 11, 0.92), rgba(5, 6, 11, 0.55));
  backdrop-filter: blur(3px);
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  opacity: 0;
  transform: translateY(100%);
}
.readout__cell {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 0.14rem;
  padding: 0 clamp(0.7rem, 1.8vw, 1.5rem);
  border-right: 1px solid var(--rule-soft);
  white-space: nowrap;
  flex: 1 1 auto;
}
.readout__cell:last-child { border-right: 0; }
.readout__cell:first-child { padding-left: var(--pad); }
.readout__cell:last-child  { padding-right: var(--pad); text-align: right; align-items: flex-end; }
.readout__k {
  font-size: 0.53rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper-3);
}
.readout__v {
  font-size: 0.72rem;
  color: var(--paper);
}
.readout__cell--date .readout__v { color: var(--accent); transition: color var(--tint); }

/* ── Cursor ─────────────────────────────────────────────────────────────────
   A ring that trails a dot. Hidden entirely on touch, where there is no cursor
   to replace and a lagging circle is just a bug. */
.cursor {
  position: fixed;
  top: 0; left: 0;
  z-index: 40;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  will-change: transform;
}
/* The outer element is only a positioner, moved every frame from JS. The ring
   is the child, so its hover scale and the per-frame translate cannot overwrite
   each other — two transforms on one node is the usual way this breaks. */
.cursor i {
  display: block;
  width: 30px; height: 30px;
  margin: -15px 0 0 -15px;
  border: 1px solid var(--accent-dim);
  border-radius: 50%;
  transition: transform 0.28s var(--ease), border-color var(--tint),
              background 0.28s var(--ease);
}
.cursor i::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 3px; height: 3px;
  margin: -1.5px 0 0 -1.5px;
  border-radius: 50%;
  background: var(--accent);
  transition: background var(--tint);
}
body.has-cursor .cursor { opacity: 1; }
.cursor.is-active i { transform: scale(1.65); background: rgba(255, 255, 255, 0.05); }
body.is-grabbing .cursor i { transform: scale(0.55); }
@media (hover: none), (pointer: coarse) { .cursor { display: none; } }
body.has-cursor { cursor: none; }
body.has-cursor a, body.has-cursor button, body.has-cursor input { cursor: none; }

/* ═══ TYPE HELPERS ════════════════════════════════════════════════════════ */

/* Word masks for the entrance. Wrapping every word rather than every measured
   line means a font swap, a resize or text-wrap: balance cannot leave a word
   clipped in half — the mask travels with the word. */
.wm { display: inline-block; overflow: hidden; vertical-align: top; }
.wi { display: inline-block; will-change: transform; }

.folio {
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-3);
}
.folio span { color: var(--accent); transition: color var(--tint); }

.prose p {
  font-size: clamp(0.87rem, min(1.02vw, 1.7vh), 1.04rem);
  line-height: 1.66;
  color: var(--paper-2);
  max-width: 34em;
}
.prose p + p { margin-top: 1.05em; }
.prose em { font-style: italic; color: var(--paper); }

/* ═══ HERO ════════════════════════════════════════════════════════════════ */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  align-content: center;
  padding: 20vh var(--pad) calc(14vh + var(--readout-h));
}
.hero__inner { max-width: 46rem; }
.hero__kicker {
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-3);
  margin-bottom: 1.5rem;
}
.hero__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(3.6rem, 13vw, 10rem);
  line-height: 0.88;
  letter-spacing: -0.015em;
  margin-bottom: 2rem;
}
.hero__lede {
  font-size: clamp(1rem, 1.35vw, 1.24rem);
  line-height: 1.6;
  color: var(--paper-2);
  max-width: 32em;
}
.hero__meta {
  margin-top: 1.6rem;
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  color: var(--paper-3);
  border-left: 1px solid var(--accent);
  padding-left: 0.9rem;
  transition: border-color var(--tint);
}

.hero__cue {
  position: absolute;
  left: var(--pad);
  bottom: calc(var(--readout-h) + 2.2rem);
  display: flex;
  align-items: center;
  gap: 0.8rem;
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--paper-3);
}
.hero__cue i {
  display: block;
  width: 46px; height: 1px;
  background: linear-gradient(to right, var(--accent), transparent);
  transform-origin: left;
  animation: cue 2.6s var(--ease-io) infinite;
}
@keyframes cue {
  0%, 100% { transform: scaleX(0.35); opacity: 0.5; }
  50%      { transform: scaleX(1);    opacity: 1; }
}

/* ═══ THE TOUR ════════════════════════════════════════════════════════════ */

.body {
  position: relative;
  min-height: 112vh;
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  align-items: center;
  gap: 0 2rem;
  padding: 14vh var(--pad) calc(14vh + var(--readout-h));
}
.body[data-side="left"]  .dossier { grid-column: 1 / span 5; }
.body[data-side="right"] .dossier { grid-column: 8 / span 5; }

/* The rail occupies the top-right corner and a right-hand dossier runs to the
   same edge, so the two collide across the top third of the screen. Reserving
   the gutter is the fix; moving the navigation somewhere it fits less well is
   not. Only needed while the rail is a vertical list of names. */
@media (min-width: 901px) {
  .body[data-side="right"] .dossier { padding-right: 5.75rem; }
}

.dossier__name {
  font-family: var(--display);
  font-weight: 400;
  /* Bound by height as well as width. A dossier is a fixed stack of parts —
     name, epithet, two paragraphs, a nine-row table — and on a 1280x800 laptop
     a width-only clamp sets the name at 108px and pushes the first paragraph
     off the top of the screen. min() lets whichever axis is tighter win. */
  font-size: clamp(2.7rem, min(8vw, 9.4vh), 6.4rem);
  line-height: 0.9;
  letter-spacing: -0.015em;
  margin: 0.75rem 0 0.4rem;
}
.dossier__epithet {
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(1rem, min(1.7vw, 2.4vh), 1.4rem);
  line-height: 1.3;
  color: var(--accent);
  margin-bottom: clamp(1.1rem, 2.4vh, 1.9rem);
  transition: color var(--tint);
}
.dossier .prose { margin-bottom: clamp(1.2rem, 2.8vh, 2.2rem); }

/* ── Data table ─────────────────────────────────────────────────────────────
   A table, set as one. Hairline rows, tabular figures, the label in sans and
   the value in mono because only one of the two columns is numeric. */
.facts {
  display: grid;
  grid-template-columns: minmax(7.5rem, 38%) 1fr;
  border-top: 1px solid var(--rule);
  font-size: clamp(0.64rem, min(0.72rem, 1.35vh), 0.72rem);
}
.facts dt, .facts dd {
  padding: clamp(0.3rem, 0.62vh, 0.5rem) 0;
  border-bottom: 1px solid var(--rule-soft);
}
.facts dt {
  color: var(--paper-3);
  letter-spacing: 0.01em;
  padding-right: 1rem;
}
.facts dd {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--paper-2);
  text-align: right;
}

/* ═══ ORRERY ══════════════════════════════════════════════════════════════
   Sticky, not pinned. position: sticky needs no measurement, cannot be knocked
   out of alignment by a resize, and never argues with the smooth-scroll layer
   over who owns the scroll position — three failure modes avoided by not
   reaching for ScrollTrigger.pin() out of habit. */

.orrery { position: relative; height: 300vh; }
.orrery__sticky {
  position: sticky;
  top: 0;
  min-height: 100svh;
  display: grid;
  align-content: space-between;
  padding: clamp(6rem, 14vh, 9rem) var(--pad) calc(2rem + var(--readout-h));
}
.orrery__head { max-width: 30rem; }
.orrery__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.4rem, 5vw, 4.2rem);
  line-height: 0.95;
  margin: 0.7rem 0 1.2rem;
}
.orrery__note {
  font-size: 0.86rem;
  line-height: 1.65;
  color: var(--paper-2);
}

.orrery__hint {
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-3);
  justify-self: start;
}
@media (hover: none), (pointer: coarse) { .orrery__hint { display: none; } }

/* ── Console ───────────────────────────────────────────────────────────────
   Controls drawn as an instrument panel: hairlines, no fills, no shadows. The
   scrub track is one pixel high because that is what a scale on a real
   instrument looks like. */
.console {
  justify-self: end;
  align-self: center;
  width: min(26rem, 100%);
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: 1rem 0;
  display: grid;
  gap: 0.9rem;
}
.console__row { display: flex; align-items: center; gap: 0.8rem; }
.console__label {
  font-family: var(--mono);
  font-size: 0.58rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--paper-3);
  flex: 0 0 auto;
}
.console__date {
  flex: 1 1 auto;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  padding: 0.25rem 0;
  font-family: var(--mono);
  font-size: 0.76rem;
  color: var(--paper);
  color-scheme: dark;
}
.console__date:focus { outline: none; border-bottom-color: var(--accent); }
.console__now {
  flex: 0 0 auto;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--accent);
  padding: 0.25rem 0.1rem;
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  cursor: pointer;
  transition: opacity 0.3s var(--ease);
}
.console__now:hover { opacity: 0.65; }

.console__end {
  font-family: var(--mono);
  font-size: 0.55rem;
  color: var(--paper-3);
  flex: 0 0 auto;
}
.console__scrub {
  flex: 1 1 auto;
  -webkit-appearance: none;
  appearance: none;
  height: 18px;
  background: none;
  cursor: pointer;
}
.console__scrub::-webkit-slider-runnable-track {
  height: 1px;
  background: var(--rule);
}
.console__scrub::-moz-range-track { height: 1px; background: var(--rule); }
.console__scrub::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 2px;
  height: 14px;
  margin-top: -6.5px;
  background: var(--accent);
  border: 0;
  border-radius: 0;
}
.console__scrub::-moz-range-thumb {
  width: 2px; height: 14px;
  background: var(--accent);
  border: 0; border-radius: 0;
}
.console__scrub:focus-visible { outline-offset: 6px; }

.console__row--rates { gap: 0.55rem; flex-wrap: wrap; }
.rate {
  background: none;
  border: 0;
  border-bottom: 1px solid transparent;
  padding: 0.2rem 0;
  font-family: var(--mono);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--paper-3);
  cursor: pointer;
  transition: color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.rate:hover { color: var(--paper); }
.rate.is-on { color: var(--accent); border-bottom-color: var(--accent); }

/* ═══ COLOPHON ════════════════════════════════════════════════════════════ */

.colophon {
  position: relative;
  background: linear-gradient(to bottom, rgba(5, 6, 11, 0) 0%, var(--ink) 22%);
  padding: 16vh var(--pad) calc(10vh + var(--readout-h));
}
.colophon__inner { max-width: var(--maxw); margin: 0 auto; }
.colophon__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(2.2rem, 5.2vw, 4.4rem);
  line-height: 0.98;
  margin: 0.8rem 0 3.5rem;
  max-width: 18em;
}
.colophon__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 0;
  border-top: 1px solid var(--rule);
}
.note {
  padding: 1.8rem 1.6rem 2rem 0;
  border-bottom: 1px solid var(--rule-soft);
  border-right: 1px solid var(--rule-soft);
}
.note:last-child { border-right: 0; }
.note h3 {
  font-family: var(--mono);
  font-weight: 500;
  font-size: 0.63rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.9rem;
  transition: color var(--tint);
}
.note p {
  font-size: 0.83rem;
  line-height: 1.66;
  color: var(--paper-2);
}
.note em { font-style: italic; color: var(--paper); }
.note sup { font-size: 0.7em; }

.credits {
  margin-top: 3.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(20rem, 1fr));
  gap: 1.4rem 3rem;
}
.credits p {
  font-size: 0.76rem;
  line-height: 1.6;
  color: var(--paper-3);
  max-width: 34em;
}
.credits strong { font-weight: 500; color: var(--paper-2); }

/* ── Short viewports ────────────────────────────────────────────────────────
 * A dossier is a fixed stack: name, epithet, two paragraphs, a table of up to
 * ten rows. Centred in the viewport it has to clear the wordmark above and the
 * readout strip below, which together come to about 150px of furniture. At
 * 1080p there is room to spare; below roughly 940px of viewport height the
 * longest dossier runs into the wordmark. The rules below buy back about 130px,
 * which covers every common laptop.
 *
 * Height queries rather than width: the constraint is genuinely vertical, and
 * a wide-but-short window has exactly the same problem as a narrow one. */
@media (min-width: 901px) and (max-height: 940px) {
  .dossier__name { font-size: clamp(2.3rem, 7.2vh, 4.2rem); margin-top: 0.5rem; }
  .dossier__epithet { font-size: 1.02rem; margin-bottom: 0.85rem; }
  .prose p { font-size: 0.85rem; line-height: 1.56; }
  .prose p + p { margin-top: 0.75em; }
  .dossier .prose { margin-bottom: 1rem; }
  .facts { font-size: 0.63rem; }
  .facts dt, .facts dd { padding: 0.26rem 0; }
}

/* ═══ RESPONSIVE ══════════════════════════════════════════════════════════
   The layout does not reflow so much as change its mind. Below 900px there is
   no room for a dossier beside a planet, so the writing goes full width and
   sits at the foot of the screen with the planet above it — which is a better
   composition on a phone than a squeezed version of the desktop one. */

@media (max-width: 1100px) {
  .body[data-side="left"]  .dossier { grid-column: 1 / span 6; }
  .body[data-side="right"] .dossier { grid-column: 7 / span 6; }
}

@media (max-width: 900px) {
  :root { --readout-h: 2.7rem; }

  /* Stacked. The body takes the top of the screen and the writing runs full
     width beneath it, which is a better composition on a phone than a squeezed
     copy of the two-column desktop one. The section is allowed to grow past the
     viewport: you scroll through the dossier while the planet stays behind it. */
  .body {
    align-items: end;
    min-height: 100svh;
    padding-top: 38vh;
  }
  .body .dossier { grid-column: 1 / -1 !important; padding-right: 0 !important; }

  /* Type comes down hard here. At desktop sizes the longest dossier is 790px on
     a 844px screen, which leaves no room for the planet it is describing. */
  .dossier__name { font-size: clamp(2rem, 9vw, 2.9rem); margin: 0.5rem 0 0.3rem; }
  .dossier__epithet { font-size: 0.98rem; margin-bottom: 0.85rem; }
  .prose p { font-size: 0.855rem; line-height: 1.58; }
  .dossier .prose { margin-bottom: 1rem; }
  .facts { font-size: 0.63rem; grid-template-columns: minmax(5.5rem, 46%) 1fr; }
  .facts dt, .facts dd { padding: 0.25rem 0; }

  /* Narrow screens stack instead of splitting, so the scrim comes from the foot
     of the viewport for both sides rather than from either edge. It stays near
     full strength across the lower two thirds, because that is where the whole
     dossier sits — a gentle fade left the epithet and first paragraph sitting
     on lit cloud. */
  .scrim i {
    background: linear-gradient(to top, rgba(5, 6, 11, 0.95) 0%,
      rgba(5, 6, 11, 0.92) 52%, rgba(5, 6, 11, 0.6) 74%,
      rgba(5, 6, 11, 0) 94%) !important;
  }
  /* !important because GSAP writes the desktop cross-fade to inline styles,
     and a stacked layout wants one scrim from below regardless of side. */
  .scrim__l { opacity: 1 !important; }
  .scrim__r { opacity: 0 !important; }

  .rail {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: flex-end;
    max-width: 58vw;
    gap: 0.3rem 0.55rem;
  }
  /* Names would wrap to three lines; the ticks alone still track progress. */
  .rail__item span { display: none; }
  .rail__item i { width: 9px; }
  .rail__item.is-live i { width: 20px; }

  .orrery { height: 260vh; }
  .orrery__sticky { align-content: start; gap: 2.4rem; }
  .console { justify-self: stretch; width: 100%; }

  .note { padding-right: 0; border-right: 0; }

  /* Six live fields will not fit. The three that answer when, what and how far
     stay; ui.js shortens their contents to match. */
  .readout__cell--lon,
  .readout__cell--light { display: none; }
  .readout__cell { padding: 0 0.6rem; }
  .readout__k { font-size: 0.48rem; letter-spacing: 0.1em; }
  .readout__v { font-size: 0.66rem; }
}

@media (max-width: 620px) {
  /* Orbital speed goes too. Epoch, body and distance are the three a reader can
     actually use, and they are the three that fit. */
  .readout__cell:nth-child(5) { display: none; }
  .mast__sub { display: none; }
  .hero__cue { display: none; }
  .hero__title { font-size: clamp(3rem, 17vw, 4.6rem); }
  .hero__lede { font-size: 0.95rem; }
  /* Two of the values run to a second line at 0.63rem, and each wrap costs a
     row of height the planet needs. */
  .facts { font-size: 0.6rem; }
  .facts dt, .facts dd { padding: 0.22rem 0; }
  .prose p { font-size: 0.83rem; }
}

/* ═══ REDUCED MOTION ══════════════════════════════════════════════════════
   Not a blanket transition kill. What actually causes trouble is large-area
   travel: the smooth-scroll layer, the camera drift, the parallax and the
   pulsing cue. Those are switched off in JS. Colour and opacity transitions are
   left alone, because removing them makes state changes harder to follow, not
   easier. */
@media (prefers-reduced-motion: reduce) {
  .hero__cue i { animation: none; transform: scaleX(1); opacity: 0.7; }
  .cursor { display: none; }
  body.has-cursor { cursor: auto; }
  html { scroll-behavior: auto; }
}

/* ═══ PRINT ═══════════════════════════════════════════════════════════════
   The whole point of keeping the figures in the DOM rather than the canvas is
   that they survive being printed, saved to Reader, or read aloud. */
@media print {
  #scene-canvas, .bezel, .cursor, .readout, .rail, .loader, .hero__cue { display: none !important; }
  body { background: #fff; color: #111; }
  .prose p, .note p, .credits p, .facts dd { color: #333; }
  .body, .hero, .orrery { min-height: 0; height: auto; page-break-inside: avoid; }
  .scrim, .colophon { background: none; }
  .orrery__sticky { position: static; }
  .console { display: none; }
  .dossier__epithet, .folio span, .note h3 { color: #111; }
}
