/* ghostprint — site.css
   v3, the type room. three values, one accent held back for state.
   organised: fonts / tokens / reset / ground / chrome / index / grid /
   type / catalog / plates / print / hub / 404 / responsive.
   no preprocessor, no inline styles anywhere: the csp in _headers is the law. */

/* ── fonts ─────────────────────────────────────────────────────────────────
   self-hosted. jetbrains mono is the official release build, not the google
   subset: it carries the whole box-drawing block (U+2500-259F, verified 160
   of 160) and the ground engine draws its grid out of those glyphs. */

@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo-400.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo-600.woff2") format("woff2");
  font-weight: 600;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo-800.woff2") format("woff2");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Archivo";
  src: url("/fonts/archivo-900.woff2") format("woff2");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-300-latin.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2190-21FF, U+2200-22FF, U+2500-25FF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-300-ext.woff2") format("woff2");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-0130, U+0132-0151, U+0154-02C5, U+02C7-02D9, U+02DB, U+02DD-1FFF, U+2070-2073, U+2075-20AB, U+20AD-2121, U+2123-218F, U+2300-24FF, U+2600-FFFF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-400-latin.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2190-21FF, U+2200-22FF, U+2500-25FF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-400-ext.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-0130, U+0132-0151, U+0154-02C5, U+02C7-02D9, U+02DB, U+02DD-1FFF, U+2070-2073, U+2075-20AB, U+20AD-2121, U+2123-218F, U+2300-24FF, U+2600-FFFF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-500-latin.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2190-21FF, U+2200-22FF, U+2500-25FF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-500-ext.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-0130, U+0132-0151, U+0154-02C5, U+02C7-02D9, U+02DB, U+02DD-1FFF, U+2070-2073, U+2075-20AB, U+20AD-2121, U+2123-218F, U+2300-24FF, U+2600-FFFF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-700-latin.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2190-21FF, U+2200-22FF, U+2500-25FF;
}
@font-face {
  font-family: "JetBrains Mono";
  src: url("/fonts/jetbrains-mono-700-ext.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-0130, U+0132-0151, U+0154-02C5, U+02C7-02D9, U+02DB, U+02DD-1FFF, U+2070-2073, U+2075-20AB, U+20AD-2121, U+2123-218F, U+2300-24FF, U+2600-FFFF;
}

@font-face {
  font-family: "Daemon Display";
  src: url("/fonts/daemon-display-900.woff2") format("woff2");
  font-weight: 900;
  font-style: normal;
  font-display: swap;
}

/* ── tokens ────────────────────────────────────────────────────────────────
   three values, exactly. everything else is one of them at an opacity.
   [data-pos] is the only attribute that flips the ground; the choice
   behind it is the reader's system on a first visit, then whatever they
   clicked, and a click always wins from then on. */

:root {
  --paper: #ffffff;
  --ink: #000000;
  --acc: #ff4400;

  --bg: #000000;
  --fg: #ffffff;
  /* the ground's ink. it was .30 / .34, judged in a headless shot, and on a
     real panel at her brightness everything past the hero went to nothing.
     type never sits on it (plates paint --bg over it), so it can afford to
     be seen. */
  --grain: rgba(255, 255, 255, 0.46);
  --rule: rgba(255, 255, 255, 0.20);
  --dim: rgba(255, 255, 255, 0.62);

  /* the positive print's values, named once so the [data-pos] rule and the
     js-off @media fallback below can both point at them instead of each
     keeping their own copy of the same five colors. */
  --bg-light: #ffffff;
  --fg-light: #000000;
  --grain-light: rgba(0, 0, 0, 0.50);
  --rule-light: rgba(0, 0, 0, 0.18);
  --dim-light: rgba(0, 0, 0, 0.62);

  /* aliases, no second values: the ground engine and the plate components
     each learned their own name for a token that already exists here. */
  --accent: var(--acc);
  --faint: var(--dim);
  --disp: "Daemon Display", var(--grotesk);

  --grotesk: "Archivo", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  --cell: 11px;
  /* the rail's lane. it is the site's only chrome now, so it is wide enough
     to hold a tree(1) listing at its natural width instead of ellipsing slugs
     mid-word. every page's left padding is measured off it, and rooms.js
     reads it live so the hover plates clear it. */
  --rail: 250px;
  /* the lane's own inset, and the ground left between one segment and the
     next. the rail is compartments now, not a slab, and this is the width of
     the ground that shows around them. */
  --rail-gap: 8px;
  /* inside a compartment. it buys its hairline and its breathing room out of
     the lane, so it stays tight: --rail never moves, because every page's
     left padding is measured off it. */
  --seg-pad: .7em;
  /* the mobile strip's inset. on desktop the segments carry their own. */
  --rail-pad: 1.15em;
  /* the top lane. zero on desktop, where nothing runs along the top any more;
     the mobile strip's height below 980px. pages pad for it either way, and
     rooms.js reads it the same way it reads --rail. */
  --chrome: 0px;
  --measure: 65ch;
}

/* the positive print. pure black ground, pure white ink, by default: no
   warm paper, no graphite, one held-back accent. [data-pos] is the other
   pair, pure white ground and pure black ink: stamped by theme.js from the
   stored choice, or from the reader's system when nothing is stored yet, and
   pinned the moment they click.

   --paper and --ink are fixed at #ffffff / #000000 and do not swap with the
   print: they are the references room.js reads, and they name the values the
   site always keeps in hand, not the state it is being viewed in. everything
   that swaps reads --bg/--fg, which is also what plates.js and rooms.js
   sample at runtime. */
:root[data-pos] {
  --bg: var(--bg-light);
  --fg: var(--fg-light);
  --grain: var(--grain-light);
  --rule: var(--rule-light);
  --dim: var(--dim-light);
}

/* js-off fallback: with no script running, nothing ever stamps [data-pos],
   so a light system is read straight off the media query instead. [data-dark]
   is the one thing that can override this, and only js ever stamps it: the
   quiet mark for a stored dark choice sitting on a light system, so a system
   change never quietly overwrites a choice js already made. js-off on a
   dark system needs nothing: the bare :root above is already dark. */
@media (prefers-color-scheme: light) {
  :root:not([data-dark]) {
    --bg: var(--bg-light);
    --fg: var(--fg-light);
    --grain: var(--grain-light);
    --rule: var(--rule-light);
    --dim: var(--dim-light);
  }
}

/* ── reset ─────────────────────────────────────────────────────────────── */

* { box-sizing: border-box; }

html {
  background: var(--bg);
  color-scheme: dark;
  -webkit-text-size-adjust: 100%;
}
:root[data-pos] { color-scheme: light; }
@media (prefers-color-scheme: light) {
  :root:not([data-dark]) { color-scheme: light; }
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--grotesk);
  font-size: 16px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a { color: inherit; }
img, svg, canvas { max-width: 100%; }

/* the selection's text takes the ground it is printed on, so it swaps with
   the state instead of dragging the paper's warm white into the negative. */
::selection { background: var(--acc); color: var(--bg); }

:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

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

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 20;
  font-family: var(--mono);
  font-size: 12px;
  background: var(--bg);
  color: var(--acc);
  padding: .6em 1em;
}
.skip:focus { left: 0; }

/* ── the ground ────────────────────────────────────────────────────────────
   a fixed <pre> of real, selectable glyphs. /js/room.js writes into it and
   reads these vars back out. .held is the selection freeze. */

#room {
  position: fixed;
  inset: 0;
  margin: 0;
  z-index: 0;
  font-family: var(--mono);
  font-size: var(--cell);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: 0;
  color: var(--grain);
  white-space: pre;
  overflow: hidden;
  user-select: text;
  -webkit-user-select: text;
  pointer-events: auto;
  text-rendering: optimizeSpeed;
  contain: content;
}
#room.held { color: var(--acc); }

/* the ground's two other depths. real text like the ground, but never
   under the pointer and never in a selection: dragging across the ground
   copies the ground. room.js sizes the face and slides them; the far one
   sits behind the ground, dim, the near one in front of it, brighter,
   and both stay under the page. */
.plane {
  position: fixed;
  left: 0;
  top: 0;
  margin: 0;
  z-index: 0;
  font-family: var(--mono);
  font-weight: 400;
  line-height: 1.02;
  letter-spacing: 0;
  white-space: pre;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
  text-rendering: optimizeSpeed;
  contain: layout style;
  will-change: transform;
}
#room-far { color: var(--grain); opacity: .42; }
#room-near { color: var(--fg); opacity: .40; }

/* the page floats over the ground and lets pointer events fall through to it,
   except where something is actually operable or readable. */
main {
  position: relative;
  z-index: 1;
  pointer-events: none;
}
main a,
main button,
main details,
main summary,
main .row,
main .catalog,
main .plate,
main .cards,
main .figure,
main .body { pointer-events: auto; }

/* a text island: the engine caps the ground under it, and the paper is
   painted behind it with a half-em bleed so type never sits in grain. */
.plate {
  background: var(--bg);
  box-shadow: 0 0 0 .5em var(--bg);
}
/* a figure window is a plate too — the engine caps the ground under it — but
   it paints its own frame and must not be filled with paper. */
figure.plate {
  background: transparent;
  box-shadow: none;
}
/* the window stays open, but its head and its caption are type: they take
   the paper back, so a caption never sets in grain once the ground is loud
   enough to be seen. */
figure.plate > .plate-head,
figure.plate > .plate-foot { background: var(--bg); }
/* a cutout that shrink-wraps its line. a label or a slug line is one short
   run inside a full-width block: plated as a block it paints a paper band the
   width of the page, which reads as a rule nobody drew.

   fit-content, not max-content: a grid item's track is sized from the item's
   min-content contribution, and `width: max-content` makes that contribution
   the max-content width — one long slug line then pushes a 1fr track to 1750px
   inside a 390px phone, and the whole document goes with it. fit-content
   shrink-wraps the same way and contributes min-content, which is a word. */
.plate.cut {
  width: fit-content;
  max-width: 100%;
}

/* ── the rail ──────────────────────────────────────────────────────────────
   the site's one chrome, down the left of every page: the home mark, the front
   page's beats, the whole site as a tree, and a system block. nothing runs
   along the top any more.

   it is segmented, not one slab. the old index floated over the ground with a
   paper bleed per label, which is why every slug had to ellipse: it was
   sharing a lane with the reading column. the fix was a panel that owns its
   width, and it worked — the tree sets at its natural measure now — but a
   panel is one rectangle sized to the tallest thing it will ever hold, so on
   a post it painted the whole drop from the listing to the clock in blank
   paper. that blank was not a margin. it was the panel admitting it had
   nothing there.

   so each block is a cutout of its own, in the plate grammar the rest of the
   site prints in: the ground colour, square corners, a lowercase label where
   it has one. they size to their contents and stack from the top. the lane
   between and under them is transparent, so what shows there is the real
   ground — grained, moving, and selectable, since the lane passes pointer
   events through and only the segments catch them.

   and no box borders. the compartments were hairlined for a while, which made
   the one piece of chrome on the page four drawn rectangles, and four drawn
   rectangles down the left of a print is a widget. the cutout is the ground
   contrast alone: --bg lying on live grain, the way every plate on the site
   already works.

   the one device kept is horizontal, not a box: a rule under each segment's
   cap, running wall to wall inside the compartment. it is the same rule the
   tree's count and the negative toggle already sit against, so the grammar is
   the site's own — a column in a print is stated by its rules and its heading,
   never by an outline — and it is what holds the compartments apart on paper,
   where the ground under the rail can be nearly blank and a --bg cutout on
   near-blank paper is a cutout of nothing. */

.rail {
  position: fixed;
  z-index: 6;
  top: 0;
  left: 0;
  bottom: 0;
  width: var(--rail);
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  gap: var(--rail-gap);
  padding: var(--rail-gap);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .06em;
  /* the lane is a lane, not a panel: the ground draws through it, and a click
     in the gaps belongs to the ground. the segments take their events back. */
  background: none;
  pointer-events: none;
}

/* one compartment. square, unruled at its edges, filled with the print's own
   ground so the tree never sets in grain. */
.rail-seg {
  pointer-events: auto;
  min-width: 0;
  background: var(--bg);
  padding: var(--seg-pad);
}

.rail-strip {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1em;
  padding: .55em var(--seg-pad) .35em;
}

/* ── the home mark ─────────────────────────────────────────────────────────
   the press's chop. a 29x11 tile of glyphs — the black sun, breathing, with
   KNEECAPS eclipsed inside it — drawn by /js/mark.js into two layers stacked
   on one grid. the sun is ink at every state; the word is the only orange in
   the rail, and only while the link is hovered or focused.

   the two layers have to land cell for cell, so both are <pre> at the same
   size with the same line-height and the second is taken out of flow onto the
   first. everything that could move an advance — tracking, indent, transform —
   is refused on both. */

.homemark {
  display: block;
  flex: 1 1 auto;
  min-width: 0;
  color: var(--fg);
  text-decoration: none;
}

/* the tile is sized off the lane, not off the font. twenty-nine cells of a
   0.6em advance are 17.4ems across, so 100/17.4 cqw is the size at which the
   grid fills the compartment exactly, at any --rail. the line box is 0.97 to
   match the engine, which draws its circle assuming a cell 0.6/0.97 as wide as
   it is tall. change one of the three and the sun goes oval. */
.hm-art {
  container-type: inline-size;
  position: relative;
  display: block;
  width: 100%;
  /* the layers are not hit targets. mark.js rewrites both pre's text ten
     times a second, and webkit and gecko fire a click only if the node under
     the press is still connected at the release, so a click on the glyph art
     that spans one tick is a click that never happens (chrome keys on the
     element and does not care). the anchor takes the hit instead, and it is
     never rewritten. */
  pointer-events: none;
}

.hm-sun,
.hm-word {
  margin: 0;
  font-family: var(--mono);
  font-size: 5.7471cqw;
  line-height: .97;
  /* both layers land cell for cell or the word prints off its aperture, so
     nothing here may touch the advance: no tracking, no indent, no transform */
  letter-spacing: 0;
  white-space: pre;
  /* glyph art, not a code block: never a scrollbar, never a selection dragged
     out of the chrome. */
  overflow: hidden;
  -webkit-user-select: none;
  user-select: none;
  /* the layer's contents are rewritten ten times a second and nothing outside
     it depends on them, so the work is fenced in — the same containment the
     ground engine's <pre> is given, for the same reason. */
  contain: layout paint;
  text-rendering: optimizeSpeed;
}

.hm-sun {
  color: var(--fg);
  opacity: .82;
  transition: opacity .2s ease;
}

/* the word rides on the sun's own grid. a mono bold has the same advance as
   its roman, so the weight costs no alignment.

   the accent goes on the hover, not on the layer. with a script running the
   two read the same — the row is blank unless the link is live — but the html
   ships the eclipse open with the word inside it, so a layer painted orange
   outright is a wordmark in accent at the head of the rail on every page with
   js off, which is neither state nor annotation. it also flashed orange in
   the gap before the deferred script's first frame. */
.hm-word {
  position: absolute;
  inset: 0;
  color: var(--fg);
  font-weight: 700;
}

.homemark:hover .hm-sun,
.homemark:focus-visible .hm-sun { opacity: 1; }
.homemark:hover .hm-word,
.homemark:focus-visible .hm-word { color: var(--acc); }

/* the strip prints the word instead below 980px, where the chrome is a 44px
   bar. it is also what a browser reads out if the tile is ever missing. */
.hm-fallback {
  display: none;
  font-family: var(--disp);
  font-weight: 900;
  font-size: 16px;
  line-height: 1;
  letter-spacing: 0;
}
.homemark:hover .hm-fallback,
.homemark:focus-visible .hm-fallback { color: var(--acc); }

/* the latch is the mobile drawer's, and it is a checkbox rather than a
   <details> on purpose: the drawer is shut on a phone and open on a desktop
   from one piece of markup, and a media query can decide that for a checkbox
   with no script. forcing a shut <details> open from css is not something the
   engines agree on. the folders in the tree are real <details>, where there is
   no such split — shut is shut on every screen. */
.rail-latch { display: none; }
.rail-x { display: none; }

/* the body holds the three lower segments and shrinks with them; it does not
   grow. growing is what made the void: a flex child told to fill the column
   fills it with its own background whether or not it has anything to put
   there. nothing in this rail grows, so the stack ends where its content
   ends and the lane below it is ground. */
.rail-body {
  flex: 0 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: var(--rail-gap);
}

/* the cap and the rule under it are the compartment's whole statement of
   itself. the rule runs wall to wall inside the box — the negative margins
   undo the segment's padding, the padding puts the label back on the measure
   the contents set at — which is the same idiom the tree's count and the
   negative toggle use at the other end of their boxes. */
.rail-cap {
  flex: none;
  margin: 0 calc(var(--seg-pad) * -1) .75em;
  padding: 0 var(--seg-pad) .6em;
  border-bottom: 1px solid var(--rule);
  letter-spacing: .18em;
  color: var(--dim);
  opacity: .75;
}

.rail-beats,
.rail-sys { flex: none; }

/* the tree is the segment asked to give first: a fully expanded listing on a
   short viewport would otherwise push the system block off the bottom of the
   screen. it shrinks instead, and the listing inside it scrolls — so the
   clock and the [a] toggle are always reachable, whatever is open. the shrink
   factor is 20 against the beats' 1, which is how "the tree gives first" is
   said in flexbox: shrink is shared in proportion to factor times base size,
   so a weight that lopsided spends the tree before the beats feel it. */
.rail-tree {
  flex: 0 20 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* a window short enough to squeeze the rail. two rules that would be wrong at
   a normal height and are only ever right here, so they are scoped rather
   than guarded: the tree gets a floor, because a segment shrunk to a label
   and a count with no listing between them is a compartment that has stopped
   being one, and the beats become the second thing that gives once the tree
   is standing on that floor. 700px is above where the front page's stack
   first runs out of column, so nothing outside this block ever shrinks. */
@media (max-height: 700px) {
  /* the cap, the count and the box's own chrome come to about 76px; the rest
     is the listing window, so this floor is a little under three rows. */
  .rail-tree { min-height: 12em; }
  .rail-beats {
    flex: 0 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: none;
  }
  .rail-beats::-webkit-scrollbar { display: none; }
}

/* ── the rail: the beat index (front page only) ────────────────────────── */

.rail-beats a {
  display: flex;
  align-items: center;
  gap: .7em;
  color: var(--fg);
  opacity: .5;
  text-decoration: none;
  padding: .2em 0;
  letter-spacing: .1em;
}
.rail-beats a:hover { opacity: 1; }
.rail-beats a[aria-current] { opacity: 1; color: var(--acc); }
.rail-beats .bar {
  display: inline-block;
  width: 1.6em;
  height: 1px;
  background: currentColor;
  opacity: .45;
  flex: none;
}
.rail-beats a[aria-current] .bar { opacity: 1; }

/* ── the rail: the tree ────────────────────────────────────────────────────
   tree(1) grammar. folders carry a trailing slash and a - / + latch, files
   are dimmed against them, and every row is a real link. the connectors are
   text, not borders, so they stay in one column whatever is open: a shut
   folder takes its whole subtree with it and nothing left behind moves. */

.dr-scroll {
  /* no tracking inside the listing. a tree is set at the font's own advance —
     .06em over a 32-character row is another three characters of width, and
     width here is the difference between a slug on one line and a slug on two. */
  letter-spacing: 0;
  flex: 1 1 auto;
  min-height: 0;
  overflow: auto;
  overscroll-behavior: contain;
  scrollbar-width: none;
  /* the segment's own padding is the margin here; this is only enough that a
     hovered slug's accent never touches the hairline. every pixel spent on
     the right of this box is a pixel off the measure the tree sets at, and
     the difference between a slug on one line and a slug on two is about
     three characters. */
  padding-right: .15em;
}
.dr-scroll::-webkit-scrollbar { display: none; }

.dr-kids { list-style: none; margin: 0; padding: 0; }
.dr-li { margin: 0; }
.dr-fold { margin: 0; }

.dr-row {
  display: flex;
  align-items: flex-start;
  line-height: 1.85;
  white-space: pre;
}
summary.dr-row {
  cursor: pointer;
  list-style: none;
}
summary.dr-row::-webkit-details-marker { display: none; }
summary.dr-row::marker { content: ""; }

.dr-pre,
.dr-mk {
  flex: none;
  white-space: pre;
  color: var(--fg);
  opacity: .3;
}
.dr-mk i { font-style: normal; }
.dr-fold[open] > summary .mk-shut { display: none; }
.dr-fold:not([open]) > summary .mk-open { display: none; }
summary.dr-row:hover .dr-mk { opacity: .85; color: var(--acc); }

.dr-link {
  color: var(--fg);
  text-decoration: none;
  /* long slugs wrap under themselves rather than ellipse mid-word: the row is
     a flex line, so the second line hangs at the label's own left edge. */
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 0;
}
.is-dir .dr-link { opacity: .9; }
.is-file .dr-link { opacity: .5; }
.is-soon .dr-link { opacity: .38; }
.dr-link:hover { opacity: 1; color: var(--acc); }

/* the path you came down: full ink, and a hairline under it. */
.is-anc .dr-link {
  opacity: 1;
  text-decoration: underline;
  text-underline-offset: .28em;
  text-decoration-thickness: 1px;
}

/* the node you are standing on: inverse-marked. ink ground, paper text — the
   one unambiguous mark in a two-value system, and it prints. */
.is-current .dr-link {
  opacity: 1;
  background: var(--fg);
  color: var(--bg);
  box-shadow: 0 0 0 .18em var(--fg);
}
.is-current .dr-pre,
.is-current .dr-mk { opacity: .6; }

/* the current print's sections, one level under its row. the number sits in
   the marker slot; the section being read goes accent. */
.dr-outline .dr-no { font-variant-numeric: tabular-nums; }
.dr-outline .dr-link { opacity: .7; }
/* sections sit four levels deep, where a wrapped title broke mid-word and
   stacked three lines high. they hold one line and end in an ellipsis; the
   full title is the link's title and the heading itself on the page. */
.dr-outline .dr-link { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.dr-outline .dr-link:hover { opacity: 1; }
.dr-outline a[aria-current="location"] { opacity: 1; color: var(--acc); }
.dr-outline .dr-row:has(a[aria-current="location"]) .dr-no { opacity: 1; color: var(--acc); }

/* the count is the tree segment's foot: a readout at the base of its own
   compartment, ruled off from the listing above it. it used to float in the
   panel a long way under the last row, which is where it read as the bottom
   of nothing. */
.dr-foot {
  flex: none;
  /* the rule runs wall to wall inside the compartment — a divider, not an
     underline — so the negative margins undo the segment's padding and the
     padding puts the text back where the listing above it sets. */
  margin: .85em calc(var(--seg-pad) * -1) 0;
  padding: .8em var(--seg-pad) 0;
  border-top: 1px solid var(--rule);
  font-size: 10px;
  letter-spacing: .12em;
  color: var(--dim);
  opacity: .7;
}

/* ── the rail: the system block ────────────────────────────────────────── */

.sys-row {
  display: flex;
  justify-content: space-between;
  gap: 1em;
  margin: 0 0 .35em;
  opacity: .55;
}
.sys-row b { font-weight: 500; font-variant-numeric: tabular-nums; }

/* the toggle is the compartment's own switch: divided off from the readouts
   above it by a rule that runs the full width of the box, the way the tree's
   count is. */
.sys-neg {
  display: block;
  /* a button shrink-wraps at width:auto whatever its display is, and the rule
     then stops in the middle of the compartment. the width is stated: the box
     plus the padding the negative margins gave back. */
  width: calc(100% + var(--seg-pad) * 2);
  text-align: left;
  font: inherit;
  letter-spacing: inherit;
  color: inherit;
  background: none;
  border: 0;
  border-top: 1px solid var(--rule);
  margin: .9em calc(var(--seg-pad) * -1) 0;
  padding: .8em var(--seg-pad) 0;
  cursor: pointer;
  opacity: .62;
}
.sys-neg:hover { opacity: 1; color: var(--acc); }

/* one rail to a page. the layout mounts it, so a second one anywhere in the
   document is a page that has not been told yet: it stands down rather than
   printing itself over the first. nothing ships one today; the rule is here
   because a duplicate would otherwise be a silent double clock. */
#rail ~ .rail { display: none; }

/* ── section grid ──────────────────────────────────────────────────────────
   swiss, wide, asymmetric. twelve columns; the left inset is the index's
   lane so the rail never sits through the copy. */

section.beat {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
  padding: 14vh 4vw 12vh calc(var(--rail) + 4vw);
  display: grid;
  grid-template-columns: repeat(12, 1fr);
  gap: 0 2vw;
  align-content: start;
}

/* the authored spans, as classes: the csp bans style="" outright. */
.c1-6 { grid-column: 1 / 6; }
.c1-7 { grid-column: 1 / 7; }
.c1-8 { grid-column: 1 / 8; }
.c1-9 { grid-column: 1 / 9; }
.c1-11 { grid-column: 1 / 11; }
.c3-11 { grid-column: 3 / 11; }
.c3-13 { grid-column: 3 / 13; }
.c7-13 { grid-column: 7 / 13; }
.c8-13 { grid-column: 8 / 13; }
.c9-13 { grid-column: 9 / 13; }
.full { grid-column: 1 / -1; }
.drop { margin-top: 3.5vh; }
.drop-far { margin-top: 6vh; }
.drop-deep { margin-top: 8vh; }
.end { align-self: end; }
.start { align-self: start; }

/* ── the hero, composed at the foot ────────────────────────────────────────
   the front page's first screen is a poster, and a poster's title block sits
   low. it used to sit high — label, mark, then two plates, all packed against
   the top padding — which put the mark straight through the middle of the sun
   and left the bottom two thirds of the screen as an unowned gap. read as a
   page that had run out rather than as a composition.

   the fix is one property. the beat is already a full-height grid; telling its
   rows to pack to the end drops the whole block to the foot of the screen and
   the space it was spending above becomes the sky the sun rises into. nothing
   moves in the markup: the order is label, mark, lede, meta, and that is the
   poster's own order read top to bottom — the imprint over the title, the
   credits under it.

   the paddings are the composition's only other statement: a floor deep enough
   that the scroll cue is not sitting on the trim, and a ceiling that is only a
   minimum — the sky is whatever the screen has left over. */
#hero {
  align-content: end;
  padding-top: 12vh;
  padding-bottom: 9vh;
}

/* tight to the mark. the label is the mark's imprint line, not a paragraph
   with a heading after it. */
#hero > .label { margin-bottom: .7em; }

/* the credits row. one step under the mark, both columns starting from the
   same line, and the step is smaller than the generic .drop because the mark's
   own line box already carries a descender's worth of air under it. */
#hero > .drop { margin-top: 2.4vh; }
#hero .lede { margin-bottom: 0; }
#hero .meta p:last-child { margin-bottom: 0; }

/* ── type ──────────────────────────────────────────────────────────────── */

/* the dimmed runs that sit on the ground — labels, meta, the foot, the pass
   readout — are dimmed by COLOUR, never by opacity. opacity fades an element's
   own background with it: a `.plate` at .6 paints its paper at .6 too, and the
   engine's hatching draws straight through the cutout that was supposed to
   stop it. every one of these used to do exactly that. */
.label {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: lowercase;
  color: var(--dim);
  margin: 0 0 1.2em 0;
}
.label b { font-weight: 700; color: var(--acc); }
/* a section beat's label is the door to its hub. the heading under it is the
   beat's hook line, not the section's name, so the label carries the link:
   the `// fiction` slug at the head of the beat goes to the fiction room, the
   same way the rail's tree does. */
.label a { text-decoration: none; }
.label a:hover,
.label a:focus-visible { color: var(--acc); }

/* the display mark. it used to be composed from the page edge and bled left
   under the floating index; the rail owns its lane, so left is a wall. it
   composes inside the reading lane instead, and it is sized off the lane
   rather than off the viewport: `vw` counts the scrollbar, which is how eight
   caps set at 16.5vw ran their last stroke under the scroll gutter at 1440
   and past the trim at 2560.

   the fallback stays a vw clamp for anything without container queries; the
   cqw line wins where they exist and is measured off the beat's content box,
   which is the lane exactly, scrollbar and rail already subtracted. the
   coefficient is measured, not guessed: fontTools against
   daemon-display-900.woff2's own hmtx, the same cell grid public/js/mark3d.js
   and tools/gen-mark-glyphs.mjs read the font through, 16 cells per em. the
   old mark (the numeric handle) set 79 cells, 4.9375em; bless her sets 65, 4.0625em,
   narrower by 79/65. the coefficient came down with it: 15.6 * 79/65 = 18.96
   keeps the exact fraction of the lane the old mark filled. re-measure if the
   mark's text changes: the coefficient belongs to the string, not to the
   font. */
section.beat { container-type: inline-size; }

h1.mark {
  grid-column: 1 / -1;
  font-family: var(--disp);
  font-weight: 900;
  font-size: min(18.96cqw, 304px);
  font-size: round(down, min(18.96cqw, 304px), 16px);
  line-height: .9;
  letter-spacing: 0;
  margin: 0;
}

/* ── the instanced mark ────────────────────────────────────────────────────
   the hero's mark, rebuilt out of the face's own cells by /js/mark3d.js. at
   rest the type is the mark; once the reader scrolls, the cells leave it and
   fall the length of the page, so their stage is the whole viewport, fixed.

   the mount is a direct child of main, not of the hero: a fixed box inside a
   size container is not promised the viewport. z-index -1 puts it in main's
   own stacking context, above the paper and below every word and every
   plate on the page: a shard passes behind copy and never over it, and the
   ground stays a whole layer further down.

   the class comes from the script and only while the mesh is actually
   drawing. it knocks the type out rather than removing it: the h1 keeps its
   accessible name, its place in the heading order, and its selection. with no
   webgl, no three, or reduced motion the class is never added and the mark is
   the type, exactly as it has always been. */
#mark3d {
  position: fixed;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  z-index: -1;
  pointer-events: none;
  contain: strict;
}
#mark3d canvas {
  display: block;
  width: 100%;
  height: 100%;
}
#hero.mark-cells h1.mark { color: transparent; }
#hero.mark-cells h1.mark::selection { color: transparent; }

h2 {
  font-family: var(--disp);
  font-weight: 900;
  font-size: 64px;
  line-height: 1;
  letter-spacing: 0;
  margin: 0 0 .35em 0;
}

h3 {
  font-family: var(--disp);
  font-weight: 900;
  font-size: 32px;
  line-height: 1;
  letter-spacing: 0;
  margin: 0 0 .5em 0;
}

.lede {
  font-size: clamp(16px, 1.5vw, 22px);
  line-height: 1.35;
  max-width: 34ch;
  margin: 0 0 2.4em 0;
}

.meta {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  color: var(--dim);
}
.meta p { margin: 0 0 .8em 0; }
.meta a:hover { color: var(--acc); }

.big {
  font-family: var(--disp);
  font-weight: 900;
  font-size: 128px;
  line-height: .9;
  letter-spacing: 0;
}

hr.rule {
  border: 0;
  border-top: 1px solid var(--rule);
  margin: 0 0 1.6em 0;
  grid-column: 1 / -1;
}

.quiet { opacity: .62; }

.tag {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .08em;
  color: var(--acc);
  white-space: nowrap;
}

/* ── catalog rows ──────────────────────────────────────────────────────── */

.catalog {
  font-family: var(--mono);
  font-size: 13px;
  border-top: 1px solid var(--rule);
}
.row {
  display: grid;
  grid-template-columns: 5.5em 1fr 5em 6.5em;
  gap: 1em;
  padding: .85em 0 .8em;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
  background: var(--bg);
  align-items: baseline;
}
.row .t {
  font-family: var(--disp);
  font-size: 16px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0;
}
.row .t small {
  display: block;
  /* the lore line is prose, not a title: it stays in the grotesk, and the
     pixel face never has to fake a 400 it does not carry */
  font-family: var(--grotesk);
  font-weight: 400;
  font-size: 12.5px;
  opacity: .7;
  letter-spacing: 0;
  max-width: 56ch;
  line-height: 1.3;
}
.row .n, .row .p, .row .d {
  opacity: .62;
  font-size: 11.5px;
  letter-spacing: .06em;
}
.row .p, .row .d { text-align: right; }
.row:hover { color: var(--acc); }
.row:hover .n { opacity: 1; color: var(--acc); }
.row[data-queued] .t { opacity: .5; }

.catalog-more {
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .08em;
  margin: 1.2em 0 0;
  background: var(--bg);
  display: inline-block;
}
.catalog-more a { text-decoration: none; }
.catalog-more a:hover,
.catalog-more a:focus-visible { color: var(--acc); }

/* ── plate windows and plate cards ─────────────────────────────────────────
   the components own their internals. these are the lanes they sit in and a
   shared frame grammar, so nothing looks unset before their styles land. */

.plates-band {
  display: grid;
  /* three windows across on the wide print. they stack at 980, where the beat
     drops to one column anyway: two of three would leave an orphan, and a
     square window at half the band is already wider than it is useful. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.6vw;
  margin-top: 5vh;
}

.cards {
  display: grid;
  /* the min() is load-bearing: a bare minmax(19rem, 1fr) track cannot shrink
     under 19rem, so on a 280-320px phone the card runs past the edge and the
     body's overflow-x: hidden clips its right frame off. */
  grid-template-columns: repeat(auto-fill, minmax(min(19rem, 100%), 1fr));
  gap: 1.4rem;
  margin-top: 4vh;
}

.figure {
  position: relative;
  background: var(--bg);
  border: 1px solid var(--rule);
}
.figure figcaption {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  opacity: .62;
  padding: .7em .9em;
  border-top: 1px solid var(--rule);
}

.plate-cap {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  color: var(--dim);
  margin: .8em 0 0;
  max-width: 42ch;
}

/* ── the print sheet ───────────────────────────────────────────────────── */

/* the sheet's geometry sits on the wrap, in rem, and the unit is load-bearing.
   these were em, and em resolves against whatever consumes it: the press block
   reads at 11px, a margin note at 11.5px, the pull-quote at 14px, the reserved
   lane at 17px. one token, four widths — the three margin items came out 143,
   150 and 182 wide inside a 251px lane, no two of them sharing a left or a
   right edge, and a 44px strip along the trim that nothing could ever reach.
   in rem the lane is one number and every margin item is the same column,
   flush to the same trim. the wrap holds the tokens so the pager can take the
   sheet's width as well, instead of 70ch measured in 11.5px mono. */
.sheet-wrap {
  position: relative;
  --sheet: 55rem;
  --marg: 12.5rem;      /* the note column */
  --marg-gap: 1.75rem;  /* prose to notes */
  --spine: 2.25rem;     /* the outer strip: section marks, then running folio */
  --gutter: 0px;        /* no reserved margin until the rail opens at 1100 */
  padding: calc(var(--chrome) + 8vh) 4vw 14vh calc(var(--rail) + 4vw);
}

.sheet {
  /* the sheet's measure, in rem for the same reason the lane is: `70ch` on
     `.body > *` is 682px of prose, 462px of press block and 642px of
     pull-quote, because each of those reads ch against its own font size.
     42.5rem is the same 680px of prose, and now the mono blocks hold it too. */
  --measure: 42.5rem;
  max-width: var(--sheet);
  background: var(--bg);
  /* the pad, and a hairline trim on the outside of it. in negative the
     sheet's ground and the page's ground are the same value, so without the
     trim the sheet has no edge at all: the living ground outside reads as a
     hole in the page instead of the table the page lies on. */
  box-shadow: 0 0 0 1.6em var(--bg), 0 0 0 calc(1.6em + 1px) var(--rule);
}

.print-head { margin: 0 0 3.2em; }
.print-head .label { margin-bottom: 1.6em; }

.folio {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .1em;
  opacity: .6;
  display: block;
  margin: 0 0 1.2em;
}
.folio a { text-decoration: none; }
.folio a:hover { color: var(--acc); }

.print-title {
  font-family: var(--disp);
  font-weight: 900;
  font-size: 64px;
  line-height: 1;
  letter-spacing: 0;
  margin: 0 0 .5em;
}
.print-sum {
  font-size: 17px;
  line-height: 1.4;
  margin: 0 0 1.6em;
  max-width: 46ch;
}
.print-meta {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  opacity: .62;
  display: flex;
  flex-wrap: wrap;
  gap: 1.4em;
  margin: 0;
  padding-top: .9em;
  border-top: 1px solid var(--rule);
}

.body { font-size: 17px; line-height: 1.62; }
.body > * { max-width: var(--measure); }
.body h2 {
  font-size: 32px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0;
  margin: 2.4em 0 .6em;
  padding-top: .6em;
  border-top: 1px solid var(--rule);
}
.body h3 {
  font-size: 16px;
  font-weight: 900;
  line-height: 1;
  letter-spacing: 0;
  margin: 2em 0 .4em;
}
.body p { margin: 0 0 1.15em; }
.body a { color: var(--acc); text-underline-offset: .18em; }
.body ul, .body ol { margin: 0 0 1.15em; padding-left: 1.3em; }
.body li { margin: 0 0 .35em; }
.body strong { font-weight: 700; }
.body em { font-style: italic; }
/* a quoted run, wrapped at build by src/lib/quotes.mjs. code inside a quote is
   still code and stands upright, and emphasis inside the italic turns roman,
   the way it does in any italic setting. the small trailing pad is the italic
   correction: the slant of the closing mark otherwise leans into the next
   word. */
.q { font-style: italic; padding-right: .06em; }
.q code,
.q em { font-style: normal; }
.body hr { border: 0; border-top: 1px solid var(--rule); margin: 2.4em 0; }

.body blockquote {
  margin: 1.6em 0;
  padding: 0 0 0 1.2em;
  border-left: 2px solid var(--acc);
  font-size: 16px;
  opacity: .85;
}

/* inline code is set, not boxed. a filled band was a grey patch in a sheet
   that prints in exactly two tones, so the word sits in ink between two
   hairline brackets, the way the house holds a state in [brackets].

   each bracket is an inline box around a word joiner, drawn by its own
   border. inline, so a span that wraps opens on one line and closes on the
   next instead of one frame stretched over both; a word joiner, so the line
   can never break between a bracket and the word it holds. jetbrains carries
   a taller x-height than archivo, so the mono comes down a step to sit on the
   prose's line at the same optical size. */
.body code {
  font-family: var(--mono);
  font-size: .84em;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--fg);
  margin: 0 .14em;
}
.body code::before,
.body code::after {
  content: '\2060';
  border: 1px solid var(--dim);
  pointer-events: none;
}
.body code::before { border-right: 0; padding-right: .26em; margin-right: .2em; }
.body code::after { border-left: 0; padding-left: .26em; margin-left: .2em; }
.body pre {
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.5;
  background: var(--bg);
  border: 1px solid var(--rule);
  padding: 1em 1.1em;
  margin: 1.6em 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  max-width: none;
}
.body pre code { background: none; padding: 0; margin: 0; font-size: inherit; font-weight: inherit; }
.body pre code::before,
.body pre code::after { content: none; }

.body table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  margin: 1.6em 0;
}
.body th, .body td {
  text-align: left;
  padding: .5em .6em .5em 0;
  border-bottom: 1px solid var(--rule);
}
.body th {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  opacity: .7;
  font-weight: 500;
}

.table-scroll {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

.print-figure {
  margin: 3.2em 0;
  max-width: none;
}

/* ── the marginalia rail ───────────────────────────────────────────────────
   the sheet's right side. three kinds of thing live in it: the press block
   (.margin-rail, authored in the layout), the pull-quote (the first blockquote
   in the body, marked by src/lib/marginalia.mjs), and margin notes (`> [m] …`
   in the markdown, same file).

   all three are ordinary in-flow children of .body. above 1100px they float
   right into a gutter the sheet reserves, with `clear: right` so they stack
   instead of overlapping; below it they unfloat and read inline, in order.
   there is exactly one copy of every word either way. */

.margin-rail {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.5;
  letter-spacing: .06em;
  border-top: 1px solid var(--rule);
  border-bottom: 1px solid var(--rule);
  padding: .8em 0;
  margin: 0 0 2.6em;
  color: var(--dim);
}
.mr-cap { margin: 0 0 .8em; opacity: .5; letter-spacing: .14em; }
.mr-numeral {
  font-family: var(--grotesk);
  font-weight: 900;
  font-size: 34px;
  line-height: .9;
  letter-spacing: -.03em;
  color: var(--fg);
  margin: 0 0 .2em;
  font-variant-numeric: tabular-nums;
}
.mr-place { margin: 0 0 .9em; }
/* the running folio in the spine. it only exists once the margin opens; with
   the margin folded away it would be a second copy of the folio line. */
.mr-spine { display: none; }
.mr-passes { margin: 0; font-variant-numeric: tabular-nums; }
.mr-figure { margin: 1.2em 0; }

.body .mnote {
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.55;
  letter-spacing: .01em;
  color: var(--dim);
  border-left: 1px solid var(--rule);
  padding: 0 0 0 1em;
  margin: 1.6em 0;
}
.body .mnote p { margin: 0 0 .7em; }
.body .mnote p:last-child { margin-bottom: 0; }
.body .mnote code { font-size: .95em; }

/* the pass bar. content pages only. */
/* the pass bar runs across the reading lane, not under the rail: the rail is
   opaque and a bar drawn beneath it would simply be two thirds of a bar. */
#pass {
  position: fixed;
  z-index: 5;
  left: var(--rail); right: 0;
  top: var(--chrome);
  height: 2px;
  pointer-events: none;
}
#pass i {
  display: block;
  height: 100%;
  width: 0;
  background: var(--acc);
}
#pass-read {
  position: fixed;
  z-index: 4;
  right: 1.4vw;
  bottom: 1.4vh;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .1em;
  color: var(--dim);
  background: var(--bg);
  padding: .2em .4em;
  pointer-events: none;
  font-variant-numeric: tabular-nums;
}

.colophon {
  margin: 5em 0 0;
  padding-top: 1.2em;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .05em;
}
.colophon-cap {
  margin: 0 0 1em;
  letter-spacing: .14em;
  opacity: .5;
}
.colophon dl { margin: 0; }
.colophon dl > div {
  display: grid;
  grid-template-columns: 9em 1fr;
  gap: 1em;
  padding: .3em 0;
}
.colophon dt { opacity: .5; }
.colophon dd { margin: 0; }

.pager {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2vw;
  margin: 4em 0 0;
  /* the sheet's width, not `--measure`: ch resolves against the pager's own
     11.5px mono, which set the run navigation to 449px under an 879px sheet
     and left the whole right half of the foot empty. */
  max-width: var(--sheet);
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: .05em;
  border-top: 1px solid var(--rule);
  padding-top: 1.1em;
  background: var(--bg);
}
.pager a { text-decoration: none; display: block; }
.pager a:hover .pager-t { color: var(--acc); }
.pager-cap { display: block; opacity: .45; letter-spacing: .14em; margin-bottom: .3em; }
.pager-t {
  display: block;
  font-family: var(--grotesk);
  font-size: 15px;
  font-weight: 600;
  letter-spacing: -.01em;
}
.pager-next { text-align: right; }
.pager-none { opacity: .35; }

/* ── hub pages ─────────────────────────────────────────────────────────── */

.hub {
  position: relative;
  padding: calc(var(--chrome) + 10vh) 4vw 14vh calc(var(--rail) + 4vw);
}
.hub-head { max-width: 56ch; }
/* the blog banner is a long line of words: it gets a wider box, blog only. */
.hub-head[data-section="blog"] { max-width: 100%; }
.hub-head h1 {
  font-family: var(--disp);
  font-weight: 900;
  font-size: 96px;
  line-height: .9;
  letter-spacing: 0;
  margin: 0 0 .3em;
}
.hub-head .meta { margin-top: 1.4em; }
.empty { margin-top: 4vh; padding: 1.1em 1.2em; border: 1px solid var(--rule); }

/* the project page's own lede, set from the project file's markdown body */
.project-lede {
  margin-top: 1.6em;
  max-width: 62ch;
  font-size: 15.5px;
}
.project-lede > * { max-width: 62ch; }

/* ── the floor ─────────────────────────────────────────────────────────────
   the plain list of projects that sits under a hub room. it renders on every
   load, before any script, and it is what remains when the room cannot be
   drawn. the room is a costume over these links; these links are the site. */

.floor {
  margin-top: 5vh;
  border-top: 1px solid var(--rule);
  padding-top: 1.2em;
  max-width: 74ch;
}
.floor-cap { margin: 0 0 1.4em; opacity: .5; }
.floor-list { list-style: none; margin: 0; padding: 0; }
.floor-list li { border-bottom: 1px solid var(--rule); }
.floor-list a {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "t n" "lore go";
  gap: .25em 1.4em;
  align-items: baseline;
  padding: 1em 0;
  text-decoration: none;
  color: inherit;
}
.fl-t {
  grid-area: t;
  font-family: var(--disp);
  font-weight: 900;
  font-size: 16px;
  line-height: 1;
  letter-spacing: 0;
}
.fl-lore {
  grid-area: lore;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--dim);
  max-width: 56ch;
}
.fl-n, .fl-go {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .12em;
  color: var(--dim);
  white-space: nowrap;
}
.fl-n { grid-area: n; }
.fl-n b { color: var(--acc); font-weight: 400; }
.fl-go { grid-area: go; text-align: right; opacity: 0; transition: opacity 120ms linear; }
.floor-list a:hover, .floor-list a:focus-visible { outline: none; }
.floor-list a:hover .fl-t,
.floor-list a:focus-visible .fl-t { color: var(--acc); }
.floor-list a:hover .fl-go,
.floor-list a:focus-visible .fl-go { opacity: 1; color: var(--acc); }
/* a ghost row: the same grid, not a link. dim, dashed, stamped, and it
   never answers a pointer. */
.floor-list .fl-row {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "t n" "lore go";
  gap: .25em 1.4em;
  align-items: baseline;
  padding: 1em 0;
  cursor: default;
}
.floor-list li.fl-ghost { border-bottom-style: dashed; }
.fl-ghost .fl-t { color: var(--dim); font-weight: 400; font-family: var(--mono); font-size: 13px; letter-spacing: .12em; }
.fl-ghost .fl-go { opacity: 1; }
/* soon: a real link to a coming soon print, stamped like a ghost row */
.floor-list li.fl-soon { border-bottom-style: dashed; }
.fl-soon .fl-t { color: var(--dim); }
.fl-soon .fl-go { opacity: 1; }
/* a redaction: a solid ink bar. the glyphs are full blocks, so there is no
   hidden text under it to select or read, only the bar and its width. */
.redact {
  font-family: var(--mono);
  letter-spacing: 0;
  line-height: 1;
  color: var(--fg);
  user-select: none;
  white-space: nowrap;
}
/* dev only: the page is public-soon in a build */
.dev-stamp { color: var(--acc); font-weight: 400; letter-spacing: .06em; }
/* the console cards only: a repo link and up to three facts, under the row.
   every other section's project has neither and this block never prints. */
.fl-meta {
  display: flex;
  flex-wrap: wrap;
  gap: .3em 1.2em;
  margin: -0.6em 0 1em;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: .04em;
}
.fl-meta a { color: inherit; }
.fl-meta a:hover, .fl-meta a:focus-visible { color: var(--acc); }
.fl-facts { color: var(--dim); }
/* the cue must be reachable without a pointer: keyboard focus shows it above,
   and a coarse pointer never hides it in the first place. */
@media (hover: none) {
  .fl-go { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .fl-go { transition: none; }
}

/* ── the console ───────────────────────────────────────────────────────────
   the projects hub. the same travel track, the same sticky stage and the same
   camera every room has carried since v3, laid out as three lanes: the index
   on the left, the bay in the middle, the readout on the right. the bay keeps
   its own coordinate space: the canvas, the annotation layer and the plates
   all live inside `.con-room`, so a plate placed at the frame edge is placed
   at the *lane's* edge and can never stray under the index.

   the boot runs once per load and is a state on the room, not a stylesheet of
   its own: `data-boot="run"` while it plays, `data-boot="done"` after, and
   straight to `done` under reduced motion. rooms.js owns the attribute. */

.hub-room[data-kind='console'] {
  --con-list: 13rem;
  --con-read: 19.5rem;
  --con-pad: 1.5rem;
}

/* the console is full bleed. the hub page pads its content past the rail and
   in from the right, and three lanes measured inside that padding leave the
   bay a slot two hundred pixels wide. the room takes the whole viewport back
   and then pays the rail itself, once. */
.hub-room[data-kind='console'] {
  width: 100vw;
  margin-left: calc(var(--con-bleed-l, calc(var(--rail) + 4vw)) * -1);
  margin-right: calc(var(--con-bleed-r, 4vw) * -1);
}
.hub-room[data-kind='console'][data-gl] .con-stage,
.hub-room[data-kind='console'][data-nogl] .con-stage {
  display: grid;
  grid-template-columns: var(--con-list) minmax(0, 1fr) var(--con-read);
  align-items: stretch;
  box-sizing: border-box;
  padding-left: var(--rail);
  /* the scan band and the trace are laid over the whole stage, and with no
     webgl the stage is not sticky and so not positioned: without this they
     would hang off the room instead and the band would show above the lanes
     with the boot long over. */
  position: relative;
}

/* the lanes' rules. a border cannot be drawn in, so each hairline is a real
   element scaled from its top edge, which is also why they are the one thing
   in this block that moves on a transition rather than on the clock. */
.con-list,
.con-read { position: relative; }
.con-list::after,
.con-read::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--rule);
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform 260ms linear;
}
.con-list::after { right: 0; }
.con-read::before { left: 0; }
.hub-room[data-kind='console'][data-boot] .con-list::after,
.hub-room[data-kind='console'][data-boot] .con-read::before { transform: scaleY(1); }

/* where a rail ends it is terminated, not cut: a register tick across its
   foot, the short rule the sheet sets across the spine at every heading,
   turned inward so the two rails close as mirrored corners, the same shape
   as the frame's brackets. nine pixels, the rule's own colour, and it arrives with the hairline's last frame so the line draws and then
   closes. the stacked layout turns the rails sideways and drops the ticks. */
.con-list::before,
.con-read::after {
  content: '';
  position: absolute;
  bottom: 0;
  width: 9px;
  border-top: 1px solid var(--rule);
  opacity: 0;
  transition: opacity 120ms linear 260ms;
}
.con-list::before { right: 0; }
.con-read::after { left: 0; }
.hub-room[data-kind='console'][data-boot] .con-list::before,
.hub-room[data-kind='console'][data-boot] .con-read::after { opacity: 1; }

/* ── lane one: the index ── */

.con-list {
  padding: calc(var(--chrome) + 4.2rem) var(--con-pad) 2rem var(--con-pad);
  font-family: var(--mono);
  overflow: hidden;
}
.cl-rail {
  list-style: none;
  margin: 0;
  padding: 0;
}
.cl-row + .cl-row { margin-top: .18em; }
.cl-link {
  display: grid;
  grid-template-columns: 3.4em 1fr;
  gap: .2em;
  align-items: baseline;
  padding: .3em 0;
  font-size: 12px;
  letter-spacing: .1em;
  text-decoration: none;
  color: var(--dim);
  background: var(--bg);
  box-shadow: 0 0 0 .3em var(--bg);
}
.cl-n { font-variant-numeric: tabular-nums; }
.cl-name { color: var(--fg); }
.cl-link:hover,
.cl-link:focus-visible { color: var(--acc); outline: none; }
.cl-link:hover .cl-name,
.cl-link:focus-visible .cl-name { color: var(--acc); }
.cl-link:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }
/* the row the camera is standing at. rooms.js toggles the class off `data-at`,
   because css cannot match one attribute's value against another's. */
.cl-row.is-at .cl-link,
.cl-row.is-at .cl-name { color: var(--acc); }
/* the block cursor, alight on exactly one row while the index types itself */
.cl-row.is-typing .cl-name::after {
  content: '\2588';
  color: var(--fg);
}

/* ── lane two: the bay ── */

.con-room {
  position: relative;
  overflow: hidden;
}
.hub-room[data-kind='console'] .hub-plate { width: min(17rem, 80%); }

/* the plate's position, and the slide it takes when a body is in the way.
   rooms.js writes --px and --py through the cssom every frame. the plate moves
   on the `translate` property rather than a transform string so the slide has
   a property of its own to ease, and nothing else on the card has to be
   rewritten to animate it.

   the plate is not transitioned while it tracks: a card easing behind a body
   that is turning would read as lag, not as motion. `is-sliding` goes on for
   the length of one slide and comes straight off again, so the ease costs
   exactly the 150 ms it is worth and no frame after that.

   the room is carried in both selectors on purpose: the component's own rules
   are astro-scoped, which buys them an attribute's worth of specificity, and a
   bare `.hub-plate` here would lose the tie to load order. */
.hub-room .hub-plates .hub-plate {
  translate: var(--px, 0px) var(--py, 0px);
}
.hub-room .hub-plates .hub-plate.is-up.is-sliding { transition-duration: 160ms, 150ms; }
.hub-room .hub-plates .hub-plate.is-sliding {
  transition: opacity 90ms linear,
              translate 150ms cubic-bezier(0.25, 0.8, 0.3, 1);
}
/* rooms.js never sets the class under reduced motion. this is the brace to
   that belt: however the class arrives, the slide is a jump. */
@media (prefers-reduced-motion: reduce) {
  .hub-room .hub-plates .hub-plate.is-sliding { transition: none; }
}

/* ── lane three: the readout ── */

.con-read {
  padding: calc(var(--chrome) + 4.2rem) var(--con-pad) 2rem var(--con-pad);
  font-family: var(--mono);
  overflow-y: auto;
  overflow-x: hidden;
}
/* one card is shown; the other four stay printed and out of the tab order,
   which is what keeps the rewrite a retype of words the page already had. */
.cr-card { display: none; }
.cr-card.is-at { display: block; }

.cr-cap {
  margin: 0 0 1.1em;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .14em;
  color: var(--dim);
}
.cr-state {
  font-weight: 700;
  color: var(--acc);
  white-space: nowrap;
}
.cr-lore {
  margin: 0 0 1.2em;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--fg);
}
.cr-facts {
  list-style: none;
  margin: 0 0 1.4em;
  padding: 0;
}
.cr-fact {
  font-size: 11px;
  line-height: 1.7;
  letter-spacing: .08em;
  color: var(--dim);
}

/* the frame. the ticks are the annotation the accent is for: four corners,
   never a rule round the whole box, so an empty frame and a full one read as
   the same instrument with and without a plate in it. */
.cr-frame {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 9;
  max-width: 100%;
  margin: 0 0 1.4em;
  border: 1px solid var(--rule);
}
.cr-tick {
  position: absolute;
  inset: 0;
  pointer-events: none;
}
.cr-frame::before,
.cr-frame::after,
.cr-tick::before,
.cr-tick::after {
  content: '';
  position: absolute;
  width: 8px;
  height: 8px;
  border: 1px solid var(--acc);
}
.cr-frame::before { top: -1px; left: -1px; border-right: 0; border-bottom: 0; }
.cr-frame::after { top: -1px; right: -1px; border-left: 0; border-bottom: 0; }
.cr-tick::before { bottom: -1px; left: -1px; border-right: 0; border-top: 0; }
.cr-tick::after { bottom: -1px; right: -1px; border-left: 0; border-top: 0; }
.cr-img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.cr-none {
  margin: 0;
  font-size: 10.5px;
  letter-spacing: .12em;
  color: var(--dim);
}

.cr-links {
  display: flex;
  gap: 1.6em;
  margin: 0;
  font-size: 11px;
  letter-spacing: .08em;
}
.cr-gh,
.cr-go {
  color: var(--acc);
  text-decoration: none;
}
.cr-gh:hover,
.cr-go:hover { text-decoration: underline; }
.cr-gh:focus-visible,
.cr-go:focus-visible { outline: 2px solid var(--acc); outline-offset: 2px; }

/* ── the boot: the sweep and the trace ── */

.con-scan {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: var(--cell);
  background: var(--rule);
  pointer-events: none;
  transform: translateY(-100%);
  visibility: hidden;
}
.hub-room[data-kind='console'][data-boot='run'] .con-scan {
  visibility: visible;
  animation: con-sweep 700ms linear 120ms 1 both;
}
@keyframes con-sweep {
  from { transform: translateY(-100%); }
  to { transform: translateY(100vh); }
}

.con-trace {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
/* the draw-in is one transition off one class. rooms.js measures the path and
   writes its length into `--ct-len` through the cssom, which is the only way a
   number can reach a stylesheet under `style-src 'self'`. */
.ct-line {
  fill: none;
  stroke: var(--acc);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
  /* the fallback is a length longer than any path three lanes can produce,
     not zero: a dasharray of zero is a solid line, so a trace whose length
     has not been measured yet would stand fully drawn through the whole boot
     instead of waiting for its own cue. */
  stroke-dasharray: var(--ct-len, 4000);
  stroke-dashoffset: var(--ct-len, 4000);
  transition: stroke-dashoffset 380ms linear;
}
.ct-line.is-drawn { stroke-dashoffset: 0; }

/* the corner marks clear the readout lane rather than setting under it */
.hub-room[data-kind='console'][data-gl] .hub-tag,
.hub-room[data-kind='console'][data-gl] .hub-cue {
  display: block;
  right: calc(var(--con-read) + 1.4vw);
}

/* no webgl: the bay hands the centre lane to the ascii web and the two text
   lanes carry on exactly as they are. the track flattens to one viewport,
   because there are no bodies left to travel between. */
.hub-room[data-kind='console'][data-nogl] .hub-track { height: 100vh; height: 100svh; }
.hub-room[data-kind='console'][data-nogl] .con-room {
  display: grid;
  place-items: center;
  padding: 2rem 1rem;
  overflow: auto;
}

@media (max-width: 860px) {
  .hub-room[data-kind='console'] {
    --con-list: auto;
    --con-read: auto;
    --con-pad: 4vw;
  }
  .hub-room[data-kind='console'][data-gl] .con-stage,
  .hub-room[data-kind='console'][data-nogl] .con-stage {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(7rem, 1fr) auto;
    padding-left: 0;
    padding-top: var(--chrome);
  }
  .con-list {
    padding: .9rem var(--con-pad) .7rem;
    border-bottom: 1px solid var(--rule);
  }
  .con-list::after,
  .con-read::before {
    top: auto;
    bottom: 0;
    left: 0;
    right: 0;
    width: auto;
    height: 1px;
    transform: scaleX(0);
    transform-origin: left center;
  }
  .con-read::before { bottom: auto; top: 0; }
  .con-list::before,
  .con-read::after { display: none; }
  .hub-room[data-kind='console'][data-boot] .con-list::after,
  .hub-room[data-kind='console'][data-boot] .con-read::before { transform: scaleX(1); }
  .cl-rail {
    display: flex;
    flex-wrap: wrap;
    gap: 0 1.1em;
  }
  .cl-row + .cl-row { margin-top: 0; }
  .cl-link { grid-template-columns: 2.6em auto; font-size: 11px; }
  .con-read {
    max-height: 34svh;
    padding: .9rem var(--con-pad) 1.2rem;
  }
  .cr-frame { max-width: 58%; }
  .cr-fact { line-height: 1.5; }
  .cr-cap { margin-bottom: .7em; }
  .cr-lore { margin-bottom: .8em; }
  .cr-facts { margin-bottom: .9em; }
  .cr-frame { margin-bottom: .9em; }
  /* both states named, so this outranks the desktop rule above rather than
     tying with it: a selector carrying one more attribute wins whatever the
     order of the two blocks. */
  .hub-room[data-kind='console'][data-gl] .hub-tag,
  .hub-room[data-kind='console'][data-gl] .hub-cue,
  .hub-room[data-kind='console'][data-nogl] .hub-tag,
  .hub-room[data-kind='console'][data-nogl] .hub-cue { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .con-list::after,
  .con-read::before,
  .con-list::before,
  .con-read::after,
  .ct-line { transition: none; }
  .hub-room[data-kind='console'][data-boot='run'] .con-scan { animation: none; }
}

/* ── 404 ───────────────────────────────────────────────────────────────── */

.dead {
  position: relative;
  padding: calc(var(--chrome) + 14vh) 4vw 12vh calc(var(--rail) + 4vw);
}
.dead-tree {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.5;
  white-space: pre;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  margin: 2.4em 0;
  opacity: .75;
  max-width: 100%;
}
.dead-tree b { color: var(--acc); font-weight: 400; }

/* ── foot ──────────────────────────────────────────────────────────────── */

.foot {
  position: relative;
  z-index: 1;
  padding: 4vh 4vw 6vh calc(var(--rail) + 4vw);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--dim);
}
.foot p { margin: 0 0 .5em; background: var(--bg); display: inline-block; }
.foot a { text-decoration: none; margin-right: 1.1em; }
.foot-cap { opacity: .45; letter-spacing: .14em; margin-right: 1.1em; }
.foot a:hover { color: var(--acc); }

/* ── responsive ────────────────────────────────────────────────────────────
   1180: the rail lane narrows a little; the tree still sets unclipped.
   980: the rail leaves the side and becomes a top strip with a drawer under
        it, the grid drops to six columns.
   820: one column. */

/* the narrow band. --rail does not move — every page's left padding is
   measured off it — so the compartments pay for themselves out of their own
   chrome instead: a tighter inset and a tighter gap, which is 8px of measure
   handed back to the tree and the difference between learning-to-theme/ on
   one line and on two. */
@media (max-width: 1180px) and (min-width: 980px) {
  :root { --rail: 214px; --rail-gap: 6px; --seg-pad: .55em; }
}

@media (max-width: 980px) {
  section.beat { grid-template-columns: repeat(6, 1fr); }
  .c1-6, .c1-7, .c1-8, .c1-9, .c1-11,
  .c3-11, .c3-13, .c7-13, .c8-13, .c9-13 { grid-column: 1 / -1; }
  .plates-band { grid-template-columns: 1fr; }
}

/* ── the rail on a phone ───────────────────────────────────────────────────
   a slim strip across the top — the mark as a word, one control — and the rail
   under it as a drawer. the latch is the checkbox, so the drawer opens and
   shuts with no script at all; below, everything the rail carries on a desktop
   is in the drawer in the same order. the old bottom strip is gone with the
   chrome that made it necessary.

   the segments stay segments in the drawer, but they stand on solid ground
   rather than on the print: a phone screen is the whole page, and reading a
   tree through moving grain in a strip of it is a worse trade than losing the
   peek. so the drawer paints its own ground and the compartments read as
   stacked cards on it. the strip itself keeps no card — it is chrome, and it
   has the drawer's own hairline under it. */
@media (max-width: 979px) {
  :root { --rail: 0px; --chrome: 44px; --rail-pad: 4vw; --rail-gap: 3vw; }

  .rail {
    bottom: auto;
    right: 0;
    width: auto;
    max-height: 100svh;
    padding: 0;
    gap: 0;
    background: var(--bg);
    pointer-events: auto;
    border-bottom: 1px solid var(--rule);
  }

  .rail-strip {
    height: var(--chrome);
    flex: none;
    border: 0;
    padding: 0 var(--rail-pad);
  }

  /* the tile stands down in the phone strip: it is nine rows tall and the
     strip is forty-four pixels, and a stamp squeezed to fit is not a stamp.
     the word takes the head of the drawer instead, which is what a phone
     wants there anyway. */
  .hm-art { display: none; }
  .hm-fallback { display: inline; }
  .homemark { flex: 0 0 auto; }

  .rail-x {
    display: inline-block;
    cursor: pointer;
    letter-spacing: .14em;
    opacity: .62;
    padding: .5em 0 .5em .8em;
  }
  .rail-x:hover { opacity: 1; color: var(--acc); }
  .rx-open { display: none; }
  .rail-latch:checked ~ .rail-strip .rx-open { display: inline; }
  .rail-latch:checked ~ .rail-strip .rx-shut { display: none; }

  /* the checkbox is the control's state and has to stay reachable by keyboard,
     so it is hidden the way the skip link is, not with display: none. */
  .rail-latch {
    display: block;
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
    border: 0;
  }
  .rail-latch:focus-visible ~ .rail-strip .rail-x {
    outline: 2px solid var(--acc);
    outline-offset: 2px;
  }

  .rail-body {
    display: none;
    max-height: calc(100svh - var(--chrome));
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--rail-gap) var(--rail-pad) 5vw;
    border-top: 1px solid var(--rule);
    gap: var(--rail-gap);
  }
  .rail-latch:checked ~ .rail-body { display: flex; }

  /* one drawer scroller, not two: the cards are as tall as their contents and
     the drawer scrolls past them, so nothing is trapped inside a card. */
  .rail-tree { flex: none; }
  .dr-scroll { overflow: visible; padding-right: 0; }
}

/* the rail proper. below this width every one of these is an ordinary block
   in reading order, which is the whole no-js, no-wide-screen floor. */
@media (min-width: 1100px) {
  /* two margins, chosen at build. "ruled" reserves the note column; "bare" is
     a print with nothing that rides in it, and it keeps only the spine. the
     sheet narrows with the lane, so the page is a column on the ground rather
     than a column with a blank strip stapled to it. */
  .sheet-wrap[data-margin="ruled"] {
    --gutter: calc(var(--marg) + var(--marg-gap) + var(--spine));
  }
  .sheet-wrap[data-margin="bare"] {
    --gutter: var(--spine);
    --sheet: calc(42rem + var(--spine));
  }

  /* the spread was anchored hard left: at 2000px the sheet ran 238 to 1211
     and left 789px of open ground on one side and none on the other, which
     is what read as a dead half rather than a margin. centred in the lane
     the depth rail leaves, the same emptiness becomes two even margins with
     the sheet lying between them. below 1100 the sheet fills the lane and
     the rule has nothing to centre. */
  .sheet { margin-inline: auto; }
  .pager { margin: 4em auto 0; }

  /* the reading column is the sheet minus the lane it reserved, taken off the
     sheet's real width so the two can never drift apart. relative because the
     spine and the section marks are positioned against it. */
  .sheet .body { max-width: calc(100% - var(--gutter)); position: relative; }

  /* scoped to the sheet: the tokens are on the wrap, and a .body used
     anywhere else (the project page's lede) must not float anything. gated on
     the ruled margin: in the bare one every one of these keeps its ordinary
     inline setting, exactly as it does below 1100px. */
  [data-margin="ruled"] .margin-rail,
  [data-margin="ruled"] .body > .mnote,
  [data-margin="ruled"] .body > blockquote.pull {
    float: right;
    /* clear is load-bearing: without it two notes near each other stack on
       the same line box and overlap, because a negative right margin gives
       the float a negative outer width. */
    clear: right;
    width: var(--marg);
    max-width: none;
    margin-right: calc(-1 * (var(--marg) + var(--marg-gap)));
    margin-left: 0;
  }

  [data-margin="ruled"] .margin-rail {
    margin-top: .35em;
    margin-bottom: 2em;
    border-top: 0;
    padding-top: 0;
  }

  [data-margin="ruled"] .body > .mnote { margin-top: .35em; margin-bottom: 1.6em; }

  /* with no lane to float into, the press block sets across the measure — and
     stacked, it is four short mono lines with two thirds of the width beside
     them. as a baseline row it is a slug line under the head, which is what a
     press block is for. */
  [data-margin="bare"] .margin-rail {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 2em;
    margin-bottom: 3em;
  }
  [data-margin="bare"] .mr-cap,
  [data-margin="bare"] .mr-place,
  [data-margin="bare"] .mr-passes { margin: 0; }
  [data-margin="bare"] .mr-numeral { font-size: 22px; margin: 0; }

  [data-margin="ruled"] .body > blockquote.pull {
    margin-top: .35em;
    margin-bottom: 1.6em;
    font-size: 14px;
    line-height: 1.5;
    padding-left: 1em;
    opacity: 1;
  }
  [data-margin="ruled"] .body > blockquote.pull p { margin: 0 0 .6em; }
  [data-margin="ruled"] .body > blockquote.pull p:last-child { margin-bottom: 0; }

  /* ── the standing furniture ──────────────────────────────────────────────
     the margin held annotation and nothing else, so a chapter with three
     margin items over sixteen thousand pixels of prose left a blank strip
     the height of the read. these two run the whole way instead: the spine,
     a ruled outer edge carrying the running folio, and a section mark at
     every h2. neither is ornament — the rule is the page's trim, and the
     mark counts which section you are standing in. */

  /* the spine. absolute inside .body, so it is exactly as tall as the read;
     the folio inside it is sticky, so it rides. it sits outside the note
     column, in the strip the notes reserve and never enter, which is what
     makes it collision-proof: no float can reach it. below 1100 it is
     display:none, and every word in it is already on the page in the header
     and the press block. */
  .sheet .body > .mr-spine {
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    position: absolute;
    top: 0;
    bottom: 0;
    left: calc(100% + var(--gutter) - var(--spine));
    width: var(--spine);
    margin: 0;
    border-left: 1px solid var(--rule);
    pointer-events: none;
  }
  /* the folio sets at the foot, where a folio goes, and sticks there — so on
     a sixteen-thousand-pixel chapter it is in the corner of every screen of
     it, and on a short print it is simply the foot of the page. */
  .sheet .body > .mr-spine span {
    position: sticky;
    bottom: 6vh;
    /* the outer half of the strip. the section marks take the inner half,
       and the two share the spine without ever landing on each other. */
    align-self: flex-end;
    writing-mode: vertical-rl;
    padding: 0;
    white-space: nowrap;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .24em;
    color: var(--dim);
    opacity: .55;
  }

  /* the section mark: a register tick across the spine at every h2, with the
     section number under it. it goes in the spine and not in the margin
     proper because the margin belongs to the notes — a numeral set there
     lands on top of whichever note happens to float beside that heading.
     the tick is not decoration: it counts, so the outer edge of the page
     says which section of the print you are standing in. */
  .sheet .body { counter-reset: sect; }
  .sheet .body h2 { position: relative; counter-increment: sect; }
  .sheet .body h2::before {
    content: '';
    position: absolute;
    top: 0;
    left: calc(100% + var(--gutter) - var(--spine));
    width: var(--spine);
    border-top: 1px solid var(--rule);
  }
  .sheet .body h2::after {
    content: counter(sect, decimal-leading-zero);
    position: absolute;
    top: .9em;
    left: calc(100% + var(--gutter) - var(--spine));
    /* the inner half of the strip; the running folio holds the outer half */
    width: calc(var(--spine) - .75rem);
    text-align: center;
    font-family: var(--mono);
    font-size: 10px;
    font-weight: 400;
    letter-spacing: .06em;
    color: var(--dim);
    opacity: .75;
  }
}

@media (min-width: 1500px) {
  .sheet-wrap {
    --sheet: 61rem;
    --marg: 15rem;
    --marg-gap: 2rem;
    --spine: 2.5rem;
  }
  .sheet-wrap[data-margin="bare"] { --sheet: calc(44rem + var(--spine)); }
  .sheet { --measure: 45rem; }
}

@media (max-width: 820px) {
  section.beat {
    padding: calc(var(--chrome) + 8vh) 6vw 9vh 6vw;
    grid-template-columns: 1fr;
  }
  /* the spans are authored against the twelve-column desktop grid; on one
     column they would open implicit tracks and squeeze copy to a word a line */
  section.beat > * { grid-column: 1 / -1 !important; }

  /* the display measure is composed for a desktop container: at 16.5vw the
     mark runs past a phone's edge and its last glyphs are simply gone. the
     coefficient came down with the same 79/65 measure the cqw line uses
     (13 * 79/65 = 15.8), and every step still lands on a whole cell. */
  h1.mark {
    margin-left: 0;
    font-size: min(15.8vw, 112px);
    font-size: round(down, min(15.8vw, 112px), 16px);
  }
  h2 { font-size: 48px; }
  .hub-head h1 { font-size: 64px; }
  .print-title { font-size: 48px; }
  .big { font-size: 64px; }

  .sheet-wrap, .hub, .dead { padding: calc(var(--chrome) + 6vh) 6vw 12vh 6vw; }
  .sheet { box-shadow: 0 0 0 .8em var(--bg), 0 0 0 calc(.8em + 1px) var(--rule); }
  .foot { padding: 4vh 6vw calc(6vh + env(safe-area-inset-bottom)) 6vw; }

  /* the bottom strip is gone with the top bar it dodged. nothing is anchored
     to the foot of a phone screen any more, so the readout sits where it
     always wanted to. */
  #pass-read { bottom: calc(1.4vh + env(safe-area-inset-bottom)); }

  .row { grid-template-columns: 4.5em 1fr; gap: .6em; }
  .row .p, .row .d { display: none; }

  .floor-list a,
  .floor-list .fl-row {
    grid-template-columns: 1fr;
    grid-template-areas: "t" "lore" "n" "go";
    gap: .35em;
  }
  .fl-go { text-align: left; opacity: 1; }

  .colophon dl > div { grid-template-columns: 1fr; gap: 0; padding: .45em 0; }
  .pager { grid-template-columns: 1fr; gap: 1.4em; }
  .pager-next { text-align: left; }
}

/* ── the scene core's annotation layer (public/js/scenes/core.js) ──
   the plates.js figure grammar at room scale: orange hairlines, a dot at
   the anchor, mono labels on a paper rect sized off their own bbox. */
.sc-annot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  overflow: visible;
}
.sc-annot .a-lead {
  stroke: var(--acc, #ff4400);
  stroke-width: 1;
  fill: none;
  vector-effect: non-scaling-stroke;
}
.sc-annot .a-dot { fill: var(--acc, #ff4400); }
.sc-annot rect.a-bg { fill: var(--bg, #000000); stroke: none; }
.sc-annot text {
  font-family: var(--mono, ui-monospace, Menlo, Consolas, monospace);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  fill: var(--acc, #ff4400);
}
.sc-annot text.a-num {
  font-family: var(--disp, "Archivo", Helvetica, Arial, sans-serif);
  font-weight: 900;
  font-size: 13px;
  letter-spacing: -0.02em;
}
.project-backdrop .sc-annot { position: fixed; }
/* the scene hub's plate appears in place: it tracks its body by the frame
   and is never eased across the stage */
.hub-room[data-scene-live] .hub-plates .hub-plate { transition: opacity 160ms linear; }
/* telemetry: the ticking mono readouts a scene pins to its frame */
.sc-annot text.a-tele { fill: var(--dim, rgba(255, 255, 255, 0.62)); font-size: 9px; }

/* the pointer is a crosshair everywhere: links, buttons, canvases, the lot.
   !important so no per-element rule above (or a script's inline style) can
   bring the hand or the arrow back. the localhost-only copy editor's text
   targets keep the i-beam, since a crosshair there hides where the caret goes. */
html, html * { cursor: crosshair !important; }
html input:not([type]), html input[type="text"], html input[type="search"], html input[type="email"], html input[type="url"], html input[type="password"], html textarea,
html [contenteditable=""], html [contenteditable="true"], html [contenteditable="plaintext-only"],
html .copy-edit [data-copy], html .copy-edit [data-src] { cursor: text !important; }
