/* =============================================================
   EXPLORE THE SILO — style.css
   Step 1: structural layout only. No visual design yet.
   Colour, texture and type arrive in step 4 (regions.css);
   all animation lives in motion.css.
   ============================================================= */

/* =============================================================
   FONTS, SERVED (docs/17 Stage 7, step 2)
   The site loaded no fonts at all: a Mac with Oswald installed painted
   Oswald, and everywhere else titles fell to Arial Narrow or plain sans
   and reflowed. Latin subset, regular only - the only weight either stack
   uses - about 22 KB for both. swap, so text never waits on a font.
   Licences: assets/fonts/FONTS-LICENSE.md.
   ============================================================= */
@font-face {
  font-family: "Oswald";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/oswald-400-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: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ibm-plex-mono-400-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;
}

:root {
  --font-display: "Elephantmen", "Oswald", "Arial Narrow", sans-serif;
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* One monospace everywhere. IBM Plex Mono when it is served; until then the
     system mono - SF Mono or Menlo on a Mac, Consolas on Windows - rather
     than whatever the browser calls generic monospace. */
  --font-mono: "IBM Plex Mono", "SF Mono", ui-monospace, Menlo, Consolas, "Liberation Mono", monospace;
  /* One text treatment everywhere (docs/08 §B): the scrim never inverts
     by region — a pale scrim over the pale Up Top erased half the frame. */
  --paper: #E8E2D6;
  --shadow: #141311;
  /* Named in docs/11, values lifted straight from the region palettes in
     docs/02 — no colour here that is not already in the silo. */
  --dust: #C4BCAC;          /* Up Top --surface-deep */
  --concrete-mid: #7C776C;  /* The Mids --structure */
  --rust: #6B4A33;          /* Down Deep --structure */
  --amber: #C8873A;         /* Down Deep --accent */
  --void: #080B10;          /* the Digger Void's own black, sampled */
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
}

body {
  font-family: var(--font-body);
  line-height: 1.6;
  background: #0b0b0c;
  color: var(--paper);
}

/* ---- Scroll track --------------------------------------------
   Height is set from JS: reserved scene distance + the sum of
   every entry's weight. It carries no visible content of its own. */
/* Stacking (docs/05 §7): canvas 0, scroll container 1, text column 2,
   controls 10. Explicit, never DOM order. */
#scroll-track {
  position: relative;
  z-index: 1;
  width: 100%;
}

#scene-surface { height: 100vh; }
#scene-descent { height: 150vh; }
#scene-ramp    { height: 292vh; }   /* 220 until the ramp was doubled (2026-09-21) */
/* 200vh, not 100. The last beat - down off the hall view, across the
   water and out through the mouth of the bore - is END_BEAT_VH = 100vh
   of scroll in silo3d.js, and a page cannot scroll through its own last
   viewport. At 100vh this element reserved the viewport and left nothing
   to travel: the maximum scroll position landed exactly on endT = 0, so
   the camera never left the hall and the tunnel and its door could not
   be reached at all. 100vh for the viewport, 100vh for the beat. */
#scene-end     { height: 200vh; }

/* ---- Interior: two fixed columns ------------------------------ */
#interior {
  position: fixed;
  inset: 0;
  z-index: 2;
  display: grid;
  grid-template-columns: 60fr 40fr;
  pointer-events: none;           /* let the page scroll through it */
}
#interior .column {
  position: relative;
  z-index: 2;                     /* interface above the 3D canvas */
  height: 100vh;
  overflow: hidden;
}
/* The 3D canvas (js/silo3d.js) fills the viewport edge to edge behind
   everything: the far wall of the shaft is the background behind the
   text, the stair sits on the right. z-index 0 and no pointer events,
   so it can never cover the interface or swallow a click. */
.silo-canvas {
  position: fixed;
  inset: 0;
  z-index: 0;
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Post treatment (docs/05 §6): film grain at 4%, radial vignette to
   35% at the corners. Above the canvas, below the text. */
#grain-overlay {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(ellipse at center, rgba(0,0,0,0) 48%, rgba(0,0,0,0.35) 100%);
}
#grain-overlay::after {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.04;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 240px 240px;
}
/* Text scrim (docs/08 §B): always dark, always light text. Above the
   canvas (0), below the text (2). It used to end at 44% of the width,
   while the body's lines run to about 41% - so the right end of every
   line sat over almost bare scene, and measured over fifteen levels,
   5.8% of the text area (11.7% at the Bazaar) was below 4.5:1 against
   the paper colour, halo aside. Ending at 62% it is 0.27% (2.0% at
   worst), for a 5% darkening of the stair's half of the frame. */
#text-scrim {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  width: 100vw;
  z-index: 1;
  pointer-events: none;
  /* Lighter and narrower (docs/25 §16): at 0.92 fading out by 62% of the width
     half the silo sat under it. The text halo carries the contrast the scrim
     gives up. */
  background: linear-gradient(to right,
    rgba(20,19,17,0.74) 0%,
    rgba(20,19,17,0.58) 24%,
    rgba(20,19,17,0.30) 36%,
    rgba(20,19,17,0.00) 47%);
}
/* On a held beat - Judicial, IT, the Bazaar - the frame behind the card is at its
   brightest, and the body text measured under 4.5:1 there (docs/31 #29). A second
   layer comes up for the beat only and goes again after it. */
#text-scrim::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(to right,
    rgba(20,19,17,0.38) 0%,
    rgba(20,19,17,0.46) 24%,
    rgba(20,19,17,0.38) 36%,
    rgba(20,19,17,0.00) 50%);
  opacity: 0; transition: opacity 600ms cubic-bezier(0.4, 0, 0.2, 1);
}
:root.is-beat #text-scrim::after { opacity: 1; }
@media (prefers-reduced-motion: reduce) { #text-scrim::after { transition: none; } }
#interior .column > * { pointer-events: auto; }
/* Legibility over the far wall: a soft scrim behind the text column
   (docs/05 §8.9). The region palettes in step 4 refine the ink. */

/* Left column */
#info {
  display: flex;
  align-items: center;
}
.env-scene {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
.level-card {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  max-width: min(620px, 46vw);
  padding: 56px 48px 56px max(48px, 5vw);
  /* A halo hugs every glyph so a lamp face behind a word cannot break
     contrast where the gradient has thinned. */
  text-shadow:
    0 0 1px var(--shadow),
    0 0 3px var(--shadow),
    0 0 8px var(--shadow),
    0 0 16px var(--shadow);
}
.level-head {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  margin: 0 0 24px;
}
.level-number {
  font-family: var(--font-display);
  font-size: clamp(3.5rem, 7vw, 7rem);
  line-height: 0.9;
  letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
/* Filler entries render the number at roughly 40% of documented size */
.level-card.is-filler .level-number {
  font-size: clamp(1.6rem, 4.8vw, 4rem);
}
.level-region {
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
}
.level-name {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(1.6rem, 2.6vw, 2.4rem);
  line-height: 1.1;
  margin: 0 0 20px;
}
.level-nocanon {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  margin: -12px 0 20px;
}
/* A sentence to read, not a label: in the body face. It was a 13 px
   monospace paragraph, which reads slowly and looked like data. */
.level-note {
  font-family: var(--font-body);
  font-style: italic;
  font-size: 14px;
  line-height: 1.55;
  max-width: 27em;               /* the body's measure */
  padding: 0.5rem 0.75rem;
  border-left: 3px solid currentColor;
  margin: 0 0 20px;
}
.level-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 10px 8px;
  margin: 0 0 28px;
}
.level-tabs button,
.tip-button {
  font: inherit;
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  padding: 0.35rem 0.7rem;
  background: none;
  border: 1px solid currentColor;
  cursor: pointer;
  color: inherit;
}
.level-tabs button[aria-pressed="true"], .level-tabs button[aria-selected="true"] {
  text-decoration: underline;
}
/* Set by renderTabs when a card's tabs would wrap (only the Generator's four
   do: 546 px in a 477 px row). */
.level-tabs.is-dense { gap: 10px 6px; }
.level-tabs.is-dense button { font-size: 11px; letter-spacing: 0.02em; padding: 0.3rem 0.5rem; }
.level-body {
  font-size: 17px;
  line-height: 1.65;
  /* Measure of 62 characters. Not 62ch: the ch unit is the width of a
     "0" and proportional text averages ~0.44em per character, so 62ch
     would run to ~88 chars. 27em ≈ 62 real characters at 17px. */
  max-width: 27em;
  margin: 0 0 32px;
}
/* Filler summaries sit at 55% opacity. Size and opacity only — never blur. */
.level-card.is-filler .level-body {
  opacity: 0.55;
}
/* Metadata, set as text. They were bordered pills, which read as buttons
   and did nothing when pressed. */
.level-chips {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 4px 0;
}
.level-chips li {
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.03em;
  color: rgba(232, 226, 214, 0.8);
}
.level-chips li + li::before { content: "\00b7"; margin: 0 9px; opacity: 0.5; }
.level-chips li[data-kind="department"]::before { content: "DEPT · "; }

/* Right column: structure placeholder + depth gauge */
#structure {
  display: grid;
  grid-template-columns: 1fr 96px;
}
.structure-stage {
  position: relative;
  height: 100vh;
  overflow: hidden;
}

/* ---- The cross-section --------------------------------------
   Colours reference the region palette variables (step 4) with
   neutral fallbacks. Per-segment stroke-width and opacity on the
   stair are set as attributes from JS — do not set them here, a
   CSS rule would override every segment with one value. */
.silo-svg {
  display: block;
  width: 100%;
  height: 100%;
}
.silo-svg.is-hidden { display: none; }   /* 3D backend active */
.silo-shell rect {
  fill: var(--structure, #8a8a8a);
  opacity: 0.35;
}
.silo-plates rect {
  fill: var(--structure, #8a8a8a);
  opacity: 0.7;
}
.silo-plates rect.is-filler { opacity: 0.45; }
.silo-core {
  fill: var(--structure, #8a8a8a);
  opacity: 0.55;
}
.silo-stair line {
  stroke: var(--structure, #9a9a9a);
  /* Butt caps: with round caps the semi-transparent segments overlap
     at every joint and stack into a visible bead pattern. */
  stroke-linecap: butt;
}
.silo-treads line {
  stroke: var(--structure, #9a9a9a);
  stroke-linecap: butt;
}
.silo-landings line {
  stroke: var(--structure, #9a9a9a);
  stroke-width: 5;
  stroke-linecap: butt;
}
.silo-labels text {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  fill: currentColor;
  opacity: 0.35;
}
.silo-labels text.is-canon   { opacity: 0.85; }
.silo-labels text.is-current { opacity: 1; font-weight: bold; }

/* The horizontal rule and floor number that sat over the structure were
   removed on request; the depth gauge carries the position now. */

/* Depth gauge */
/* The gauge sits against the viewport edge, so it carries its own
   right-hand clearance (--gauge-edge) for scrollbars and rounding.
   Everything inside is centred on the remaining width. */
.depth-gauge {
  --gauge-edge: 24px;
  position: relative;
  height: 100vh;
  padding: 4vh var(--gauge-edge) 4vh 0;
  overflow: hidden;              /* nothing may leak past the viewport edge */
}
.gauge-ticks {
  position: absolute;
  top: 4vh;
  bottom: 12vh;
  left: calc((100% - var(--gauge-edge)) / 2);
  width: 1px;
  background: currentColor;
  opacity: 0.35;
}
.gauge-ticks .tick {
  position: absolute;
  left: -3px;
  width: 7px;
  height: 1px;
  background: currentColor;
}
/* Documented levels = solid marks; filler = thin ticks */
.gauge-ticks .tick.is-canon  { left: -6px; width: 13px; height: 2px; }
.gauge-ticks .tick.is-filler { opacity: 0.5; }

/* The gauge is the silo, so it is also the way through it (Phase 10).
   Every documented level is a button on its own tick: the strip is narrow,
   so the hit area is 18px tall and reaches across the whole column while
   the mark stays the same size it was. */
.depth-gauge { pointer-events: auto; }
.gauge-jump {
  position: absolute;
  left: 0;
  right: var(--gauge-edge);
  height: 18px;                  /* replaced per target by layoutGaugeJumps */
  margin-top: 0;
  padding: 0;
  appearance: none;
  background: none;
  border: 0;
  cursor: pointer;
}
.gauge-jump::after {
  content: "";
  position: absolute;
  top: 50%;
  left: calc(50% - 9px);
  width: 18px;
  height: 2px;
  margin-top: -1px;
  background: currentColor;
  opacity: 0;
  transition: opacity 120ms ease;
}
.gauge-jump:hover::after, .gauge-jump:focus-visible::after { opacity: 0.9; }
.gauge-jump:focus-visible { outline: 1px solid rgba(232, 226, 214, 0.85); outline-offset: 1px; }
@media (prefers-reduced-motion: reduce) { .gauge-jump::after { transition: none; } }
.gauge-thumb {
  position: absolute;
  top: 4vh;
  left: calc((100% - var(--gauge-edge)) / 2);
  width: 10px;
  height: 10px;
  margin-left: -5px;
  margin-top: -5px;
  border: 2px solid currentColor;
  border-radius: 50%;
  will-change: transform;
}
.gauge-readout {
  position: absolute;
  bottom: 2vh;
  left: 0;
  right: var(--gauge-edge);
  margin: 0;
  padding: 0 4px;
  text-align: center;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.4;
  white-space: nowrap;           /* column is wide enough for "Down Deep" */
}

/* ---- Below 900 px (docs/00 Phase 12) ---------------------------
   The canvas is edge to edge behind everything at every width, so the
   right-hand column only has to hold the depth gauge. Before this block
   the card kept its desktop cap of 46vw - 172 px on a 375 px phone - and
   with its padding the text ran one word to a line, 1,712 px tall. */
@media (max-width: 900px) {
  #interior { grid-template-columns: 1fr 30px; }
  #structure { grid-template-columns: 0 30px; }
  .depth-gauge { --gauge-edge: 10px; }
  .structure-caption, .gauge-readout { display: none; }
  .level-card { max-width: min(560px, 62vw); padding: 28px 22px 28px 24px; }
  .level-head { margin-bottom: 14px; }
  .level-number { font-size: clamp(2.4rem, 8vw, 3.6rem); }
  .level-card.is-filler .level-number { font-size: clamp(1.4rem, 5vw, 2.2rem); }
  .level-name { font-size: clamp(1.3rem, 4vw, 1.8rem); margin-bottom: 14px; }
  .level-body { font-size: 15px; line-height: 1.55; margin-bottom: 20px; }
  .level-tabs { margin-bottom: 18px; }
  #intro { padding: 0 24px; }
  .intro-title { font-size: clamp(2.6rem, 13vw, 5rem); }
  .intro-sub { font-size: 12px; letter-spacing: 0.1em; }
  .intro-scroll { left: 24px; bottom: calc(28px + env(safe-area-inset-bottom)); }
}
/* Portrait phones and tablets: the card is a sheet along the bottom, the
   shaft is the top of the screen, and the scrim rises from below. The
   camera lifts its frame to match (silo3d.js, SHEET_LIFT). The body is
   cut to five lines; tapping the card opens the rest (main.js). */
@media (max-width: 900px) and (orientation: portrait) {
  #info { align-items: flex-end; }
  .level-card {
    max-width: none;
    width: 100%;
    max-height: 50vh;
    overflow: hidden;
    padding: 16px 16px calc(22px + env(safe-area-inset-bottom)) 18px;
  }
  .level-card.is-open { max-height: 78vh; overflow-y: auto; overscroll-behavior: contain; }
  .level-head { gap: 0.7rem; margin-bottom: 6px; }
  .level-name { margin-bottom: 10px; }
  /* No vertical padding on a clamped block: overflow clips at the padding
     edge, so padding showed a sliver of the line after the clamp. */
  .level-note { font-size: 13px; margin-bottom: 12px; padding-top: 0; padding-bottom: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  .level-body { display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 12px; cursor: pointer; }
  .level-card.is-open .level-body, .level-card.is-open .level-note,
  .level-card.is-measuring .level-body, .level-card.is-measuring .level-note { display: block; -webkit-line-clamp: unset; overflow: visible; }
  .level-tabs { flex-wrap: nowrap; overflow-x: auto; margin-bottom: 12px; scrollbar-width: none; }
  .level-chips { display: none; }
  .level-card.is-open .level-chips { display: flex; }
  .level-card.has-more::after {
    content: "MORE \25BE";
    position: absolute;
    right: 18px;
    bottom: calc(6px + env(safe-area-inset-bottom));
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.16em;
    opacity: 0.7;
  }
  #text-scrim {
    background: linear-gradient(to top,
      rgba(20,19,17,0.94) 0%,
      rgba(20,19,17,0.80) 38%,
      rgba(20,19,17,0.00) 62%);
  }
  .spoiler-hint { top: 104px; right: 10px; width: min(232px, calc(100vw - 20px)); }
  .ctl-label { font-size: 10px; letter-spacing: 0.1em; }
}
/* Landscape phones: 375 px tall, and the side card was 499. The same
   compact card as portrait, beside the shaft instead of under it. The
   camera keeps its desktop frame here. */
@media (max-width: 900px) and (orientation: landscape) and (max-height: 520px) {
  .level-card { max-height: 100vh; overflow: hidden; padding: 12px 18px 16px 20px; }
  .level-card.is-open { overflow-y: auto; overscroll-behavior: contain; }
  .level-head { gap: 0.6rem; margin-bottom: 2px; }
  .level-number { font-size: 2.1rem; }
  .level-card.is-filler .level-number { font-size: 1.4rem; }
  .level-name { font-size: 1.2rem; margin-bottom: 8px; }
  .level-note { font-size: 12px; margin-bottom: 8px; padding-top: 0; padding-bottom: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
  .level-body { font-size: 14px; line-height: 1.5; margin-bottom: 8px; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; cursor: pointer; }
  .level-card.is-open .level-body, .level-card.is-open .level-note,
  .level-card.is-measuring .level-body, .level-card.is-measuring .level-note { display: block; -webkit-line-clamp: unset; overflow: visible; }
  .level-tabs { flex-wrap: nowrap; overflow-x: auto; margin-bottom: 8px; scrollbar-width: none; }
  .level-tabs button { padding: 0.2rem 0.5rem; font-size: 11px; }
  .level-chips { display: none; }
  .level-card.is-open .level-chips { display: flex; }
  .level-card.has-more::after {
    content: "MORE \25BE";
    position: absolute;
    right: 18px;
    bottom: 2px;
    font-family: var(--font-mono);
    font-size: 10px;
    letter-spacing: 0.16em;
    opacity: 0.7;
  }
  .intro-title { font-size: clamp(2.2rem, 9vh, 3.4rem); }
  .intro-scroll { bottom: 16px; }
}


/* =============================================================
   PHASES (docs/08 §A)
   surface → descent → inside → end. Set on <html> by js/silo3d.js.
   The interface only exists inside the silo.
   ============================================================= */
#interior, #text-scrim {
  opacity: var(--ui-fade, 1);
  transition: opacity 260ms cubic-bezier(0.4, 0, 0.2, 1);
}
/* Hidden until the airlock: over the plain (surface, approach) and down the
   ramp tunnel the reader is not in the silo yet, and the card they would
   see is Level 1's. It arrives in `descent`, as the hall opens. */
:root[data-phase="surface"] #interior,
:root[data-phase="surface"] #text-scrim,
:root[data-phase="approach"] #interior,
:root[data-phase="approach"] #text-scrim,
:root[data-phase="ramp"] #interior,
:root[data-phase="ramp"] #text-scrim { opacity: 0; pointer-events: none; }
/* NOT AT THE DOOR (2026-09-21). The end stage used to be listed here too, and never once ran: it began at a scroll
   position the page cannot reach. When that was mended for the Census Record, this took the last card - and "The
   Door", the way into Silo 17 - off the screen at the very place it is for. The interface stays at the door. */

/* ---- Scene 1: the title over the dead plain ------------------- */
#intro {
  position: fixed;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 max(48px, 6vw);
  pointer-events: none;
  opacity: var(--intro-fade, 1);
  color: #1b1a17;                 /* dark type on the pale overcast */
}
:root[data-phase="inside"] #intro,
:root[data-phase="end"] #intro { display: none; }
.intro-title {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(3rem, 9vw, 9rem);
  line-height: 0.88;
  letter-spacing: -0.02em;
  margin: 0;
}
.intro-sub {
  font-family: var(--font-mono);
  font-size: 15px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  margin: 20px 0 0;
  opacity: 0.9;
}
.intro-scroll {
  position: absolute;
  left: max(48px, 6vw);
  bottom: 48px;
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.16em;
  display: flex;
  align-items: center;
  gap: 12px;
  opacity: 0.9;
  margin: 0;
}
.intro-arrow {
  width: 1px;
  height: 34px;
  background: currentColor;
  transform-origin: top;
  animation: introDrop 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
@keyframes introDrop {
  0%   { transform: scaleY(0.1); opacity: 0.2; }
  45%  { transform: scaleY(1);   opacity: 1; }
  100% { transform: scaleY(1);   opacity: 0; }
}

/* ---- The loader (docs/08 §E): the poster ----------------------------
   Measured on the 2000 x 1500 poster: the drawing runs from y 5.3% to 84.9%, the
   title and its underline below it. One effect only: the drawing fills with light
   from the top down - a window sliding down over a lit copy that is counter-shifted
   to stay put, set by js/silo3d.js as transforms, so the browser animates it off the
   main thread, which the build keeps busy. The title block stays as drawn. */
#loader {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: #050505;          /* the poster's own black, measured: anything else shows the poster as a band */
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: opacity 500ms ease;
}
.loader-poster {
  position: relative; width: min(100vw, 133.333vh); aspect-ratio: 4 / 3; flex: none;
  /* static edge fade, so the poster's grainy border never shows as a frame; it never animates */
  -webkit-mask-image: linear-gradient(to right, transparent, #000 9%, #000 91%, transparent), linear-gradient(to bottom, transparent, #000 4%, #000 96%, transparent);
  -webkit-mask-composite: source-in;
          mask-image: linear-gradient(to right, transparent, #000 9%, #000 91%, transparent), linear-gradient(to bottom, transparent, #000 4%, #000 96%, transparent);
          mask-composite: intersect;
}
.loader-poster img { position: absolute; inset: 0; width: 100%; height: 100%; display: block; user-select: none; }
/* Faded, not darkened: over a background of the poster's own black, opacity dims the lines and leaves the black
   exactly as it is - brightness() darkened the black too, and the lit title block showed as a brighter band. */
.lp-dim { opacity: 0.3; }
.lp-win {
  position: absolute; inset: 0; overflow: hidden;
  transform: translateY(-94.7%);              /* lit down to 5.3%: the top of the drawing */
  transition: transform 700ms cubic-bezier(0.25, 0.1, 0.25, 1);
  will-change: transform;
  -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
          mask-image: linear-gradient(to bottom, #000 calc(100% - 26px), transparent);
}
.lp-win .lp-lit { transform: translateY(94.7%); transition: transform 700ms cubic-bezier(0.25, 0.1, 0.25, 1); will-change: transform; }
.lp-title { clip-path: inset(86% 0 0 0); }     /* the title block is always lit */
/* The stage and the slow-network note are for screen readers only: the poster's own title block stays as drawn. */
.loader-stage, .loader-slow { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
#loader.is-done { opacity: 0; pointer-events: none; }
/* A tall narrow screen: crop the black sides instead of shrinking the silo to a sliver. */
@media (max-aspect-ratio: 4 / 5) { .loader-poster { width: 175vw; } }
@media (prefers-reduced-motion: reduce) {
  .lp-win, .lp-win .lp-lit { transition: none; }
}


/* =============================================================
   THE ENDING (docs/11)
   A cafeteria counter on the last landing. It continues out of the
   Digger Void: same black, no seam, and the only framed element is
   the board. Nothing is centred; everything sits on the level text's
   left margin.
   ============================================================= */
#tipjar {
  position: fixed;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  background: var(--void);
  opacity: var(--tip-fade, 0);
  pointer-events: none;
}
:root[data-phase="end"] #tipjar { pointer-events: auto; }

.tip-scene { position: absolute; inset: 0; width: 100%; height: 100%; }
.tip-grain {
  position: absolute;
  inset: 0;
  opacity: 0.04;
  pointer-events: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='240' height='240'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 1  0 0 0 0 1  0 0 0 0 1  0 0 0 1 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
  background-size: 240px 240px;
}

/* ---- The lamp and its pool. Hard edges, flat fills, no gradients. --- */
.tip-beam  { fill: var(--dust); opacity: 0.045; }
.tip-beam2 { fill: var(--dust); opacity: 0.05; }
.tip-lamp-stem    { fill: var(--concrete-mid); opacity: 0.5; }
.tip-lamp-housing { fill: var(--concrete-mid); opacity: 0.8; }
.tip-lamp-face    { fill: var(--paper); }
.tip-counter-top   { fill: var(--concrete-mid); opacity: 0.42; }
.tip-counter-front { fill: var(--shadow); opacity: 0.86; }
.tip-counter-worn  { fill: var(--dust); opacity: 0.14; }   /* worn smooth along the front edge */
.tip-pool          { fill: var(--dust); opacity: 0.13; }

/* ---- The cups: silo ceramics. One light, upper left. --------------- */
.tip-ceramic-lit  { fill: var(--dust); }
.tip-ceramic-dark { fill: var(--concrete-mid); }
.tip-ceramic-edge { fill: var(--dust); opacity: 0.45; }   /* reflected light */
.tip-rim          { fill: var(--concrete-mid); }
.tip-handle-dark  { fill: var(--concrete-mid); }
.tip-crema        { fill: var(--rust); }
.tip-crema-lit    { fill: none; stroke: var(--amber); stroke-width: 2.5; }
.tip-milk         { fill: var(--dust); }
.tip-swirl        { fill: var(--amber); opacity: 0.5; }
.tip-craze        { fill: none; stroke: var(--concrete-mid); stroke-width: 1; opacity: 0.55; }
.tip-chip         { fill: var(--void); }   /* a bite out of the rim, not a highlight */
.tip-cast         { fill: var(--shadow); opacity: 0.72; }   /* one hard cast shadow */
.tip-contact      { fill: var(--shadow); opacity: 0.9; }
.tip-stain        { fill: var(--rust); opacity: 0.34; }

/* ---- The copy: level-card margins, level-card type ----------------- */
.tip-inner {
  position: relative;
  z-index: 1;
  box-sizing: border-box;
  max-width: min(620px, 46vw);
  padding: 56px 48px 56px max(48px, 5vw);
}
.tip-eyebrow {
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin: 0 0 12px;
  opacity: 0.7;
}
.tip-head {
  font-family: var(--font-display);
  font-weight: normal;
  font-size: clamp(1.6rem, 2.6vw, 2.4rem);
  line-height: 1.1;
  margin: 0 0 20px;
}
.tip-body { font-size: 17px; line-height: 1.65; max-width: 27em; margin: 0 0 32px; }

/* The board is the only framed element on the page. */
.tip-board { border: 1px solid currentColor; padding: 20px 22px 24px; }
.tip-board-title {
  font-family: var(--font-mono);
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  margin: 0;
}
.tip-board-rule { border: 0; border-top: 1px solid currentColor; opacity: 0.35; margin: 12px 0 18px; }
.tip-item + .tip-item { margin-top: 26px; }
.tip-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-family: var(--font-mono);
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.1em;
  margin: 0;
}
.tip-dots { flex: 1 1 auto; border-bottom: 1px dotted currentColor; opacity: 0.5; transform: translateY(-3px); }
.tip-chits { white-space: nowrap; }
.tip-desc { font-size: 15px; opacity: 0.8; margin: 8px 0 2px; max-width: 26em; }
.tip-euro {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  opacity: 0.45;
  margin: 0 0 14px;
}
.tip-legal {
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.7;
  opacity: 0.45;
  margin: 32px 0 0;
  max-width: 60em;
}

/* ---- The buttons ARE the level tabs: same rule, same everything ---- */
.tip-stairs { width: 26px; height: 20px; overflow: visible; }
.tip-stairs path { transition: transform 320ms cubic-bezier(0.4, 0, 0.2, 1); }
.tip-button { display: inline-flex; align-items: center; gap: 12px; text-decoration: none; }
.tip-button:hover, .tip-button:focus-visible { border-color: var(--paper); }
.tip-button:hover .tip-stairs path,
.tip-button:focus-visible .tip-stairs path { transform: translate(8px, 5px); }


/* =============================================================
   THE CONTROLS (Phase 10)
   Above the scrim and the grain, below nothing. Mono caps in the
   site's own idiom; the panel is a list, not a dropdown, because
   twenty-seven entries do not fit in one.
   ============================================================= */
/* THE EVENT AREA. The gauge owns the right-hand edge of the viewport and
   is now clickable, so the controls sit clear of it: its 96px column plus
   its own 24px of edge, and the events stack under the reading controls
   rather than over the silo. */
.controls {
  position: fixed;
  top: 18px;
  right: calc(96px + 22px);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 6px;
  font-family: var(--font-mono);
}
.ctl {
  appearance: none;
  background: rgba(20, 19, 17, 0.72);
  color: var(--paper, #e8e2d6);
  border: 1px solid rgba(232, 226, 214, 0.28);
  border-radius: 2px;
  padding: 7px 11px;
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.13em;
  cursor: pointer;
  /* No backdrop blur (docs/34 P8): over a canvas that redraws every frame the browser re-blurred it each time, and
     at 72% opacity the blur was hardly visible. */
  transition: border-color 140ms ease, background 140ms ease;
}
.ctl:hover, .ctl:focus-visible { border-color: rgba(232, 226, 214, 0.6); background: rgba(20, 19, 17, 0.88); }
.ctl-caret {
  display: inline-block;
  width: 0; height: 0;
  margin-left: 8px;
  border-left: 3.5px solid transparent;
  border-right: 3.5px solid transparent;
  border-top: 4px solid currentColor;
  vertical-align: 2px;
  transition: transform 160ms ease;
}
.ctl[aria-expanded="true"] .ctl-caret { transform: rotate(180deg); }

.jump-menu {
  width: 232px;
  max-height: min(58vh, 520px);
  overflow-y: auto;
  background: rgba(16, 15, 14, 0.94);
  border: 1px solid rgba(232, 226, 214, 0.22);
  border-radius: 2px;
  padding: 4px 0;
}
.jump-menu button {
  appearance: none;
  display: flex;
  gap: 10px;
  width: 100%;
  background: none;
  border: 0;
  border-left: 2px solid transparent;
  color: rgba(232, 226, 214, 0.82);
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.06em;
  text-align: left;
  padding: 6px 12px;
  cursor: pointer;
}
.jump-menu button:hover, .jump-menu button:focus-visible {
  background: rgba(232, 226, 214, 0.08);
  color: var(--paper, #e8e2d6);
  outline: none;
}
.jump-menu button[aria-current="true"] { border-left-color: rgba(232, 226, 214, 0.75); color: var(--paper, #e8e2d6); }
.jump-menu .jm-n { flex: 0 0 30px; opacity: 0.6; }
.jump-menu .jm-name { flex: 1 1 auto; }
.jump-menu .jm-region {
  margin: 8px 12px 2px;
  font-size: 10px;
  letter-spacing: 0.16em;
  color: rgba(232, 226, 214, 0.55);
}
.jump-menu .jm-region:first-child { margin-top: 4px; }

/* ---- Keyboard (roadmap Phase 12) --------------------------------
   The skip link sits above everything, off screen until it has focus.
   The level tabs had no focus mark of their own, and the menu's buttons
   turned the outline off for a 0.08 background. */
.skip-link {
  position: fixed;
  top: 10px;
  left: 10px;
  z-index: 30;
  padding: 8px 12px;
  background: rgba(16, 15, 14, 0.96);
  color: var(--paper, #e8e2d6);
  border: 1px solid rgba(232, 226, 214, 0.6);
  font-family: var(--font-mono);
  font-size: 12px;
  letter-spacing: 0.1em;
  text-decoration: none;
  transform: translateY(-200%);
}
/* Keyboard focus only (docs/25 §23): on :focus a mouse click that landed focus
   on it left it showing in the corner. */
.skip-link:focus-visible { transform: none; outline: 2px solid var(--paper, #e8e2d6); outline-offset: 2px; }
.level-tabs button:focus-visible { outline: 2px solid var(--paper, #e8e2d6); outline-offset: 3px; }
.jump-menu button:focus-visible { border-left-color: var(--paper, #e8e2d6); background: rgba(232, 226, 214, 0.14); }
.ctl:focus-visible { outline: 1px solid rgba(232, 226, 214, 0.85); outline-offset: 2px; }
/* Read, not seen: the level announcer. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

@media (max-width: 900px) { .controls { top: 10px; right: 40px; } .jump-menu { width: 196px; } }
@media (prefers-reduced-motion: reduce) { .ctl, .ctl-caret { transition: none; } }

/* ---- Spoiler control (Phase 10) -------------------------------- */
.ctl-row { display: flex; align-items: center; gap: 8px; }
/* Type floor (docs/17 Stage 7): 11 px and 70%+ for anything a reader must read. */
.ctl-label { font-size: 11px; letter-spacing: 0.14em; color: rgba(232, 226, 214, 0.74); }
.spoiler-set { display: flex; gap: 3px; }
.ctl-s { padding: 6px 8px; letter-spacing: 0.08em; opacity: 0.72; }
.ctl-s[aria-pressed="true"] { opacity: 1; border-color: rgba(232, 226, 214, 0.62); background: rgba(232, 226, 214, 0.14); }

/* ---- The Blackout control (Phase 11) ---------------------------
   Wider than the others and stacked, because it is the only control
   here that does something to the world rather than to the reading. */
.ctl-black {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 3px;
  line-height: 1.2;
  border-color: rgba(255, 149, 60, 0.34);
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 260ms ease, transform 260ms ease, border-color 140ms ease, background 140ms ease;
}
.ctl-black.is-in { opacity: 1; transform: none; }
.ctl-black:hover, .ctl-black:focus-visible { border-color: rgba(255, 149, 60, 0.72); }
.ctl-black-s { font-size: 11px; letter-spacing: 0.12em; color: rgba(255, 168, 90, 0.82); }
/* The outage is held now, not timed: it stays out until the reader switches it
   back, so the control must still look pressable and must not carry a progress
   bar to the end of a length it no longer has. */
.ctl-black.is-running { border-color: rgba(255, 149, 60, 0.75); background: rgba(50, 26, 8, 0.86); cursor: pointer; }
.ctl-black.is-running::after { content: none; }
.ctl-black.is-running .ctl-black-t { color: rgba(255, 176, 96, 0.95); }
/* UNAVAILABLE, NOT GONE. An event that cannot run on this floor stays in
   its place in the stack, dimmed, and its second line says where it can
   run. Hiding it made the Event Area change shape from floor to floor and
   left the reader no way to know the event existed. Specific enough to
   override each event's own colour. */
.ctl-black.is-off { cursor: default; border-color: rgba(232, 226, 214, 0.14); background: rgba(20, 19, 17, 0.55); }
/* Dimmed, but the line that says where it runs stays readable. */
.ctl-black.is-off .ctl-black-t { color: rgba(232, 226, 214, 0.55); }
.ctl-black.is-off .ctl-black-s { color: rgba(232, 226, 214, 0.74); }
.ctl-black.is-off:hover, .ctl-black.is-off:focus-visible { border-color: rgba(232, 226, 214, 0.32); }

/* ---- The visor control (docs/19 §3) -------------------------------
   The Blackout's shape in the green of a sensor feed that is lying to
   you. It is the only control that appears on the surface, so it also has
   to survive the pale overcast behind it: a darker ground than the other
   two. */
.ctl-visor { border-color: rgba(96, 170, 96, 0.40); background: rgba(14, 20, 14, 0.80); }
.ctl-visor:hover, .ctl-visor:focus-visible { border-color: rgba(130, 210, 130, 0.78); }
.ctl-visor .ctl-black-s { color: rgba(140, 205, 140, 0.82); }
.ctl-visor.is-running { border-color: rgba(130, 210, 130, 0.8); background: rgba(16, 34, 16, 0.9); }
.ctl-visor.is-running .ctl-black-t { color: rgba(196, 236, 196, 0.96); }

/* ---- The Flood control (docs/19 §2) -------------------------------
   The Blackout's shape in the cold green-blue of standing water, so two
   events stacked in the same corner are told apart at a glance. */
.ctl-flood { border-color: rgba(96, 160, 170, 0.34); }
.ctl-flood:hover, .ctl-flood:focus-visible { border-color: rgba(120, 190, 200, 0.72); }
.ctl-flood .ctl-black-s { color: rgba(130, 190, 200, 0.82); }
.ctl-flood.is-running { border-color: rgba(120, 190, 200, 0.75); background: rgba(12, 30, 34, 0.88); }
.ctl-flood.is-running .ctl-black-t { color: rgba(186, 226, 232, 0.95); }

/* ---- The Race to the Top (docs/29 §2) -----------------------------
   The Blackout's shape in the pale grey of a porter's chit: this is not
   an event that happens to the silo, it is work the reader is asked to
   do. The clock and the record it issues carry the same type as the
   level signage - mono, stencilled caps, no colour of their own. */
.ctl-race { border-color: rgba(214, 208, 196, 0.34); }
.ctl-race:hover, .ctl-race:focus-visible { border-color: rgba(232, 226, 214, 0.72); }
.ctl-race .ctl-black-s { color: rgba(214, 208, 196, 0.82); }
.ctl-race.is-running { border-color: rgba(232, 226, 214, 0.8); background: rgba(26, 25, 22, 0.9); }
.ctl-race.is-running .ctl-black-t { color: rgba(240, 234, 222, 0.96); }

/* The clock. Top centre of the picture, clear of the level card and the
   Event Area, and nothing else on screen changes while a run is on. */
.race-clock {
  position: fixed;
  top: 18px; left: min(640px, 50vw); right: 140px;
  z-index: 6;
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  pointer-events: none;
  font-family: var(--font-mono);
  color: var(--paper, #e8e2d6);
  text-shadow: 0 1px 14px rgba(0, 0, 0, 0.85);
}
/* `hidden` has to win. A class that sets `display` beats the attribute's own
   user-agent rule, so without this the clock showed before any run had started
   and the record's full-screen ground sat over the page, swallowing every
   click. The gate did not catch it: it compares canvas frames, which never see
   the DOM. */
/* Both records share the card; `#census-record` carries the same class.
   The [hidden] guard covers every element that uses it - see the v468
   regression: a class that sets `display` beats the attribute. */
.race-clock[hidden], .race-record[hidden], #census-record[hidden] { display: none; }
/* The relic overlays join the same guard, for the same reason. */
.relic-bracket[hidden], #relic-card[hidden], .relic-logged[hidden] { display: none; }
/* The census card runs longer than the race chit: its rows are the descent. */
.cr-note { margin-top: 12px; font-size: 11px; line-height: 1.6; letter-spacing: 0.06em; color: rgba(232, 226, 214, 0.6); }
.cr-actions { margin-top: 16px; display: flex; gap: 18px; align-items: baseline; }
.cr-actions button { appearance: none; background: none; border: 0; padding: 0; font: inherit; letter-spacing: 0.18em; color: var(--paper, #e8e2d6); cursor: pointer; text-decoration: underline; }

/* ---- Tip the builder (docs/29 §4) ---------------------------------
   The only orange thing on the site that is not an emergency: black on
   amber, the one control that is allowed to look like a button. It sits
   below `.way-in` (bottom 96px) so the two never meet, above the scrim
   and below every overlay. On small screens the card becomes a bottom
   sheet, so it moves to the top left instead, clear of the controls. */
.tip-btn {
  position: fixed;
  left: max(48px, 6vw);
  bottom: 28px;
  z-index: 6;
  display: inline-block;
  padding: 9px 14px;
  background: #e08a2e;
  color: #14130f;
  border: 1px solid rgba(20, 19, 15, 0.35);
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.45);
  transition: background 140ms ease, transform 140ms ease;
}
.tip-btn:hover, .tip-btn:focus-visible { background: #f2a44e; transform: translateY(-1px); }
.tip-btn[hidden] { display: none; }
@media (max-width: 900px) {
  .tip-btn { left: 12px; top: calc(10px + env(safe-area-inset-top)); bottom: auto; font-size: 10px; padding: 7px 11px; }
}
@media (prefers-reduced-motion: reduce) { .tip-btn { transition: none; } }

/* The same thing on the record at the door: part of the paper, a size up. */
.tip-big {
  display: block;
  margin-top: 16px;
  padding: 12px 14px;
  background: #e08a2e;
  color: #14130f;
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 13px;
  letter-spacing: 0.18em;
  text-align: center;
  text-decoration: none;
  transition: background 140ms ease;
}
.tip-big:hover, .tip-big:focus-visible { background: #f2a44e; }
@media (prefers-reduced-motion: reduce) { .tip-big { transition: none; } }
.rc-time { font-size: clamp(26px, 3.6vw, 44px); letter-spacing: 0.08em; font-variant-numeric: tabular-nums; }
.rc-sub { font-size: 11px; letter-spacing: 0.18em; color: rgba(232, 226, 214, 0.72); }
@media (max-width: 900px) { .race-clock { left: 12px; right: 12px; top: 10px; } }

/* The record issued at the top: a chit, not a trophy. Dark ground, one
   hairline rule, stencilled caps, a stamp. Dismissed by a click anywhere. */
.race-record {
  position: fixed; inset: 0; z-index: 8;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8, 8, 7, 0.72);
  font-family: var(--font-mono);
  color: var(--paper, #e8e2d6);
}
.rr-card {
  width: min(92vw, 420px);
  padding: 22px 24px 18px;
  background: rgba(18, 17, 15, 0.97);
  border: 1px solid rgba(232, 226, 214, 0.26);
  border-radius: 2px;
  text-align: left;
}
.rr-head { font-size: 11px; letter-spacing: 0.22em; color: rgba(232, 226, 214, 0.62); }
.rr-title { margin: 10px 0 14px; font-size: 15px; letter-spacing: 0.18em; text-transform: uppercase; }
.rr-rule { height: 1px; background: rgba(232, 226, 214, 0.28); margin: 0 0 14px; }
.rr-row { display: flex; justify-content: space-between; gap: 16px; font-size: 12px; letter-spacing: 0.08em; padding: 3px 0; }
.rr-row span:first-child { color: rgba(232, 226, 214, 0.58); }
.rr-time { font-size: clamp(30px, 4.4vw, 42px); letter-spacing: 0.06em; font-variant-numeric: tabular-nums; margin: 6px 0 12px; }
.rr-stamp {
  margin-top: 16px; padding: 6px 10px; display: inline-block;
  border: 1px solid rgba(214, 208, 196, 0.5); border-radius: 2px;
  font-size: 10px; letter-spacing: 0.24em; color: rgba(232, 226, 214, 0.78);
  transform: rotate(-2deg);
}
.rr-foot { margin-top: 16px; font-size: 10px; letter-spacing: 0.12em; color: rgba(232, 226, 214, 0.42); }
.rr-x { margin-top: 14px; appearance: none; background: none; border: 0; padding: 0; font: inherit; letter-spacing: 0.18em; color: var(--paper, #e8e2d6); cursor: pointer; text-decoration: underline; }

/* ---- The Confiscation Record (docs/29 §4) -------------------------
   The find card is one item; this is the shelf it goes on. Wider than the
   other records, because 36 slots and a description have to stand side by
   side, and it folds to one column where that will not fit. An empty slot
   carries its number and nothing else: no name, no outline, nothing that
   would tell a reader what is still out there. */
/* The Record's own control. A row, not the Event Area's stacked block: with
   two stacked lines it ran the full width of the rail and read as a caption
   rather than a button, which is exactly how it was missed. Never hidden -
   the hunt has to be findable before anything has been found. */
.ctl-record { display: flex; align-items: center; gap: 8px; }
.ctl-record-i { width: 13px; height: 13px; flex: 0 0 auto; }
.ctl-record-n {
  font-variant-numeric: tabular-nums;
  padding: 1px 5px; border-radius: 2px;
  background: rgba(232, 226, 214, 0.14);
  color: rgba(232, 226, 214, 0.92);
}

.cf-card { width: min(94vw, 720px); }
.cf-body { display: flex; gap: 20px; align-items: flex-start; }
.cf-grid {
  flex: 0 0 auto;
  display: grid; grid-template-columns: repeat(6, 1fr); gap: 6px;
  width: min(60vw, 336px);
}
.cf-slot {
  appearance: none; aspect-ratio: 1; padding: 0;
  display: flex; align-items: center; justify-content: center;
  background: rgba(232, 226, 214, 0.04);
  border: 1px solid rgba(232, 226, 214, 0.16);
  border-radius: 2px; color: var(--paper, #e8e2d6);
  cursor: default;
}
.cf-slot.is-found { cursor: pointer; background: rgba(232, 226, 214, 0.1); border-color: rgba(232, 226, 214, 0.34); }
.cf-slot.is-found:hover { background: rgba(232, 226, 214, 0.17); }
.cf-slot.is-on { border-color: rgba(232, 226, 214, 0.82); background: rgba(232, 226, 214, 0.2); }
/* Outstanding items sit at the edge of visible - present, so the reader knows
   the shape of what is missing, but plainly not theirs yet. Logging one brings
   it to full and lights its slot. */
.cf-glyph { width: 68%; height: 68%; opacity: 0.17; transition: opacity 160ms ease; }
.cf-slot.is-found .cf-glyph { opacity: 1; }
.cf-thumb { width: 90%; height: 90%; object-fit: contain; filter: brightness(0) invert(0.92); opacity: 0.16; transition: opacity 160ms ease; }
.cf-slot.is-found .cf-thumb { filter: none; opacity: 1; }
.cf-thumb-wait { display: block; width: 90%; height: 90%; }   /* holds the slot while its picture is made: no glyph first */
.cf-fade { animation: cf-fade-in 220ms ease both; }
@keyframes cf-fade-in { from { opacity: 0; } }
.cf-hero { display: block; width: 100%; max-width: 240px; aspect-ratio: 1; object-fit: contain; margin: 4px auto 14px; }
.cf-hero-wait { background: rgba(232, 226, 214, 0.04); }
.cf-no { font-size: 10px; letter-spacing: 0.04em; color: rgba(232, 226, 214, 0.22); font-variant-numeric: tabular-nums; }
.cf-detail { flex: 1 1 auto; min-width: 0; }
.cf-name { margin: 0 0 10px; font-size: 13px; letter-spacing: 0.16em; text-transform: uppercase; }
.cf-empty { font-size: 11px; letter-spacing: 0.12em; color: rgba(232, 226, 214, 0.42); line-height: 1.7; }
@media (max-width: 760px) {
  .cf-body { flex-direction: column; }
  .cf-grid { width: 100%; }
}

/* ---- Relics: the bracket and the find (docs/29 §4) ----------------
   A thin bracket, not a highlight: four corners drawn around whatever the
   pointer is over, with one lowercase-free label beside it. No glow, no
   icon, no sound - the object itself is the only thing that lights up, and
   it does that by standing in the lamplight. The bracket never intercepts
   the pointer (the scene owns picking), so it is pointer-events: none. */
.relic-bracket {
  position: fixed; z-index: 7; pointer-events: none;
  width: 46px; height: 46px; margin: -23px 0 0 -23px;
  --rb: rgba(232, 226, 214, 0.82);
  background:
    linear-gradient(var(--rb), var(--rb)) left top / 11px 1px no-repeat,
    linear-gradient(var(--rb), var(--rb)) left top / 1px 11px no-repeat,
    linear-gradient(var(--rb), var(--rb)) right top / 11px 1px no-repeat,
    linear-gradient(var(--rb), var(--rb)) right top / 1px 11px no-repeat,
    linear-gradient(var(--rb), var(--rb)) left bottom / 11px 1px no-repeat,
    linear-gradient(var(--rb), var(--rb)) left bottom / 1px 11px no-repeat,
    linear-gradient(var(--rb), var(--rb)) right bottom / 11px 1px no-repeat,
    linear-gradient(var(--rb), var(--rb)) right bottom / 1px 11px no-repeat;
}
.rb-label {
  position: absolute; left: calc(100% + 10px); top: 50%; transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: 0.22em;
  color: rgba(232, 226, 214, 0.82); text-shadow: 0 1px 6px rgba(0, 0, 0, 0.8);
}
@media (max-width: 900px) { .rb-label { display: none; } }

/* The find card is the record card: same paper, same rules. */
.rl-desc { margin-top: 12px; font-size: 12px; line-height: 1.7; letter-spacing: 0.04em; color: rgba(232, 226, 214, 0.78); }
.rl-class-1 { color: #d2706a; }
.rl-class-2 { color: #7ea6d6; }
.rl-class-3 { color: #d8b45c; }
.rl-class-4 { color: #8fbf86; }

/* One line, bottom left, gone in two seconds. Never a toast, never a badge. */
.relic-logged {
  position: fixed; left: max(48px, 6vw); bottom: 96px; z-index: 6;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.16em;
  color: rgba(232, 226, 214, 0.86); text-shadow: 0 1px 8px rgba(0, 0, 0, 0.85);
  transition: opacity 260ms ease;
}
.relic-logged.is-fading { opacity: 0; }
/* The only affordance: the pointer changes over a relic. The scene sets the
   class, so it is right even though the object is inside the canvas. */
html.on-relic, html.on-relic canvas { cursor: pointer; }
@media (max-width: 900px) { .relic-logged { left: 12px; right: 12px; bottom: 76px; font-size: 10px; } }
@media (prefers-reduced-motion: reduce) { .relic-logged { transition: none; } }

/* ---- The Depth Shake control and the rattle (docs/19 §1) ----------
   The Blackout's shape, in the cold grey of bare steel instead of
   emergency amber. The rattle moves the page's own columns and controls
   for the length of the drop: one heavy dip, then smaller and faster,
   then still. Transform only, so it never touches layout. */
.ctl-quake { border-color: rgba(190, 200, 205, 0.34); }
.ctl-quake:hover, .ctl-quake:focus-visible { border-color: rgba(205, 214, 220, 0.72); }
.ctl-quake .ctl-black-s { color: rgba(196, 206, 212, 0.82); }
.ctl-quake.is-running { border-color: rgba(205, 214, 220, 0.75); background: rgba(22, 26, 28, 0.88); }
.ctl-quake.is-running .ctl-black-t { color: rgba(226, 232, 236, 0.95); }
@keyframes quakeRattle {
  0%   { transform: none; }
  5%   { transform: translate(0, 7px); }
  11%  { transform: translate(-2px, -5px); }
  18%  { transform: translate(2px, 4px); }
  26%  { transform: translate(-2px, -3px); }
  35%  { transform: translate(1px, 3px); }
  46%  { transform: translate(-1px, -2px); }
  58%  { transform: translate(1px, 1.5px); }
  72%  { transform: translate(0, -1px); }
  86%  { transform: translate(0, 0.5px); }
  100% { transform: none; }
}
html.is-quaking #info, html.is-quaking .depth-gauge, html.is-quaking #controls { animation: quakeRattle 1.15s linear both; }
@media (prefers-reduced-motion: reduce) {
  html.is-quaking #info, html.is-quaking .depth-gauge, html.is-quaking #controls { animation: none; }
}

/* The gate itself: the level's own text is covered, never removed, so
   nothing about the layout moves when the reader changes the setting. */
.level-card.is-gated .level-name,
.level-card.is-gated .level-body,
.level-card.is-gated .level-chips,
.level-card.is-gated .level-tabs,
.level-card.is-gated .level-note { filter: blur(7px); opacity: 0.42; user-select: none; pointer-events: none; }
/* A warning, not a subtitle: amber rule, and a way to see it anyway. */
.level-gate {
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin: 14px 0 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  color: rgba(232, 226, 214, 0.9);
  border-left: 2px solid rgba(200, 135, 58, 0.85);
  padding-left: 10px;
}
.level-gate[hidden] { display: none; }
.level-gate button {
  appearance: none; background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; letter-spacing: 0.14em; color: var(--paper, #e8e2d6); text-decoration: underline;
}
.spoiler-hint::after {
  content: ""; position: absolute; top: 12px; right: -6px;
  border: 6px solid transparent; border-right: 0; border-left-color: rgba(232, 226, 214, 0.22);
}
:root:not([data-phase="surface"]):not([data-phase="approach"]) .spoiler-hint { display: none; }

/* Drag to look: the picture is grabbable once inside, where drag works. */
:root[data-phase="inside"] #env-scene, :root[data-phase="inside"] #structure-stage { cursor: grab; }
html.is-dragging, html.is-dragging * { cursor: grabbing !important; }
.drag-hint {
  position: fixed; left: min(640px, 50vw); right: 140px; bottom: 6vh; z-index: 5;
  margin: 0; text-align: center; pointer-events: none;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
  color: rgba(232, 226, 214, 0.85); text-shadow: 0 1px 12px rgba(0, 0, 0, 0.8);
  opacity: 0; transition: opacity 400ms ease;
}
.drag-hint.is-on { opacity: 1; }
@media (max-width: 900px) { .drag-hint { display: none; } }
.spoiler-hint {
  position: fixed;
  /* Beside S1-S3, pointing at them, not over the Event Area. */
  top: 14px; right: calc(96px + 22px + 272px);
  z-index: 6;
  width: 232px;
  margin: 0;
  padding: 10px 12px;
  background: rgba(16, 15, 14, 0.94);
  border: 1px solid rgba(232, 226, 214, 0.22);
  border-radius: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.55;
  letter-spacing: 0.04em;
  color: rgba(232, 226, 214, 0.8);
}
.spoiler-hint button {
  display: block; margin-top: 8px;
  appearance: none; background: none; border: 0; padding: 0;
  font: inherit; letter-spacing: 0.16em;
  color: var(--paper, #e8e2d6); cursor: pointer; text-decoration: underline;
}
@media (prefers-reduced-motion: reduce) { .level-card.is-gated .level-name,
  .level-card.is-gated .level-body { filter: none; opacity: 0.25; } }

/* =============================================================
   REGION TRANSITION (Phase 10)
   The rule grows across the viewport, the name rises into it, both hold,
   both leave. Nothing goes to black: the silo is still there behind it.
   ============================================================= */
.region-sweep {
  position: fixed;
  inset: 0;
  z-index: 5;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 18px;
  opacity: 0;
  transition: opacity 320ms ease;
}
.region-sweep.is-on { opacity: 1; }
.rs-rule {
  display: block;
  height: 1px;
  width: 0;
  background: rgba(232, 226, 214, 0.75);
  transition: width 560ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.region-sweep.is-on .rs-rule { width: min(72vw, 900px); }
.rs-name {
  font-family: var(--display, "Oswald", sans-serif);
  font-size: clamp(28px, 5.2vw, 68px);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--paper, #e8e2d6);
  text-shadow: 0 2px 26px rgba(0, 0, 0, 0.75);
  transform: translateY(10px);
  opacity: 0;
  transition: transform 460ms cubic-bezier(0.22, 0.61, 0.36, 1), opacity 460ms ease;
}
.region-sweep.is-on .rs-name { transform: none; opacity: 1; }
/* On desktop the sweep belongs to the picture, not the text. Centred on the
   viewport, its 66 px name sat 127x107 px deep in the level card's
   description. It now lives right of the card and below the Event Area's
   stack, clear of both. Under 900 px the card is a bottom sheet and the
   centre of the frame is picture, so the sweep stays where it was. */
@media (min-width: 901px) {
  .region-sweep { left: min(640px, 50vw); right: 140px; justify-content: flex-end; padding-bottom: 20vh; }
  .region-sweep.is-on .rs-rule { width: 100%; }
}

/* ---- Tab transition (Phase 10) --------------------------------- */
.level-body { transition: opacity 180ms ease, transform 180ms ease; }
.level-body.is-swapping { opacity: 0; transform: translateY(6px); }

/* ---- Card change (docs/17 Stage 7, step 4) -----------------------
   Every field used to swap at once, and on the fast filler floors the text
   strobed. The number stays put and updates at once; everything under it
   fades out briefly, swaps, and comes back in order - name, note and tabs,
   body, chips. Opacity and transform only. */
.level-card > :not(.level-head) { transition: opacity 220ms ease, transform 220ms ease; }
.level-card > .level-name { transition-delay: 0ms; }
.level-card > .level-gate, .level-card > .level-nocanon, .level-card > .level-note, .level-card > .level-tabs { transition-delay: 50ms; }
.level-card > .level-body { transition-delay: 90ms; }
.level-card > .level-chips { transition-delay: 130ms; }
.level-card.is-changing > :not(.level-head) { opacity: 0; transform: translateY(5px); transition-duration: 110ms; transition-delay: 0ms; }

/* ---- Event progress ----------------------------------------------
   A running event says how long it has left: a hairline that fills over
   the event's real duration (set per run as --run-ms). */
.ctl-black { position: relative; }
.ctl-black.is-running::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: rgba(232, 226, 214, 0.6); transform-origin: left center;
  animation: runProgress var(--run-ms, 1000ms) linear forwards;
}
@keyframes runProgress { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* ---- The way in: one quiet line over the silent stretch -----------
   Between the title fading (vh 130) and the Level 1 card (vh 385) nothing was
   written on screen. Over the plain, dark on the pale sky; in the tunnel,
   light on the dark. Above where the scroll hint fades out. */
.way-in {
  position: fixed; left: max(48px, 6vw); bottom: 96px; z-index: 3;
  margin: 0; pointer-events: none;
  font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.16em; text-transform: uppercase;
}
.way-in span { position: absolute; left: 0; bottom: 0; white-space: nowrap; opacity: 0; transition: opacity 600ms ease; }
.way-in [data-phase-for="approach"] { color: rgba(27, 26, 23, 0.85); }
.way-in [data-phase-for="ramp"] { color: rgba(232, 226, 214, 0.85); text-shadow: 0 1px 10px rgba(0, 0, 0, 0.7); }
:root[data-phase="approach"] .way-in [data-phase-for="approach"],
:root[data-phase="ramp"] .way-in [data-phase-for="ramp"] { opacity: 1; transition-delay: 250ms; }
/* Gone once the camera is through the airlock, not when the ramp phase ends
   (docs/30 #10). Same specificity as the rule above and later, so it wins. */
:root.is-walked-in .way-in [data-phase-for="ramp"] { opacity: 0; transition-delay: 0ms; }

@media (prefers-reduced-motion: reduce) {
  .level-card > :not(.level-head), .way-in span { transition: none !important; }
  .level-card.is-changing > :not(.level-head) { opacity: 1; transform: none; }
  .ctl-black.is-running::after { animation: none; transform: scaleX(1); opacity: 0.35; }
  .region-sweep, .rs-rule, .rs-name, .level-body { transition: none; }
  .region-sweep.is-on .rs-rule { width: min(72vw, 900px); }
}

/* Standing sits on the Record's own header line, small, opposite the
   department (docs/29 §5); the Notice is a ruled list, one clue a line. */
.cf-head { display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.cf-standing { color: rgba(232, 226, 214, 0.86); }
.cf-notice { list-style: none; margin: 10px 0 0; padding: 0; max-height: 300px; overflow-y: auto; }
.cf-notice li { display: flex; gap: 12px; font-size: 10.5px; letter-spacing: 0.08em; line-height: 1.5; padding: 4px 0; border-top: 1px solid rgba(232, 226, 214, 0.1); color: rgba(232, 226, 214, 0.78); }
.cf-notice-no { flex: 0 0 auto; color: rgba(232, 226, 214, 0.42); font-variant-numeric: tabular-nums; }
.cf-view, .cf-present { appearance: none; background: none; border: 0; padding: 0; font: inherit; letter-spacing: 0.18em; color: var(--paper, #e8e2d6); cursor: pointer; text-decoration: underline; }

/* Through the door (docs/31 B12): Silo 17 has no levels to jump to, no events
   and no depth to gauge. The card and the Record stay. */
.in-other-silo #jump-toggle, .in-other-silo #jump-menu,
.in-other-silo #blackout-btn, .in-other-silo #visor-btn, .in-other-silo #flood-btn,
.in-other-silo #quake-btn, .in-other-silo #race-btn,
.in-other-silo #depth-gauge { visibility: hidden; }
html.on-door, html.on-door canvas { cursor: pointer; }

/* Keyboard focus on the papers (docs/31 #29): the dismiss, save and view buttons,
   the Record's slots and the spoiler gate's button had no mark of their own. */
.rr-card button:focus-visible, .rr-card a:focus-visible, .level-gate button:focus-visible {
  outline: 2px solid var(--paper, #e8e2d6); outline-offset: 3px;
}
.cf-slot:focus-visible { outline: 2px solid var(--paper, #e8e2d6); outline-offset: 1px; }
.race-record:focus { outline: none; }

/* ---- Go out to clean (docs/31 #13) --------------------------------
   Outside, the page steps back: no card, no controls but the one that says
   what is happening, and the helmet around the view. The visor is a dark,
   slightly irregular rim with the glass's own curve in it; the breath is in
   motion.css. */
:root.is-cleaning #info, :root.is-cleaning #depth-gauge, :root.is-cleaning #text-scrim,
:root.is-cleaning #intro, :root.is-cleaning #tipjar, :root.is-cleaning #level-announce,
:root.is-cleaning #controls > *:not(#clean-btn) { visibility: hidden; }
.visor-frame {
  position: fixed; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(ellipse 62% 58% at 50% 50%, rgba(0,0,0,0) 0%, rgba(0,0,0,0) 72%, rgba(6,7,8,0.55) 86%, rgba(4,5,6,0.96) 100%),
    radial-gradient(ellipse 120% 80% at 50% 115%, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0) 60%);
}
.visor-frame::before {                      /* the glass: a faint reflection band across the top */
  content: ""; position: absolute; left: 12%; right: 12%; top: 7%; height: 18%;
  background: linear-gradient(to bottom, rgba(232,236,240,0.07), rgba(232,236,240,0));
  border-radius: 50% 50% 40% 40% / 100% 100% 30% 30%;
}
.visor-frame::after {                       /* breath on the glass, low in the view */
  content: ""; position: absolute; left: 22%; right: 22%; bottom: -6%; height: 34%;
  background: radial-gradient(ellipse at 50% 100%, rgba(225,230,232,0.22) 0%, rgba(225,230,232,0) 70%);
  opacity: 0.45;
}
.clean-hint {
  position: fixed; left: 50%; bottom: 7vh; transform: translateX(-50%); z-index: 2; margin: 0;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; letter-spacing: 0.2em;
  color: rgba(232, 226, 214, 0.86); text-shadow: 0 1px 10px rgba(0,0,0,0.85); pointer-events: none; white-space: nowrap;
}
.clean-stamp {
  position: fixed; left: 50%; top: 50%; z-index: 2; margin: 0; pointer-events: none;
  transform: translate(-50%, -50%) rotate(-3deg);
  padding: 10px 18px; border: 2px solid rgba(214, 208, 196, 0.7); border-radius: 2px;
  font-family: var(--font-mono, ui-monospace, monospace); font-size: 20px; letter-spacing: 0.28em;
  color: rgba(232, 226, 214, 0.92); background: rgba(18, 17, 15, 0.35); text-shadow: 0 1px 12px rgba(0,0,0,0.8);
}
:root.is-cleaning .silo-canvas { cursor: crosshair; }
@media (max-width: 640px) { .clean-hint { font-size: 10px; letter-spacing: 0.12em; white-space: normal; width: 86vw; text-align: center; } }
/* A finger on the lens wipes; it must not pan the page (which would abandon the cleaning). A wheel still scrolls away. */
:root.is-cleaning, :root.is-cleaning body { touch-action: none; overscroll-behavior: none; }
