/* SIMULACRA.BLACK
   Cathedral proportions: a narrow nave of content, vast black margins.
   Layered blacks only — no pure #000, no pure #FFF, no radius, no shadow. */

:root {
  --void:    #050505;
  --crypt:   #0D0C0E;
  --ash:     #1A181C;
  --bone:    #D9D3C7;
  --smoke:   #6E6A66;
  --oxblood: #571C24;

  --display: 'Bodoni Moda', Georgia, serif;
  --mono:    'IBM Plex Mono', ui-monospace, monospace;

  /* The floor has to sit BELOW the fluid step, or narrow viewports get the
     floor and the nine-letter wordmark overflows. Nine Bodoni caps measure
     ~6.05x the font size, so 14vw keeps the word inside the gutters down to
     320px. */
  --step-hero:  clamp(2.5rem, 14vw, 12rem);
  --step-h2:    clamp(1.8rem, 4vw, 3rem);
  --step-body:  0.875rem;
  --step-label: 0.6875rem;

  --nave: 640px;
  --measure: 56ch;
  /* One spine for the whole page: every band, the archive sheet and the map
     all share this width and this gutter, so their left edges agree. */
  --spine: 1120px;
  --gutter: clamp(1.25rem, 4vw, 3rem);

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
}

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

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

body {
  margin: 0;
  background: var(--void);
  color: var(--bone);
  font-family: var(--mono);
  font-size: var(--step-body);
  line-height: 1.8;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* Accessibility ------------------------------------------------------- */

.vh {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap; border: 0;
}

.skip {
  position: absolute; left: 50%; top: 0;
  transform: translate(-50%, -110%);
  z-index: 10;
  padding: 0.75rem 1.25rem;
  background: var(--crypt);
  border: 1px solid var(--ash);
  color: var(--bone);
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-decoration: none;
}
.skip:focus { transform: translate(-50%, 0); }

:focus-visible {
  outline: 1px solid var(--bone);
  outline-offset: 4px;
}

/* Nav ----------------------------------------------------------------- */

.nav {
  /* Fixed, not absolute: the bar belongs to the instrument, not to the hero,
     so it stays on screen through the whole descent. It is a real bar now —
     per-item plates read as patches floating over the ground rather than as
     a piece of chrome. */
  position: fixed; inset: 0 0 auto 0;
  z-index: 12;
  display: flex; justify-content: space-between;
  padding: 1rem clamp(1.25rem, 4vw, 3rem);
  background: rgba(5, 5, 5, 0.86);
  backdrop-filter: blur(9px) saturate(0.9);
  -webkit-backdrop-filter: blur(9px) saturate(0.9);
  border-bottom: 1px solid var(--ash);
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
}

.nav__enter {
  color: var(--smoke);
  text-decoration: none;
  transition: color 300ms var(--ease);
}
.nav__enter:hover { color: var(--bone); }

/* Hero ---------------------------------------------------------------- */

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  place-items: center;
  overflow: hidden;
  padding: 6rem 1.5rem;
}

.hero__canvas {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  /* The form sits behind the name — the copy is always partly obscured
     by the thing it is a copy of. */
  opacity: 0;
  transition: opacity 1200ms var(--ease);
}
.hero__canvas.is-lit { opacity: 1; }

.hero__type {
  position: relative;
  z-index: 2;
  text-align: center;
}

.hero__word {
  margin: 0;
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--step-hero);
  line-height: 0.95;
  letter-spacing: -0.02em;
  color: var(--bone);
  display: flex;
  justify-content: center;
}

/* The stack holds the real wordmark and its two ghosts in the same box. */
.hero__stack { position: relative; display: block; }

.hero__ghost {
  position: absolute;
  inset: 0;
  display: flex;
  justify-content: center;
  font-family: var(--display);
  font-weight: 400;
  font-size: var(--step-hero);
  line-height: 0.95;
  letter-spacing: -0.02em;
  pointer-events: none;
  opacity: 0;
}
.hero__ghost--a { color: var(--bone); }      /* carries the displacement */
.hero__ghost--b { color: var(--oxblood); }   /* carries the colour fringe */

/* Each copy slides in from its own side and converges on the original, then
   goes out. The name arrives by its copies cancelling, which is the whole
   idea of the site stated in one gesture. */
.js .hero__stack.is-set .hero__ghost--a { animation: converge-a 1500ms var(--ease) 250ms both; }
.js .hero__stack.is-set .hero__ghost--b { animation: converge-b 1500ms var(--ease) 250ms both; }

/* The bone copy is held well down — at full strength an offset duplicate of a
   200px wordmark reads as a mistake rather than a register error. The oxblood
   copy runs near-opaque because that colour is nearly black to begin with. */
@keyframes converge-a {
  0%   { opacity: 0;    transform: translate(-0.13em, -0.04em) scaleX(1.03); }
  25%  { opacity: 0.32; }
  100% { opacity: 0;    transform: none; }
}
@keyframes converge-b {
  0%   { opacity: 0;    transform: translate(0.15em, 0.05em) scaleX(0.975); }
  25%  { opacity: 0.95; }
  100% { opacity: 0;    transform: none; }
}

.hero__word span { display: inline-block; }
.js .hero__word span {
  /* Each letter starts displaced and turned by its own amount, set from JS. */
  opacity: 0;
  transform: translate(var(--dx, 0), var(--dy, 0.14em)) rotate(var(--dr, 0deg)) scale(var(--ds, 1));
}
.hero__word.is-set span {
  animation: letter-in 1050ms var(--ease) forwards;
}

@keyframes letter-in {
  0%   { opacity: 0; transform: translate(var(--dx, 0), var(--dy, 0.14em)) rotate(var(--dr, 0deg)) scale(var(--ds, 1)); }
  55%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}

.hero__meta {
  /* A label plate: knocks the point cloud out behind the telemetry line so it
     stays readable where it crosses the slab. Same colour as the ground, so it
     reads as absence rather than a box. */
  display: inline-block;
  background: var(--void);
  padding: 0.6rem 1.25rem;
  margin: 2rem 0 0;
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
}
.js .hero__meta {
  opacity: 0;
  transition: opacity 900ms var(--ease) 700ms;
}
.hero__meta.is-set { opacity: 1; }

.hero__gen {
  position: absolute;
  right: clamp(1.5rem, 4vw, 3rem);
  bottom: 1.75rem;
  z-index: 2;
  margin: 0;
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  color: var(--smoke);
}
.js .hero__gen {
  opacity: 0;
  transition: opacity 900ms var(--ease) 900ms;
}
.hero__gen.is-set { opacity: 1; }

/* Bands --------------------------------------------------------------- */

.band {
  max-width: var(--spine);
  margin: 0 auto;
  padding: clamp(3.5rem, 6vw, 5.5rem) var(--gutter);
  border-top: 1px solid var(--ash);
}

/* Prose keeps a readable measure but stays on the spine's left edge rather
   than centring itself to a narrower column — the mismatched left edges were
   what made the sections read as unrelated blocks.
   The doctrine is deliberately NOT capped here: --measure is in `ch`, which
   resolves against the mono body font (~470px), and that was squeezing the
   display lines into two even though they had room to spare. */
.band > .reveal { max-width: var(--measure); }

.eyebrow {
  margin: 0 0 2.25rem;
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
}

/* I. Doctrine --------------------------------------------------------- */

.doctrine__line {
  margin: 0 0 0.9rem;
  font-family: var(--display);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(1.5rem, 3.1vw, 2.6rem);
  line-height: 1.18;
  letter-spacing: -0.01em;
  color: var(--bone);
  /* "The map preceded the territory." is 31 characters; a 22ch cap was
     breaking every statement across two lines. */
  max-width: 34ch;
  text-wrap: balance;
}
.doctrine__line:last-child { margin-bottom: 0; }

/* II. Archive ---------------------------------------------------------- */

/* The archive is the one section allowed to break the nave, because images
   need the width. Everything else stays in the narrow column. */
/* Everything is on the spine now; the modifier stays only so the markup does
   not need rewriting. */
.band--wide { max-width: var(--spine); }

/* The archive gets tighter vertical padding than the other bands — the point
   is to see the whole sheet at once, and the default 9rem top and bottom was
   pushing it past a single screen on its own. */
#archive .eyebrow { margin-bottom: 1.75rem; }

/* A contact sheet, not a scrapbook: uniform tiles on a fixed grid so the whole
   archive lands in roughly one screen however many plates there are. */
.plates {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 6px;
}

.plate {
  position: relative;
  display: block;
  width: 100%;
  margin: 0;
  aspect-ratio: 4 / 5;
  background: var(--crypt);
  /* A hairline mount, and a slight tilt set per plate from JS — pinned
     prints on a wall rather than a CSS grid that happens to hold images. */
  border: 1px solid var(--ash);
  padding: 0;
  cursor: zoom-in;
  overflow: hidden;
}

.plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Held back until hover so the wall reads as one dark field, not a
     scrapbook — the images resolve as you move through them. */
  opacity: 0.62;
  transition: opacity 400ms var(--ease);
}
.plate:hover img, .plate:focus-visible img { opacity: 1; }

.plate__cap {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 0.75rem;
  background: var(--void);
  font-size: var(--step-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--smoke);
  text-align: left;
  opacity: 0;
  transition: opacity 300ms var(--ease);
}
.plate:hover .plate__cap, .plate:focus-visible .plate__cap { opacity: 1; }

.plates__empty {
  margin: 0;
  color: var(--smoke);
  max-width: var(--measure);
}

/* Lightbox */
.lb {
  position: fixed; inset: 0; z-index: 20;
  display: none;
  place-items: center;
  padding: 3rem 1.5rem;
  background: var(--void);
}
.lb.is-open { display: grid; }
.lb img { max-width: 100%; max-height: 84vh; display: block; }
.lb__cap {
  margin: 1.25rem 0 0;
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
  text-align: center;
}
.lb__shut {
  position: absolute; top: 1.5rem; right: 1.5rem;
  background: transparent; border: 1px solid var(--ash); border-radius: 0;
  color: var(--smoke); font-family: var(--mono); font-size: var(--step-label);
  letter-spacing: 0.18em; text-transform: uppercase;
  padding: 0.6rem 1rem; cursor: pointer;
}
.lb__shut:hover { border-color: var(--oxblood); color: var(--bone); }

/* III. Transmission --------------------------------------------------- */

/* Colophon ------------------------------------------------------------ */

.colophon {
  max-width: var(--spine);
  margin: 0 auto;
  padding: 2.5rem var(--gutter) 3rem;
  border-top: 1px solid var(--ash);
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
  text-align: center;
}

/* Scroll reveals ------------------------------------------------------ */

.js .reveal > * {
  opacity: 0;
  transform: translateY(1rem);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
}
.js .reveal.is-in > * { opacity: 1; transform: none; }

/* Doctrine staggers one line at a time; everything else lands at once. */
.doctrine.is-in > .doctrine__line:nth-child(2) { transition-delay: 80ms; }
.doctrine.is-in > .doctrine__line:nth-child(3) { transition-delay: 160ms; }

/* Mobile -------------------------------------------------------------- */

@media (max-width: 40rem) {
  .hero { padding-left: 1rem; padding-right: 1rem; }
  .hero__meta { padding-left: 0.5rem; padding-right: 0.5rem; }
  /* Was 0.45, held back from when the hero was a dense point cloud. The city
     plate is far finer, and dimming it that far left the header looking empty
     on a phone. */
  .hero__canvas { opacity: 0; }
  .hero__canvas.is-lit { opacity: 0.9; }
  .plates { columns: 2 140px; }
  .nav__mark { letter-spacing: 0.12em; }
}

/* Reduced motion ------------------------------------------------------ */

@media (prefers-reduced-motion: reduce) {
  .js .hero__word span,
  .hero__word.is-set span { opacity: 1; transform: none; animation: none; }
  .hero__ghost { display: none; }
  .js .hero__meta, .js .hero__gen { opacity: 1; }
  .hero__meta, .hero__gen, .hero__canvas { transition: none; }
  .js .reveal > * { opacity: 1; transform: none; transition: none; }
}

/* Nav additions -------------------------------------------------------- */

.nav { gap: 0.75rem 3rem; align-items: baseline; flex-wrap: wrap; }

.nav__right { display: flex; gap: 1.25rem; align-items: baseline; margin-left: auto; }

.nav__audio {
  background: transparent; border: 0; padding: 0; border-radius: 0;
  color: var(--smoke); font-family: var(--mono); font-size: var(--step-label);
  letter-spacing: 0.18em; text-transform: uppercase; cursor: pointer;
  transition: color 300ms var(--ease);
}
.nav__audio:hover { color: var(--bone); }
.nav__audio[aria-pressed="true"] { color: var(--oxblood); }
.nav__audio[disabled] { opacity: 0.5; cursor: default; }

/* The eroding paragraph ------------------------------------------------ */

.erode {
  margin: 2.75rem 0 0.6rem;
  max-width: var(--measure);
  color: var(--bone);
  line-height: 2;
  white-space: pre-wrap;   /* the gaps are the content — never collapse them */
}
.erode__gap { color: transparent; }

.erode__note {
  margin: 0;
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
}

/* IV. Telemetry -------------------------------------------------------- */

.log {
  height: 13rem;
  overflow: hidden;                /* it scrolls itself; no scrollbar wanted */
  border-top: 1px solid var(--ash);
  border-bottom: 1px solid var(--ash);
  padding: 1rem 0;
  margin-bottom: 3rem;
  /* Fades at both ends so lines arrive and leave rather than popping. */
  mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 18%, #000 82%, transparent);
}

.log__line {
  margin: 0 0 0.3rem;
  font-size: var(--step-label);
  letter-spacing: 0.06em;
  /* Older lines recede, so the feed has depth instead of reading as a block
     of identical grey. */
  color: var(--ash);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.log__line:nth-last-child(-n+6) { color: var(--smoke); }
.log__line:nth-last-child(-n+2) { color: var(--bone); }

/* Reveal children individually (doctrine keeps its stagger) -------------- */
.js .doctrine > .reveal-child { opacity: 0; transform: translateY(1rem);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease); }
.js .doctrine.is-in > .reveal-child { opacity: 1; transform: none; }
.doctrine.is-in > .reveal-child:nth-child(2) { transition-delay: 80ms; }
.doctrine.is-in > .reveal-child:nth-child(3) { transition-delay: 160ms; }

@media (max-width: 40rem) {
  .nav { gap: 0.75rem 1rem; }
  .log { height: 10rem; }
}

@media (prefers-reduced-motion: reduce) {
  .js .doctrine > .reveal-child { opacity: 1; transform: none; transition: none; }
  .nav__audio, .plate img, .plate__cap { transition: none; }
}

/* ======================================================================
   Mobile pass + motion pass
   ====================================================================== */

/* Staggered arrival for the index, the plates and the log. The page is a
   descent, so things should land in reading order rather than all at once. */

@keyframes row-in {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}

/* Plates arrive on scroll, in order down the column. */
.js .plate { opacity: 1; }
.plate img { transition: opacity 400ms var(--ease), transform 900ms var(--ease),
             filter 600ms var(--ease); }
.plate:hover img, .plate:focus-visible img { transform: scale(1.04); }

/* The lightbox arrives rather than appearing. */
.lb { opacity: 0; transition: opacity 260ms var(--ease); }
.lb.is-open { opacity: 1; }
.lb img { animation: lb-in 380ms var(--ease) both; }
@keyframes lb-in {
  from { opacity: 0; transform: scale(0.985); }
  to   { opacity: 1; transform: none; }
}

/* Newest telemetry line arrives from below. */
.log__line:last-child { animation: log-in 420ms var(--ease) both; }
@keyframes log-in {
  from { opacity: 0; transform: translateY(0.5rem); }
  to   { opacity: 1; transform: none; }
}

/* The entry panel unfolds. */

@keyframes panel-in {
  from { opacity: 0; transform: translateY(-0.5rem); }
  to   { opacity: 1; transform: none; }
}

/* The audio toggle pulses while sound is running, so the state is legible
   without a meter. */
.nav__audio[aria-pressed="true"] { animation: pulse 3.2s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }

/* ---------------------------------------------------------------------- */

@media (max-width: 40rem) {
  /* Nav becomes two rows: identity, then controls. The liturgy line is the
     first thing to go if there is no room for it. */
  .nav { padding: 1rem; gap: 0.5rem 1rem; }
  .nav__right { gap: 0.9rem; }

  .hero { padding-top: 7rem; }
  .hero__gen { bottom: 1rem; right: 1rem; }

  .band { padding-left: 1.25rem; padding-right: 1.25rem; }
  .band--wide { padding-left: 0.75rem; padding-right: 0.75rem; }

  .erode { margin-top: 2.5rem; line-height: 1.9; }

  .plates { columns: 2 130px; }

  .log { height: 8.5rem; }
  .log__line { font-size: 0.5625rem; letter-spacing: 0.02em; }

  .lb { padding: 1rem; }
  .lb__shut { top: 1rem; right: 1rem; }
}

/* Phones in landscape and small tablets: keep the hero from eating the screen. */
@media (max-height: 30rem) and (orientation: landscape) {
  .hero { min-height: 30rem; }
}

/* Touch devices never get a hover state, so the plates must resolve on their
   own — otherwise the archive reads as a grid of dim rectangles. */
@media (hover: none) {
  .plate img { opacity: 0.9; }
  .plate__cap { opacity: 1; background: transparent;
                text-shadow: 0 1px 2px var(--void); }
}

@media (prefers-reduced-motion: reduce) {
  .plate, .lb img, .log__line:last-child {
    animation: none; opacity: 1; transform: none;
  }
  .nav__audio[aria-pressed="true"] { animation: none; }
  .plate:hover img, .plate:focus-visible img { transform: none; }
  .lb { transition: none; }
}

/* ======================================================================
   Motion, second pass
   ====================================================================== */

/* The nav settles in after the wordmark, not with it. */
.js .nav > * { opacity: 0; animation: nav-in 900ms var(--ease) 1100ms forwards; }
.js .nav > *:nth-child(2) { animation-delay: 1220ms; }
.js .nav > *:nth-child(3) { animation-delay: 1340ms; }
@keyframes nav-in { to { opacity: 1; } }

/* A hairline that draws itself under each section marker as the band
   arrives — the rule is the section opening, not a decoration sitting there. */
.eyebrow { position: relative; padding-bottom: 1rem; }
.eyebrow::after {
  content: "";
  position: absolute; left: 0; bottom: 0;
  height: 1px; width: 0;
  background: var(--ash);
  transition: width 900ms var(--ease) 120ms;
}
.band.is-lit .eyebrow::after { width: 100%; }

/* The copy number rolls up to the visitor's own, then stops. */
.hero__meta .rolling { color: var(--smoke); }

/* Index rows: the code slides a hair on hover, the title stays put, so the
   movement reads as the catalogue shifting rather than the entry. */

/* Expunged rows breathe very slightly out of phase with each other, so the
   index looks like a live record rather than a printed table. */

@keyframes expunge { 0%, 100% { opacity: 0.55; } 50% { opacity: 1; } }

/* Colophon arrives last. */
.js .colophon { opacity: 0; transition: opacity 900ms var(--ease); }
.js .colophon.is-in { opacity: 1; }

/* Reading position, as a hairline across the very top. */
.progress {
  position: fixed; top: 0; left: 0; z-index: 12;
  height: 1px; width: 0;
  background: var(--bone);
  opacity: 0.35;
}

/* The transmission field underline sweeps in on focus. */

@media (prefers-reduced-motion: reduce) {
  .js .nav > * { opacity: 1; animation: none; }
  .eyebrow::after { transition: none; width: 100%; }
  .js .colophon { opacity: 1; transition: none; }
  .progress { display: none; }
}

/* ======================================================================
   The archive plates
   ====================================================================== */

/* Misregistration. On hover the plate separates into two offset copies of
   itself sitting under the real one — the image and its duplicates pulling
   apart. This is the site's whole premise, so it is the one place in the
   archive worth spending motion on. */
.plate::before,
.plate::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: var(--src);
  background-size: cover;
  background-position: center;
  opacity: 0;
  pointer-events: none;
  transition: opacity 500ms var(--ease), transform 500ms var(--ease);
  /* Normal blend, not screen: these images are dark and the ground is near
     black, so a screen blend lightened almost nothing and the separation was
     invisible. Offset copies at plain opacity actually read as misregistration. */
  filter: grayscale(1) contrast(1.15);
}
.plate::before { transform: translate(0, 0); }
.plate::after  { transform: translate(0, 0); }

.plate:hover::before, .plate:focus-visible::before {
  opacity: 0.45; transform: translate(-11px, -5px);
}
.plate:hover::after, .plate:focus-visible::after {
  opacity: 0.34; transform: translate(11px, 5px);
}

/* The real image sits above its ghosts. */
.plate img { position: relative; z-index: 1; }

/* A register code per plate, arriving from the left on hover. */

/* The caption rises rather than just fading. */
.plate__cap { transform: translateY(0.5rem);
  transition: opacity 300ms var(--ease), transform 300ms var(--ease); }
.plate:hover .plate__cap, .plate:focus-visible .plate__cap {
  opacity: 1; transform: none;
}

/* A dropped frame: rare, brief, and never twice in the same place. */
.plate.is-dropping img { opacity: 0.18; filter: contrast(1.6) brightness(0.7); }
.plate.is-dropping::before { opacity: 0.4; transform: translate(-3px, 0); }

/* Lightbox navigation */
.lb__nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 3;
  background: transparent; border: 1px solid var(--ash); border-radius: 0;
  color: var(--smoke); font-family: var(--mono); font-size: 1rem;
  padding: 0.9rem 1.1rem; cursor: pointer;
  transition: border-color 300ms var(--ease), color 300ms var(--ease);
}
.lb__nav:hover { border-color: var(--oxblood); color: var(--bone); }
.lb__nav--prev { left: 1.5rem; }
.lb__nav--next { right: 1.5rem; }
.lb--single .lb__nav { display: none; }

.lb img.from-right { animation: lb-right 420ms var(--ease) both; }
.lb img.from-left  { animation: lb-left  420ms var(--ease) both; }
@keyframes lb-right {
  from { opacity: 0; transform: translateX(1.5rem) scale(0.99); }
  to   { opacity: 1; transform: none; }
}
@keyframes lb-left {
  from { opacity: 0; transform: translateX(-1.5rem) scale(0.99); }
  to   { opacity: 1; transform: none; }
}

@media (max-width: 40rem) {
  /* No hover on a phone, so the ghost separation never shows there. */
  .lb__nav { padding: 0.7rem 0.85rem; }
  .lb__nav--prev { left: 0.5rem; }
  .lb__nav--next { right: 0.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  .js .plate { opacity: 1; transform: none; }
  .plate::before, .plate::after { display: none; }
  .plate.is-dropping img { opacity: 0.9; filter: none; }
  .lb img.from-right, .lb img.from-left { animation: none; }
}

/* ======================================================================
   The archive, stylised
   ====================================================================== */

/* Each plate hangs at its own small angle. --tilt is deterministic per index,
   so the wall looks hand-pinned but never reshuffles between visits. */
.plate { transition: border-color 400ms var(--ease); }
.plate:hover, .plate:focus-visible {
  border-color: var(--smoke);
  z-index: 5;
}

/* The register code sits in the mount, always visible, like a catalogue
   sticker — it stops being a hover easter egg and starts being the label. */

/* Caption rises over the code on hover. */

/* An animated plate says so, so a still frame in a grid of stills is not
   mistaken for one. */

@media (max-width: 40rem) {
  .plates { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); gap: 3px; }
}

@media (prefers-reduced-motion: reduce) {
  .plate, .js .plate.is-in { transform: none; transition: none; }
  .plate:hover, .plate:focus-visible { transform: none; }
}

/* An animated plate announces itself as a mark rather than a label — a small
   triangle in the corner of the mount. A real element, not a pseudo: ::before
   and ::after are both spoken for by the misregistration ghosts, and reusing
   ::after would have broken the ghost on exactly the animated plates. */
.plate__motion {
  position: absolute;
  top: 0.55rem; right: 0.55rem;
  z-index: 3;
  width: 0; height: 0;
  border-left: 7px solid var(--oxblood);
  border-top: 5px solid transparent;
  border-bottom: 5px solid transparent;
  opacity: 0.85;
}

/* ======================================================================
   III. Territory — the generated survey sheet
   ====================================================================== */

.terr {
  position: relative;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--ash);
  overflow: hidden;
  background: var(--void);
}

.terr__map { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }

/* Text sits on the sheet the way it does on a printed map: no panels, no
   boxes, just type placed on the terrain. */
.terr__plate {
  position: absolute;
  left: 7%;
  top: 46%;
  transform: translateY(-50%);
}

.terr__label {
  margin: 0 0 0.35rem;
  font-family: var(--display);
  font-style: italic;
  font-size: clamp(0.95rem, 1.6vw, 1.35rem);
  color: var(--bone);
  opacity: 0.75;
}

.terr__word {
  margin: 0;
  font-family: var(--mono);
  font-weight: 400;
  font-size: clamp(1.5rem, 4.2vw, 3.25rem);
  letter-spacing: 0.02em;
  line-height: 1;
  color: var(--bone);
  text-shadow: 0 0 22px rgba(5, 5, 5, 0.75);
}

/* A leader line out of the wordmark to a fixed point, as a survey callout. */
.terr__word::after {
  content: "";
  position: absolute;
  left: 100%;
  top: 68%;
  width: clamp(2rem, 9vw, 8rem);
  height: 1px;
  margin-left: 0.9rem;
  background: var(--bone);
  opacity: 0.55;
}

.terr__fix {
  position: absolute;
  left: 7%;
  bottom: 8%;
  margin: 0;
  font-size: var(--step-label);
  letter-spacing: 0.16em;
  color: var(--bone);
  /* The sheet is deliberately near-black; without this the fix was sitting
     at the same value as the ground it was printed on. */
  text-shadow: 0 0 12px rgba(5, 5, 5, 0.9);
}
.terr__sub {
  display: block;
  margin-top: 0.4rem;
  color: var(--smoke);
}

.terr__corner {
  position: absolute;
  right: 6%;
  top: 34%;
  margin: 0;
  font-size: var(--step-label);
  letter-spacing: 0.14em;
  color: var(--smoke);
}

@media (max-width: 40rem) {
  .terr { aspect-ratio: 3 / 4; }
  .terr__plate { top: 40%; left: 8%; }
  .terr__fix { left: 8%; bottom: 7%; }
  .terr__corner { display: none; }
}

/* The deposit slot — the way into the curator, as the archive's next vacancy. */
.plate--deposit {
  display: grid;
  place-items: center;
  gap: 0.5rem;
  align-content: center;
  border: 1px dashed var(--ash);
  background: transparent;
  cursor: pointer;
  text-decoration: none;
  transition: border-color 400ms var(--ease), background 400ms var(--ease);
}
.plate--deposit:hover, .plate--deposit:focus-visible {
  border-color: var(--smoke);
  background: var(--crypt);
}
.plate__plus {
  font-family: var(--mono);
  font-size: 1.5rem;
  line-height: 1;
  color: var(--smoke);
  transition: color 400ms var(--ease), transform 500ms var(--ease);
}
.plate--deposit:hover .plate__plus { color: var(--bone); transform: rotate(90deg); }
.plate__deposit-label {
  font-size: 0.5625rem;
  letter-spacing: 0.22em;
  color: var(--ash);
  transition: color 400ms var(--ease);
}
.plate--deposit:hover .plate__deposit-label { color: var(--smoke); }

/* It is not a photograph, so it gets none of the plate treatments. */
.plate--deposit::before, .plate--deposit::after { display: none; }

/* Archive show / hide -------------------------------------------------- */

.eyebrow--split {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
}

.eyebrow__toggle {
  background: transparent;
  border: 0;
  padding: 0;
  border-radius: 0;
  font-family: var(--mono);
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--smoke);
  cursor: pointer;
  white-space: nowrap;
  transition: color 300ms var(--ease);
}
.eyebrow__toggle:hover { color: var(--bone); }

/* Collapsed by grid-rows rather than max-height, so it works whatever the
   sheet's height turns out to be. */
.plates {
  display: grid;
  transition: opacity 400ms var(--ease);
}
.plates.is-shut {
  display: none;
}

/* ======================================================================
   Depth and instrument frame
   ====================================================================== */

/* Parallax layers are promoted once rather than per frame. */
.hero__canvas, .hero__type, .hero__gen, .terr__map { will-change: transform; }
.terr__map { transform: scale(1.14); transform-origin: center; }

/* The page sits inside an instrument: hairline crop marks pinned to the
   viewport corners, present in every section rather than decorating one.
   This is the device that makes the sections read as one plate. */
.crop {
  position: fixed;
  z-index: 11;
  width: 14px; height: 14px;
  pointer-events: none;
  border-color: var(--ash);
  border-style: solid;
  border-width: 0;
  opacity: 0;
  animation: crop-in 1200ms var(--ease) 1500ms forwards;
}
.crop--tl { top: 12px; left: 12px; border-top-width: 1px; border-left-width: 1px; }
.crop--tr { top: 12px; right: 12px; border-top-width: 1px; border-right-width: 1px; }
.crop--bl { bottom: 12px; left: 12px; border-bottom-width: 1px; border-left-width: 1px; }
.crop--br { bottom: 12px; right: 12px; border-bottom-width: 1px; border-right-width: 1px; }
@keyframes crop-in { to { opacity: 1; } }

@media (max-width: 40rem) {
  .crop { width: 9px; height: 9px; }
  .crop--tl, .crop--tr { top: 7px; }
  .crop--bl, .crop--br { bottom: 7px; }
  .crop--tl, .crop--bl { left: 7px; }
  .crop--tr, .crop--br { right: 7px; }
}

@media (prefers-reduced-motion: reduce) {
  .hero__canvas, .hero__type, .hero__gen, .terr__map { transform: none !important; }
  .terr__map { transform: scale(1.14) !important; }
  .crop { animation: none; opacity: 1; }
}

/* The signal source — an official, licensed player, loaded on demand -------- */

.signal { margin-top: 2.5rem; }

.signal__open {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.75rem 1.25rem;
  background: transparent;
  border: 1px solid var(--ash);
  border-radius: 0;
  color: var(--smoke);
  font-family: var(--mono);
  font-size: var(--step-label);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  transition: border-color 300ms var(--ease), color 300ms var(--ease);
}
.signal__open:hover { border-color: var(--oxblood); color: var(--bone); }

.signal__dot {
  width: 6px; height: 6px;
  background: var(--smoke);
  transition: background 300ms var(--ease);
}
.signal__open[aria-expanded="true"] .signal__dot { background: var(--oxblood); }

/* The third-party player is walled off behind the site's own frame so it does
   not drag its styling into the page. */
.signal__frame {
  margin-top: 1.25rem;
  border: 1px solid var(--ash);
  background: var(--crypt);
  line-height: 0;
}
.signal__frame[hidden] { display: none; }
.signal__frame iframe { display: block; width: 100%; border: 0; }

.signal__note {
  margin: 0.9rem 0 0;
  font-size: var(--step-label);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--smoke);
  max-width: var(--measure);
}

/* The generated night city, fixed behind everything ---------------------- */
.bg-city {
  position: fixed;
  inset: 0;
  width: 100vw; height: 100vh;
  z-index: 0;
  pointer-events: none;
  opacity: 0;
  transition: opacity 1600ms var(--ease);
}
.bg-city.is-lit { opacity: 0.30; }

/* Content floats above the ground. The nav is deliberately NOT in this list:
   it is position:fixed, and this rule was overriding that and letting the bar
   scroll away with the page. */
main, .colophon { position: relative; z-index: 1; }
.hero { z-index: 2; }

/* The territory section keeps its annotations but no longer carries its own
   picture — it is a plate laid on the page's own ground. */
.terr--open {
  border: 1px solid var(--ash);
  aspect-ratio: 21 / 9;
  /* Solid, not a scrim: the territory plate masks the contour field entirely
     so the section reads as a blank sheet with only its annotations on it. */
  background: var(--void);
}

@media (prefers-reduced-motion: reduce) {
  .bg-city { transition: none; }
}

/* The way in, stated as a broken register entry ------------------------- */

.nav__write {
  color: var(--smoke);
  text-decoration: none;
  transition: color 300ms var(--ease);
  position: relative;
}
.nav__write:hover, .nav__write:focus-visible { color: var(--bone); }

/* On hover the label fails and re-reads — the anchor text is corrupt in the
   same way everything else on this page is a degraded copy. */
.nav__write::after {
  content: attr(data-glitch);
  position: absolute;
  left: 0; top: 0;
  color: var(--oxblood);
  clip-path: inset(0 0 0 0);
  opacity: 0;
  pointer-events: none;
}
.nav__write:hover::after { animation: wr1te 620ms steps(2, end) 2; opacity: 1; }
@keyframes wr1te {
  0%   { transform: translate(0, 0);        clip-path: inset(0 0 62% 0); opacity: 0.9; }
  25%  { transform: translate(-2px, 1px);   clip-path: inset(48% 0 22% 0); }
  50%  { transform: translate(2px, -1px);   clip-path: inset(20% 0 55% 0); }
  75%  { transform: translate(-1px, 0);     clip-path: inset(70% 0 4% 0); }
  100% { transform: translate(0, 0);        clip-path: inset(0 0 100% 0); opacity: 0; }
}

/* The hero starts below the bar rather than under it. */
.hero { padding-top: 7.5rem; }

@media (max-width: 40rem) {
  .nav__write { order: -1; }
  .hero { padding-top: 8.5rem; }
}

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

/* Text always sits on solid ground ------------------------------------- */

/* The contour field is atmosphere, never a surface to read off. Each band
   carries its own near-opaque backing so type keeps full contrast no matter
   what is happening behind it. */
/* Alternating ground. Solid bands and open bands take turns down the page, so
   the contour field surfaces and submerges instead of sitting behind
   everything at one flat value. Open bands still carry enough backing that
   type keeps its contrast. */
.band {
  background: rgba(5, 5, 5, 0.965);
  backdrop-filter: blur(2.5px);
  -webkit-backdrop-filter: blur(2.5px);
}
/* main's children run: hero(1) archive(2) transmission(3) doctrine(4)
   territory(5). Odd bands carry the plan; even bands are solid black. */
main > section.band:nth-child(odd) {
  background: rgba(5, 5, 5, 0.62);
  backdrop-filter: blur(1px);
  -webkit-backdrop-filter: blur(1px);
}
main > section.band:nth-child(even) {
  background: rgba(5, 5, 5, 0.985);
}
.colophon { background: rgba(5, 5, 5, 0.97); }

/* The hero is the one place the ground is meant to show. */
.hero { background: transparent; }

/* ======================================================================
   Mobile: touch ergonomics
   ====================================================================== */

/* Every nav control measured 14-20px tall — far under the ~44px needed to be
   hit reliably with a thumb. The type stays small; the hit area does not. */
.nav__write, .nav__audio, .nav__enter, .eyebrow__toggle {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
}

@media (max-width: 40rem) {
  .nav {
    padding: 0.35rem 1rem;
    gap: 0 0.9rem;
    align-items: center;
  }
  .nav__mark { min-height: 44px; display: inline-flex; align-items: center; }
  .nav__right { gap: 0.85rem; align-items: center; }

  /* The mark and the controls each get a full row rather than being crushed
     onto one line at 10px. */
  .nav__mark { flex: 1 1 100%; }
  .nav__right { flex: 1 1 100%; justify-content: space-between; margin-left: 0; }

  .hero { padding-top: 9.5rem; }

  /* 21:9 collapsed to a 150px slit on a phone. */
  .terr--open { aspect-ratio: 4 / 3; }
  .terr__word { font-size: clamp(1.35rem, 8vw, 2.4rem); }
  .terr__plate { top: 38%; }

  /* Give the sheet a little more room per image. */
  .plates { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 4px; }

  /* The archive toggle is a primary control on a phone. */
  .eyebrow--split { align-items: center; }

  /* Lightbox controls need thumbs, not cursors. */
  .lb__shut, .lb__nav { min-height: 44px; min-width: 44px;
                        display: inline-flex; align-items: center; justify-content: center; }
}

/* Pointer-coarse devices anywhere, not just narrow ones. */
@media (pointer: coarse) {
  .plate { min-height: 44px; }
  .signal__open { min-height: 48px; }
}
