/* GENERATED FILE — DO NOT EDIT.
     Written by landing/art/build-prototype.mjs from
     landing/art/prototype-template.html, which is the only file you edit.
     An edit made here survives until the next build and then disappears. */
/* ============================================================
   Open Brain — brain/spine art prototype, pass 4
   ------------------------------------------------------------
   The hand-drawn silhouette is gone. The brain is now sampled
   from an 1894 stipple engraving (PSM V46 D167, Sanger Brown,
   public domain) and the spine from a CT-derived vertebral
   column (NIH 3D 3DPX-016837, CC-BY, "My Segmenter"), with the
   ribs cropped away and a sagittal slab taken through it.

   Volume is no longer a lighting model I invented: each brain
   dot carries the tone the illustrator drew at that spot, and
   each spine dot carries Lambert-plus-rim off the real mesh
   normal. That is the whole reason for both pipelines.

   Palette lifted verbatim from carrel/src/app/globals.css. Dark only.
   (It used to say landing/styles.css, which was the pre-merge page's copy of
   the same values; that file was deleted 2026-08-27 with variant.html — see
   LAUNCH-PLAN.md 4.1 — so this names the source rather than an intermediate.)
   The brain is black and white; the only chroma on it is the
   chromatic aberration, and the only chroma near it is the
   cold cyan of the gel.

   No build step at RUNTIME, no dependencies, no external
   requests. The point clouds are inlined by build-prototype.mjs
   at authoring time.
   ============================================================ */

:root {
  --bg: #0b0d10;
  --surface: #14171c;
  --surface-2: #1c2027;
  --border: #262b33;
  --border-strong: #363d47;

  --ink: #e8eaed;
  --ink-muted: #9aa3af;
  /* Measured against `--bg` (#0b0d10): 4.66:1, where this was #626b78 and
     3.61:1 — under the 4.5:1 WCAG AA floor for text this size, and this token
     is not decoration. It sets the specimen's own numbers (48,320 characters,
     29 passages), the benchmark's bar labels, the hero's in/out headings, the
     terminal's output line and the CTA's fine print — all of them content,
     none of them above 13px. `--ink-muted` stays at 7.63:1, so the three-step
     hierarchy is intact and only the bottom step moved. */
  --ink-faint: #737d8b;

  --accent: #7dd3a0;
  --accent-dim: #2f6b4d;

  --gel: 128, 206, 232;
  /* The track stays narrow so the text is not pushed outward; the canvas
     gets much wider so the column can swing into the EMPTY half of each
     section. That empty half was the dead space — the column filling it is
     the whole point of bowing away from the paragraph. */
  --rail: 270px;         /* grid track — the gap the text keeps clear */
  /* ⚠️ THIS HAS TO TRACK THE VIEWPORT, because the stem does. The stem is
     drawn on the stage at min(w/SW, h/SH), so its width grows with the
     window; the column was drawn into a canvas pinned at 780px, and every
     length in placements() is a fraction of that canvas. Two halves of one
     drawing on two different scales can only agree at a single window width
     — the stem's neck comes out at 82px at a 1568 viewport and 63px at 1200
     while the bones stay put — so the join came apart on resize. */
  --rail-canvas: min(50vw, 880px);  /* drawing box — room for the full swing */

  --wrap: clamp(20rem, 38vw, 29rem);
  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
          "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "Cascadia Code", Consolas,
          "Liberation Mono", monospace;

  color-scheme: dark;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

.hero {
  position: relative;
  min-height: 100svh;
  display: grid;
  /* ⚠️ 1fr, AND the stage must keep an `auto` minimum (no min-height: 0 on
     it) — the two halves of one rule. The 1fr is what stops `min-height:
     100svh` leaving SLACK BELOW THE STAGE on a window taller than the art:
     the stem is drawn to the stage's bottom edge and the column starts at the
     hero's, so every pixel of that slack is a dead band between them. It
     measured 84px at 1064x1121 and read as the join simply being broken.
     The `auto` minimum is what stops the same row collapsing the stage back
     to whatever is left of the screen, which is what pinned the brain to a
     third of the viewport however large BSCALE was set. */
  grid-template-rows: auto 1fr;
  overflow: hidden;
}

.hero-copy {
  text-align: center;
  padding: clamp(2rem, 5vh, 3.5rem) 1.5rem clamp(.5rem, 2vh, 1rem);
  display: grid;
  gap: .7rem;
  justify-items: center;
}

.eyebrow {
  font-family: var(--mono);
  font-size: .7rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0;
}

h1 {
  font-size: clamp(1.9rem, 4.4vw, 3rem);
  line-height: 1.05;
  letter-spacing: -.03em;
  font-weight: 700;
  margin: 0;
  max-width: 22ch;
  text-wrap: balance;
}

.lede {
  margin: 0;
  color: var(--ink-muted);
  /* 44ch was set against a two-line lede. The real one is four sentences, and
     at 44ch it stacked six lines deep — which pushes the brain down the page
     and costs the hero most of what shrinking the brain just bought. */
  max-width: 62ch;
  text-wrap: balance;
}

/* ⚠️ THE STAGE CARRIES THE ART BOX'S ASPECT, and that is what makes the
   brain big. The fit is `min(w / SW, h / SH)`, so whichever axis is
   proportionally shorter decides the scale — and inside a 100svh hero it was
   always the height, pinning the brain to ~35% of the screen no matter what
   scale it was drawn at. Matching the aspect makes both terms equal: the fit
   becomes exactly w / SW, the brain spans the fraction of the WIDTH it is
   given, and the hero grows past one screen to make room. Change SH in the
   script and this ratio must change with it or the letterbox comes back. */
.stage {
  position: relative;
  width: 100%;
  aspect-ratio: 1000 / 516;
  /* ⚠️ NO min-height: 0 here. It was what let the 1fr row shrink the stage to
     the screen's leftovers; the aspect-ratio has to survive as this row's
     automatic minimum or the letterbox — and the small brain — come back. */

  /* ⚠️ AND align-self: end, which is the whole join. A grid item stretches to
     its row only while its height is auto — aspect-ratio makes the height
     DEFINITE, so the stage stops stretching and sits at the TOP of a row that
     `min-height: 100svh` can leave taller than the art. Measured 48px at
     858x904: the stem is drawn to the stage's bottom edge, the column starts
     at the hero's, and every pixel between them is empty. Sending the stage
     to the bottom of its row puts that slack ABOVE the brain, where it is
     just dark space nobody can see. */
  align-self: end;
}

.art, .overlay {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.art { z-index: 0; }
.overlay { z-index: 1; pointer-events: none; }

.box rect {
  fill: rgba(28, 32, 39, .82);
  stroke: var(--border-strong);
  stroke-width: 1;
}

/* ⚠️ THE LABELS SET THEIR OWN SIZE AND THE BRAIN IS SIZED AROUND WHAT IS
   LEFT, not the other way round. These six words are what the hero is for —
   a reader has to understand, before scrolling, that a text message and a PDF
   and a recorded lecture all end up in the same place. A brain nobody can
   read the inputs off is a picture of a brain.
   12 rather than the spec's 13: at 13 a twenty-character label overruns a
   165-unit box, and the copy that fits in eighteen characters is worse than
   the copy that fits in twenty. 12 units is ~18px at a 1512px viewport,
   against the 15px this was. */
.box text {
  fill: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .03em;
}

/* ⚠️ THE LEADERS ARE THE ONE PLACE THE PAGE CAN *SHOW* "IT GOES IN", AND THEY
   USED TO BE INERT. The hero is a brain made of fourteen thousand moving
   particles with eight static dashed rails bolted to it: nothing about a
   stationary `2 5` dash says which end is the source, so "six ways in, two
   ways out" had to be inferred from which side of the picture a box landed
   on. Identical pills either side of a symmetric drawing is not a strong
   hint — and it got weaker, not stronger, when the left stack grew to six
   and the two arrangements stopped even having the same shape.

   Moving the dash along the path is the whole fix and it costs no JS and no
   markup: the dash pattern is already there, so animating `stroke-dashoffset`
   turns the existing rails into travelling marks in the same stipple
   vocabulary as the art beside them. -14 is exactly two periods of `2 5`, so
   the loop is seamless — any other number ticks visibly once per cycle. */
.lead {
  stroke: var(--border-strong);
  stroke-width: 1;
  stroke-dasharray: 2 5;
  fill: none;
}

@keyframes lead-flow { to { stroke-dashoffset: -14; } }

.lead.in { animation: lead-flow 1.6s linear infinite; }
/* `reverse`, not a second keyframe set: one definition, and the direction is
   stated where the direction is decided. */
.lead.out { animation: lead-flow 1.6s linear infinite reverse; }

/* ⚠️ WHAT COMES OUT IS THE ONLY THING HERE THE READER DOES NOT ALREADY OWN,
   and it is the page's one accent doing the distinguishing. Inputs are the
   reader's own material and stay neutral; the two outputs are what Open Brain
   gives back. Direction alone would carry it on a screen you are watching —
   this carries it in a screenshot, on a still frame, and under reduced
   motion, which is where the animation is switched off. */
.lead.out { stroke: var(--accent-dim); }
.box.out rect { stroke: var(--accent-dim); }

.anchor { fill: rgba(232, 234, 237, .55); }
.anchor.out { fill: var(--accent); }

/* ---------- the spinal column ---------------------------------------- */

/* ⚠️ The CANVAS is wider than the grid track it sits in. The track reserves
   the space the text must keep clear; the canvas is the box the column is
   allowed to swing inside. Making them the same number clipped the gel
   against the track edge and drew a visible rectangle down the page. */
.column {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: 0;
  bottom: 3rem;
  width: var(--rail-canvas);
  z-index: 0;
  pointer-events: none;
}

.bone { width: 100%; height: 100%; display: block; }

/* The gel used to be a CSS gradient on this box, which is why it read as a
   bar: a straight vertical band cannot follow a column that bends. It is
   painted along the path on the canvas now, with the bones. */

.spine { position: relative; padding-bottom: 6rem; }

.stop {
  position: relative;
  display: grid;
  grid-template-columns: 1fr var(--rail) 1fr;
  align-items: center;
  /* Taller than the text needs. The extra height is what buys the column
     its swing: bends further apart mean a gentler slope, and a gentler
     slope means the bones still overlap while travelling further sideways. */
  padding: clamp(6rem, 19vh, 13rem) 1.5rem;
}

.card {
  max-width: var(--wrap);
  display: grid;
  gap: .65rem;
  opacity: .4;
  transition: opacity .5s ease;
}

.stop.on .card { opacity: 1; }

/* ⚠️ THE CARD ALTERNATES SIDES; THE TEXT INSIDE IT DOES NOT. The left-hand
   card used to be `text-align: right`, which was fine while a section was one
   short paragraph and is not fine now that these carry three paragraphs and
   bulleted lists — a ragged LEFT edge costs the reader the one fixed point
   the eye returns to on every line, and a right-aligned bullet list is barely
   readable at all. `justify-self` still pushes the card against the column,
   so the alternation the spine bends around is unchanged; only the setting
   inside the box is. */
.stop:nth-child(odd) .card { grid-column: 1; justify-self: end; }
.stop:nth-child(even) .card { grid-column: 3; justify-self: start; }

/* ⚠️ The section blooms are gone — THOSE were the ovals. Recolouring them
   from cyan to white kept the shape, and an ellipse of light floating beside
   a column of bone reads as an artifact whatever colour it is. The active
   section is already marked by its card coming up to full opacity, so they
   were saying the same thing twice anyway. */

.card h2 {
  margin: 0;
  font-size: clamp(1.3rem, 2.4vw, 1.75rem);
  letter-spacing: -.02em;
  font-weight: 650;
  text-wrap: balance;
}

.card p { margin: 0; color: var(--ink-muted); }

/* The provenance line under the term section. Smaller and dimmer because it
   is a footnote about the picture rather than part of the argument — but it
   is the page's most load-bearing sentence, so it is on the page and not in
   a caption nobody reads: every course and deadline in that screenshot was
   written by the shipped code from six real syllabus files. */
.card .fineprint {
  font-size: .84rem;
  line-height: 1.55;
  color: var(--ink-faint);
  border-left: 1px solid var(--border-strong);
  padding-left: .85rem;
  margin-top: .25rem;
}
.card strong { color: var(--ink); font-weight: 640; }
.card em { color: var(--ink); font-style: italic; }

/* A step inside a section — "How it works" has three, and they are a real
   sequence, so they carry numbers. Nothing else on the page does. */
.card h3 {
  margin: .75rem 0 -.3rem;
  font-size: 1rem;
  font-weight: 640;
  letter-spacing: -.01em;
  color: var(--ink);
  display: flex;
  align-items: baseline;
  gap: .55rem;
}

.card h3 .n {
  font-family: var(--mono);
  font-size: .62rem;
  font-weight: 400;
  letter-spacing: .13em;
  color: var(--accent);
  flex: none;
}

.card ul {
  margin: .15rem 0 0;
  padding-left: 1.05rem;
  display: grid;
  gap: .5rem;
  color: var(--ink-muted);
}

.card li::marker { color: var(--ink-faint); }

/* `.figures` lived here — the 0/6 / 4/6 / 6/6 numerals set small INSIDE the
   recall card. They are `.bars` now, facing it, because they are the proof of
   that section rather than a statistic mentioned in passing. Measured, the
   numerals covered 11% of the facing half and the chart covers 47%. */

/* ---------- what faces the card ----------------------------------------
   ⚠️ EVERY SECTION IS A FACING PAIR NOW, and the gallery that used to hold
   all four screenshots is gone. It was one 2,328px block titled "What it
   looks like" — a heading that admits it is an aside — sitting two thirds
   of the way down while every section above it argued without showing.
   Each screenshot now stands next to the claim it proves, and the page
   lost a section rather than gaining one.

   Measured before the change: the seven one-sided sections were 5,843px,
   59% of the page, at 17% fill. The facing half was 28% of that emptiness
   and the padding above and below was 43%.

   ⚠️ WHAT FACES THE CARD IS SIZED TO FILL THE TRACK, WHICH IS THE WHOLE
   RULE. Measured: the same 0/6 → 6/6 result set as three numerals covered
   11% of the facing half; redrawn as a chart that uses the space, 47%. A
   screenshot manages 53% and the dissolve 61%. An artifact chosen before
   that constraint is applied will usually not meet it. */

/* ⚠️ `.facing` FILLS ITS TRACK AND ALIGNS ITS CONTENTS — it must not
   shrink-wrap. With `justify-self` doing the alignment the box sized to its
   contents, and its contents are `width: 100%` of that box: circular, so it
   resolved to the element's intrinsic default. Every screenshot rendered
   **2px by 2px**, and nothing errored — the layout was valid, the images were
   loaded, they were simply invisible. Found by measuring the rendered fill
   rather than by looking, because at 2px they read as nothing at all rather
   than as something wrong. */
.facing {
  position: relative;
  z-index: 1;
  display: flex;
  width: 100%;
  min-width: 0;
  opacity: .4;
  transition: opacity .5s ease;
}

.facing > * { min-width: 0; }

.stop.on .facing { opacity: 1; }

/* The mirror of the card's alternation. These two rules and the two above
   them must stay opposite: put both on one side and the section has an
   empty half again, with the other half doubled up. */
/* ⚠️ BOTH ITEMS ARE PINNED TO ROW 1, AND WITHOUT IT HALF THE PAGE STACKS.
   Grid auto-flow is sparse — it never steps the cursor backwards. On an even
   section the card takes column 3, so the facing artifact asking for column 1
   is asking for a slot already passed and lands in row 2 instead: text above
   its own picture, in the wrong column, with the section grown by the height
   of both. It fails as a *spacing* bug, which is why it survived the gallery
   and came straight back here. Measured before the fix: `term` stood at
   1137px against `intake`'s 770 for the same content. */
.stop .card,
.stop .facing { grid-row: 1; }

/* Both halves ride the column's swing, so the gap either side of it is the
   same number in every section. `--card-dx` / `--face-dx` are written per section by
   `alignToSpine()`; see the long note there for why the boxes being
   equidistant from the PAGE was not the same thing as looking equidistant.
   A transform rather than a margin: this must not reflow the grid, because
   the tracks are `1fr` and a margin would resize the very boxes whose widths
   the column's lean is computed from. */
.stop .card { transform: translateX(var(--card-dx, 0px)); }
.stop .facing { transform: translateX(var(--face-dx, 0px)); }

.stop:nth-child(odd) .facing { grid-column: 3; justify-content: flex-start; }
.stop:nth-child(even) .facing { grid-column: 1; justify-content: flex-end; }

/* A screenshot has an edge, because it is a captured rectangle of another
   program. Art drawn on the page's own ground does not, and `.art` says so. */
/* ⚠️ `max-width` LIVES ON THE FIGURE NOW, NOT ON THE IMG, and moving it was
   the whole cost of adding captions. The caption has to end where the picture
   ends — a 38rem screenshot over a caption running the full width of the track
   reads as two unrelated things — and a `max-width` on the img alone cannot
   constrain a sibling. Read the `.facing` note above before touching this: the
   figure is `width: 100%` of a flex item that must NOT shrink-wrap, which is
   the arrangement that once rendered every screenshot 2px by 2px with nothing
   erroring. Measured after this change: 597px rendered at 1512, unchanged.
   `.art` is the hero canvas and has no figure — hence the `img` rules below
   still carrying width, so an unwrapped image is never left unsized. */
.facing figure {
  display: block;
  width: 100%;
  max-width: 38rem;
  margin: 0;
}

.facing img {
  display: block;
  width: 100%;
  max-width: 38rem;
  height: auto;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
}

.facing img.art { border: 0; border-radius: 0; background: none; }

/* ⚠️ A CAPTION IS WHAT THE PICTURE IS EVIDENCE OF — NOT WHAT IS IN IT, WHICH
   IS `alt`'s JOB, AND NOT THE ARGUMENT, WHICH IS THE CARD'S. Three texts, three
   readers: `alt` describes the image to somebody who cannot see it, the caption
   tells everybody what it proves, and the card beside it makes the case. They
   must not converge — a caption that restates the card is repetition the reader
   has to skip, and a caption that restates the `alt` makes a screen reader say
   everything twice, which is what the lightbox did until this existed.
   ⚠️ IT IS DELIBERATELY NOT WHERE THE PROVENANCE LINES GO. `.card .fineprint`
   argues, one screen up, that "the term is invented; the reading of it is not"
   belongs on the page and not "in a caption nobody reads". That is still right:
   it is the page's most load-bearing sentence and it is an argument, so it
   stays in the argument column. */
.facing figcaption {
  font-size: .84rem;
  line-height: 1.55;
  color: var(--ink-muted);
  margin-top: .7rem;
  text-align: left;
}

/* ⚠️ THE LIGHTBOX CAPTION IS AN APPEND, NEVER A SECOND STRING. `.lb-only` is
   in the document, inside the one figcaption, and merely hidden on the page —
   so the page caption is a strict PREFIX of the expanded one and the shared
   half exists exactly once. Two independent caption texts per image is the
   drift this repo keeps paying for (`RECORDER_MIMES` vs `AUDIO_FORMATS`; the
   two hero label lists `check-document.mjs` asserts are identical), and it
   fails silently — nothing errors when a caption and its expansion stop
   agreeing. `check-lightbox.mjs` asserts the prefix.
   The expansion earns its place because the screenshots are 2010px captures
   shown at ~597px: "look at the red cap" is only actionable once you are in. */
.facing figcaption .lb-only { display: none; }
.lightbox figcaption .lb-only { display: inline; color: var(--ink-faint); }

/* ---------- a screenshot opens ------------------------------------------
   ⚠️ A BUTTON, NOT A CLICK HANDLER ON THE IMAGE. These are the page's
   evidence, rendered at about 597px from a 2010px capture — a third of the
   detail that was captured, and the fine print inside them (Library's own
   size limits, the Calendar banner) is exactly what the sections beside them
   are claiming. So they open. Making the image itself clickable would put
   the page's only interactive content out of reach of a keyboard and give a
   screen reader no way to know it does anything; a real `button` is focusable,
   Enter/Space work for free, and the `alt` becomes its accessible name.

   It carries none of its own chrome — no padding, no border, no background —
   because the frame belongs to the screenshot, which already has one. What it
   adds is a hover/focus lift, which is the only cue on the page that anything
   here is clickable at all. */
.zoom {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  color: inherit;
  cursor: zoom-in;
  border-radius: 10px;
  transition: transform .25s ease, box-shadow .25s ease;
}

.zoom:hover, .zoom:focus-visible {
  transform: translateY(-2px);
  box-shadow: 0 10px 34px rgba(0, 0, 0, .55);
}

.zoom:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }

/* ⚠️ THE OVERLAY IS `position: fixed` AND SITS ABOVE THE COLUMN. The bone
   canvas is 7,114px of `z-index` company, and the companion of that problem is
   the section `.facing`, which sets `z-index: 1` to sit over it. A backdrop
   without its own stacking order gets vertebrae drawn through it. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  /* ⚠️ `minmax(0, 1fr)`, NOT `1fr`. A grid track's automatic minimum is its
     content, so a 2010x1320 picture whose aspect ratio wants 894px of height
     keeps the row at 894px inside an 868px box and hangs off the bottom of
     the screen — `max-height: 100%` on the image cannot save it, because the
     100% it resolves against is the row the image itself has stretched.
     Measured before the fix: the overlay was 868px tall and the image 894. */
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 1rem;
  /* ⚠️ IT IS A `figure`, AND A FIGURE HAS UA MARGINS — `1em 40px`. Against
     `position: fixed; inset: 0` those are not ignored: they inset the box, so
     the backdrop covered 1432x868 of a 1512x900 window and the strip around
     the edge was still the page. Clicking it did nothing, which is the one
     interaction a backdrop exists to have. */
  margin: 0;
  place-items: center;
  padding: clamp(1rem, 4vh, 3rem);
  /* Near-opaque: with the picture larger than the screen the page behind adds
     nothing, and at .93 its headings ghosted through the margins around a
     panned capture. */
  background: rgba(6, 8, 10, .975);
  opacity: 0;
  transition: opacity .2s ease;
}

.lightbox[hidden] { display: none; }
.lightbox.on { opacity: 1; }

/* The picture's own box, so it can be BIGGER than the screen and panned. */
.lb-stage {
  width: 100%;
  height: 100%;
  min-width: 0;
  min-height: 0;
  overflow: auto;
  display: grid;
  place-items: center;
}

.lightbox img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--surface);
}

/* The alt text, shown. It is written as a description of what the picture
   proves, so it is worth reading next to the picture and not only instead
   of it. */
.lightbox figcaption {
  max-width: 62ch;
  text-align: center;
  color: var(--ink-muted);
  font-size: .82rem;
  line-height: 1.5;
}

/* ⚠️ `lb-close`, BECAUSE `.close` IS ALREADY THE CTA SECTION at the foot of
   the page — `<section class="stop close">`, whose `padding` is
   `clamp(3rem, 8vh, 6rem)`. That rule is declared later in this stylesheet,
   and a shorthand it sets and this block does not is simply inherited by the
   button: measured, a control specified as 2.5rem square rendered 50x119 and
   hung off the top corner. Nothing errored, and the CSS was doing exactly what
   it says. */
.lightbox .lb-close {
  position: absolute;
  top: .75rem;
  right: .75rem;
  width: 2.5rem;
  height: 2.5rem;
  display: grid;
  place-items: center;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink);
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.lightbox .lb-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* ⚠️ ON A PHONE, FITTING THE PICTURE TO THE SCREEN MADE IT SMALLER THAN THE
   THUMBNAIL. Measured at 390x844: the overlay's own padding is larger than the
   section padding behind it, so the opened picture came out **322px wide
   against a 342px thumbnail** — the control whose entire purpose is to show
   the detail was shrinking the image by 6%.

   And fitting cannot be the answer here at any padding. These are 2010px
   captures of a dense screen; at 390px wide their body text lands near 4px,
   so a fit-to-width view is unreadable however neatly it is centred. What a
   reader wants on a phone is the fine print, which means the picture has to be
   larger than the screen and pannable. It opens at 200vw, roughly a third of
   the capture's real scale, which puts Library's own size limits and
   Calendar's banner at a legible size; `.lb-stage` scrolls in both directions
   and the open is centred on it. */
@media (max-width: 700px) {
  .lightbox { padding: .5rem; gap: .5rem; }
  .lightbox img {
    max-width: none;
    max-height: none;
    width: 200vw;
    height: auto;
  }
  /* Two lines is enough to say what the picture is; the picture is the point,
     and on a 844px screen the full description was taking 98px of it. */
  .lightbox figcaption {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
}

@media (prefers-reduced-motion: reduce) {
  .zoom, .lightbox { transition: none; }
  .zoom:hover, .zoom:focus-visible { transform: none; }
}

/* ⚠️ The dissolve is deliberately TRANSPARENT at full width, so it sits on
   the page's own ground rather than in a box. Below 900px the column runs
   down the middle of the layout instead of beside it, and a transparent
   canvas lets a spine draw straight through the middle of the brain — so it
   takes an opaque ground there, exactly like the screenshots already have.
   The rule is in the narrow block, with the cards. */

/* ---------- the dissolve ------------------------------------------------
   ⚠️ NO NEW POINT CLOUD. This is the same 24,000 points the hero draws,
   already decoded, displaced by an amount that grows left to right. The
   expensive part of this page's art was never the drawing — it was
   `image-to-points.mjs` against an 1894 stipple engraving and
   `mesh-to-points.mjs` against a 62 MB CT mesh. This adds neither.

   ⚠️ IT HAS TO STAY RECOGNISABLE AT ONE END or the other end means
   nothing. Three variants were rendered and rejected: cropped to just the
   disintegrating edge reads as static, so does an even scatter, and
   pulling the points into six clumps — the literal reading of "it is in
   six different places" — rendered as six grey smudges. The intact half is
   what makes the scattered half legible as scatter rather than as noise. */
.dissolve {
  display: block;
  width: 100%;
  max-width: 38rem;
  aspect-ratio: 620 / 470;
}

/* ---------- the ingest specimen ----------------------------------------
   `how` is the one section that spans every screen, so no single capture is
   honest about it. What it gets instead is the part no screenshot can show:
   the middle of the pipeline, where a document stops being a file. */
.pipe {
  display: grid;
  gap: 0;
  width: 100%;
  max-width: 32rem;
  font-size: .88rem;
}

.pipe-row {
  display: grid;
  grid-template-columns: 1.15rem 1fr;
  gap: 0 .95rem;
  padding: .62rem 0;
  align-items: start;
}

/* The rail down the left of the specimen is one continuous line drawn by
   the cells rather than an element, so it cannot fall out of step with the
   rows it connects. */
/* ⚠️ `align-self: stretch` IS WHAT MAKES IT A LINE. The row is
   `align-items: start` so the text hangs from the top, and a cell that does
   not opt out of that is only as tall as its own content — which is nothing.
   `height: 100%` cannot save it: 100% of an auto-height row is auto. Without
   this the rail renders as five disconnected ticks and reads as a bulleted
   list with a rendering fault rather than as a sequence. */
.pipe-row .dot {
  position: relative;
  align-self: stretch;
  min-height: 1.5rem;
}

.pipe-row .dot::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--border-strong);
  transform: translateX(-.5px);
}

.pipe-row:first-child .dot::before { top: .52rem; }
.pipe-row:last-child .dot::before { bottom: calc(100% - .52rem); }

.pipe-row .dot::after {
  content: "";
  position: absolute;
  left: 50%;
  top: .52rem;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  border-radius: 50%;
  background: var(--ink-faint);
}

.pipe-row.hit .dot::after { background: var(--accent); box-shadow: 0 0 0 3px rgba(125, 211, 160, .14); }

.pipe-row b { font-weight: 620; color: var(--ink); }
.pipe-row span { display: block; color: var(--ink-muted); line-height: 1.55; }

.pipe-row .n {
  font-family: var(--mono);
  font-size: .68rem;
  letter-spacing: .1em;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* ---------- the retrieval chart ----------------------------------------
   ⚠️ These three numbers used to sit INSIDE the card as `.figures`, small,
   where they read as a statistic mentioned in passing. They are the whole
   proof of the section, so they are the artifact now.

   ⚠️ A PERCENTAGE HEIGHT AGAINST AN AUTO GRID ROW RESOLVES TO NOTHING, and
   the first build of this drew all three bars as hairlines with nothing
   erroring. The row has to be definite — `1fr` inside a grid with a set
   height is. */
.bars {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  align-items: end;
  gap: 1.5rem;
  width: 100%;
  max-width: 28rem;
  height: 15rem;
}

.bar { display: grid; grid-template-rows: auto 1fr auto; gap: .55rem; height: 100%; }

.bar b {
  font-size: 1.6rem;
  line-height: 1;
  letter-spacing: -.03em;
  font-weight: 650;
  font-variant-numeric: tabular-nums;
}

.bar u {
  display: block;
  align-self: end;
  width: 100%;
  border-radius: 3px 3px 0 0;
  background: var(--accent-dim);
  min-height: 2px;
}

.bar span {
  font-family: var(--mono);
  font-size: .6rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-faint);
  border-top: 1px solid var(--border-strong);
  padding-top: .5rem;
}

/* ⚠️ THE HEIGHTS ARE DATA ATTRIBUTES, NOT INLINE STYLES, and that is the CSP
   again rather than a preference. `style="height:62%"` is an inline style and
   `style-src 'self'` refuses it — silently, leaving three bars of nothing.
   The build script refuses to write a `style=` for exactly this reason, which
   is how this got caught. The score belongs in the markup either way: it is a
   measurement, not a presentation choice. */
.bar[data-score="0"] u { height: 0; }
.bar[data-score="4"] u { height: 62%; }
.bar[data-score="6"] u { height: 93%; }

.bar.hit b { color: var(--accent); }
.bar.hit u { background: var(--accent); }

/* ---------- the connect specimen ---------------------------------------
   MCP's surface is somebody else's app, so there is nothing of ours to
   photograph. The command is the artifact. */
.term-block {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 10px;
  padding: 1.15rem 1.3rem;
  width: 100%;
  max-width: 32rem;
  font-family: var(--mono);
  font-size: .76rem;
  line-height: 1.85;
  color: var(--ink-muted);
  overflow-x: auto;
}

.term-block .p { color: var(--ink-faint); user-select: none; }
.term-block .a { color: var(--accent); }
.term-block .c { color: var(--ink-faint); }
/* ⚠️ IT WRAPS, AND UNTIL IT DID THE URL WAS CUT OFF ON EVERY PHONE. A `pre`
   holds its line, and this one is a 60-character URL inside a 298px box:
   measured at 390px the content ran 456px wide against a 298px box with
   `overflow-x: visible`, so the address ended mid-word at the border — no
   scrollbar, no ellipsis, nothing to say the rest existed. The document did
   not scroll sideways either, which is why it never showed up as an overflow
   bug; it simply printed a shorter, wrong URL.

   Wrapping rather than scrolling, because a horizontally scrollable box on a
   phone has no affordance saying so, and this is a thing a reader may want to
   COPY. `anywhere` rather than `break-word`: the string has no spaces to break
   at, which is exactly the case `break-word` declines to handle. */
.term-block pre {
  margin: 0;
  font: inherit;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.term-block .tools {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem .45rem;
  margin-top: .95rem;
  padding-top: .95rem;
  border-top: 1px solid var(--border);
}

.term-block .tools i {
  font-style: normal;
  font-size: .68rem;
  letter-spacing: .02em;
  color: var(--ink-muted);
  border: 1px solid var(--border-strong);
  border-radius: 4px;
  padding: .08rem .38rem;
  white-space: nowrap;
}

.term-block .tools em {
  font-style: normal;
  font-size: .68rem;
  color: var(--ink-faint);
  padding: .08rem .1rem;
}

/* ---------- the close ------------------------------------------------- */

.close {
  padding: clamp(3rem, 8vh, 6rem) 1.5rem clamp(2rem, 5vh, 3.5rem);
  display: grid;
  justify-items: center;
}

.close-inner {
  max-width: 40rem;
  text-align: center;
  display: grid;
  gap: .9rem;
  justify-items: center;
  border-top: 1px solid var(--border);
  padding-top: clamp(2.5rem, 6vh, 4rem);
}

.close h2 {
  margin: 0;
  font-size: clamp(1.4rem, 2.6vw, 1.9rem);
  letter-spacing: -.02em;
  font-weight: 650;
  text-wrap: balance;
}

.close p { margin: 0; color: var(--ink-muted); }

.close .fine { font-size: .84rem; color: var(--ink-faint); max-width: 34rem; }

.btn {
  display: inline-block;
  margin-top: .3rem;
  padding: .7rem 1.5rem;
  border-radius: 8px;
  background: var(--accent);
  color: #0b0d10;
  font-weight: 640;
  font-size: .95rem;
  text-decoration: none;
  border: 1px solid var(--accent);
}

.btn:hover { background: #94ddb1; border-color: #94ddb1; }

.note {
  max-width: 46rem;
  margin: 0 auto;
  padding: 0 1.5rem 3rem;
  text-align: center;
  color: var(--ink-faint);
  font-size: .82rem;
}

.note a { color: var(--ink-muted); }

.tuner {
  position: fixed;
  right: 1rem;
  bottom: 1rem;
  z-index: 5;
  width: 15rem;
  background: rgba(20, 23, 28, .93);
  border: 1px solid var(--border);
  border-radius: 10px;
  backdrop-filter: blur(8px);
  font-family: var(--mono);
  font-size: .68rem;
}

.tuner summary {
  cursor: pointer;
  padding: .55rem .75rem;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--ink-muted);
  list-style: none;
}

.tuner summary::-webkit-details-marker { display: none; }
.tuner summary::after { content: " ▾"; color: var(--ink-faint); }
.tuner[open] summary::after { content: " ▴"; }

.tuner-body {
  padding: .2rem .75rem .8rem;
  display: grid;
  gap: .5rem;
  border-top: 1px solid var(--border);
}

.row { display: grid; gap: .2rem; }

.row label {
  display: flex;
  justify-content: space-between;
  color: var(--ink-faint);
  letter-spacing: .1em;
  text-transform: uppercase;
}

.row output { color: var(--ink); }

input[type="range"] { width: 100%; accent-color: var(--accent); cursor: pointer; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 4px; }

.rm-note { display: none; color: var(--ink-faint); padding-top: .3rem; line-height: 1.5; }

@media (prefers-reduced-motion: reduce) {
  .card { transition: none; }
  .card { opacity: 1; }
  /* The colour and the arrowless geometry still say in-versus-out; only the
     travel stops. */
  .lead.in, .lead.out { animation: none; }
}

/* The narrow arrangement of the hero's six labels — see the markup note.
   Hidden by default so the wide path is untouched; the media query below
   turns it on. */
.io { display: none; }

.io-head {
  font-family: var(--mono);
  font-size: .62rem;
  letter-spacing: .19em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin: 0 0 .5rem;
}
.io ul { list-style: none; margin: 0; padding: 0; display: grid; gap: .38rem; }
.io li {
  font-size: .95rem;
  color: var(--ink);
  padding-left: .85rem;
  border-left: 2px solid var(--border-strong);
  line-height: 1.45;
}
/* Same accent that marks the two output leaders and their boxes at full
   width, so the distinction survives the change of arrangement. */
.io-group.out .io-head { color: var(--accent); }
.io-group.out li { border-left-color: var(--accent-dim); }

@media (max-width: 900px) {
  :root { --rail: 74px; }
  .overlay { display: none; }

  /* ⚠️ `min-height: 100svh` HAS TO GO HERE, and the gap it left was ~310px.
     The hero is `grid-template-rows: auto 1fr` with the stage carrying a
     definite height from its `aspect-ratio` and `align-self: end`. On a
     390x844 phone the copy takes ~350px and the stage resolves to ~201px, so
     the 1fr row is ~490px tall holding a 201px item pinned to its bottom —
     most of a screen of black between the lede and the brain. That slack is
     invisible on a desktop, where the art is tall enough to fill the row,
     which is why the rule was right when it was written and wrong here. */
  .hero { min-height: auto; padding-bottom: 2.5rem; }

  .io {
    display: grid;
    gap: 1.6rem;
    padding: 1.6rem 1.5rem 0;
    max-width: 34rem;
    margin: 0 auto;
    width: 100%;
  }

  .stop { grid-template-columns: 1fr; }

  /* ⚠️ THE PAIR HAS TO BE UNWOUND EXPLICITLY. `.stop` goes to one column
     above, but the card and its facing artifact both name a column of their
     own — and `grid-column: 3` against a single-column template resolves into
     an implicit third track off the side of the page. Facing pairs are a
     wide-screen idea; at this width the artifact goes back under its text. */
  .stop:nth-child(odd) .card,
  .stop:nth-child(even) .card,
  .stop:nth-child(odd) .facing,
  .stop:nth-child(even) .facing {
    grid-column: 1;
    grid-row: auto;
    justify-self: center;
    max-width: 34rem;
    width: 100%;
    /* ⚠️ AND THE SPINE OFFSET HAS TO BE UNWOUND WITH IT. Following the column
       is a facing-pair idea: it holds a gap on each SIDE of a column that
       runs between two halves. Here the column runs down the middle of a
       single stack with the content over it, so the same translation would
       shove the whole page left and right down its length for no reason —
       and `alignToSpine()` keeps writing the variable, because the column is
       still drawn and still swings. */
    transform: none;
  }

  /* ⚠️ THE OPAQUE BACKGROUND IS NOT COSMETIC. The rail narrows to 74px here
     but the column is still drawn, so anything without a ground of its own
     gets vertebrae rendered straight through it. Screenshots hide their own
     patch; text and the built artifacts do not. */
  .stop:nth-child(odd) .card,
  .stop:nth-child(even) .card,
  .pipe, .bars, .term-block, .dissolve {
    background: var(--bg);
  }

  .stop:nth-child(odd) .card,
  .stop:nth-child(even) .card { padding: 1rem 0; }

  /* ⚠️ The card is NOT centred any more. It used to be, because a lone
     paragraph in a narrow column reads as a poster; now every card is
     followed by its own artifact and a centred paragraph above a
     left-aligned specimen reads as two unrelated blocks. Ragged-left also
     costs the reader the one fixed point the eye returns to, which is the
     same argument that stopped the left card being right-aligned. */

  /* The order matters: the card first, then what it is about. */
  .stop .card { order: 1; }
  .stop .facing { order: 2; margin-top: 1.4rem; }

  .bars { height: 12rem; }
  .tuner { width: calc(100vw - 2rem); left: 1rem; right: 1rem; }

  /* ⚠️ THE 19vh IS A SWING BUDGET, AND ON A PHONE IT BUYS NOTHING. `.stop`
     pads `clamp(6rem, 19vh, 13rem)` so the column has room to travel sideways
     between bends — bends further apart mean a gentler slope, and a gentler
     slope keeps the bones overlapping while the path moves further across.
     That argument is about the WIDE arrangement, where the rail is 270px and
     the swing IS the composition. Here the rail is 74px and the card sits
     over the column rather than beside it, so the same budget spends 160px
     above and below every section and buys no swing at all.

     Measured at 390x844 before this: 2,900px of an 11,811px page was
     inter-section padding, and the gap between the dissolve and the next
     heading ran 370px — most of a screen showing one thin dotted line. 10vh
     keeps a visible run of spine between sections without paying a quarter of
     the page for it. The clamp's ceiling is untouched, so this can only ever
     bite where vh is small, which is the case it is for. */
  .stop { padding: clamp(3.5rem, 10vh, 13rem) 1.5rem; }

  /* ⚠️ THE OPAQUE GROUND ABOVE IS PAID FOR IN `opacity`, AND AT .4 IT IS NOT
     A GROUND. `.card` dims to .4 until its section is scrolled into view, and
     opacity multiplies the box's own BACKGROUND down with its text — so the
     column shows straight through the patch that exists to hide it, and every
     unread section reads as vertebrae struck through the words. It comes
     right the moment `.stop.on` lands, which is why it never showed up while
     reading the page from the section you are on: the damage is always one
     section ahead of you.

     Moving the dim onto the card's CHILDREN leaves the box itself opaque and
     the reveal unchanged. The children are grid items and do not overlap, so
     nothing double-dims at an edge. */
  .stop .card { opacity: 1; }
  .stop .card > * { opacity: .4; transition: opacity .5s ease; }
  .stop.on .card > * { opacity: 1; }

  /* ⚠️ AND THE SAME THING ONE ELEMENT OVER, WHICH IS WHERE THE LIST OF
     GROUNDS ABOVE GOES WRONG. `.pipe, .bars, .term-block, .dissolve` are given
     `background: var(--bg)` a few rules up, and the comment there reasons that
     the screenshots need no entry because they "hide their own patch". They do
     — at full opacity. `.facing` also dims to .4, so a screenshot's own pixels
     are multiplied down exactly like the card's background was, and the column
     draws straight through the middle of the picture. The four grounds in that
     list were dimmed too, which is why adding a fifth entry for the image would
     not have fixed any of them.

     So the ground moves onto `.facing`, which is the box that now stays opaque,
     and the dim moves to its single child. `--bg` is the page's own colour, so
     the band this paints is invisible except where it hides the spine, which is
     the whole job. */
  .facing { opacity: 1; background: var(--bg); }
  .facing > * { opacity: .4; transition: opacity .5s ease; }
  .stop.on .facing > * { opacity: 1; }

  /* ⚠️ THE LABELS ARE THE TYPE THAT DOES NOT SURVIVE THE TRIP TO A PHONE.
     Letterspaced mono caps read a size smaller than body text set at the same
     px, which is a fair trade on a desktop at arm's length and not one here.
     Measured at 390px before this: `.io-head` 9.9px, the bar captions 9.6px,
     the passage counts and tool chips 10.9px — against a 15.2px body. Nothing
     below changes what this type IS; it stays mono, uppercase and faint, and
     stops being 9px. The bar caption moves furthest because it is the one
     carrying the result the chart exists to state. */
  .eyebrow { font-size: .78rem; }
  .io-head { font-size: .72rem; }
  .bar span { font-size: .72rem; letter-spacing: .08em; }
  .card h3 .n { font-size: .72rem; }
  .pipe-row .n { font-size: .74rem; }
  .term-block .tools i,
  .term-block .tools em { font-size: .74rem; }

  /* ⚠️ THE WRAP IS DELIBERATE — see `.term-block pre` above, which chose
     `overflow-wrap: anywhere` over a scrollbar because the URL has no spaces
     to break at and a reader may want to COPY it. That call stands. What it
     did not settle is how the wrapped half READS: a 60-character address
     breaking flush-left underneath a command that also wrapped gives four
     lines all starting at the same margin, which is one command printed as
     though it were three.

     A hanging indent says continuation. The narrower gutter buys back ~16px
     of line, which is one break fewer. Both are narrow-only: at full width
     the command fits on its line and neither is doing anything. */
  .term-block { padding: 1.15rem .9rem; }
  .term-block pre { padding-left: 1.15rem; text-indent: -1.15rem; }
}

/* Between the phone and the overlay's cut-in there is room for the two groups
   side by side, which is the wide arrangement's left/right split surviving at
   a size where the SVG cannot. */
@media (min-width: 560px) and (max-width: 900px) {
  .io { grid-template-columns: 1fr 1fr; gap: 2rem; max-width: 40rem; }
}
