/* ==================================================================
   DESIGN TOKENS
   ================================================================== */
:root {
  --bg: #fafaf9;      /* page background — near white */
  --bg-2: #f0efe9;    /* slightly deeper panel background */
  --line: #ddd9cf;    /* hairlines, borders */
  --ink: #17170f;     /* main text color — near black */
  --muted: #6d6c62;   /* secondary text */
  --brass: #9c6f35;   /* the one accent color, deepened for contrast on white */

  --sans: "Archivo", -apple-system, "Segoe UI", sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", monospace;

  /* The squared paper and the static are drawn on canvases now, by
     paper.js — their sizes and strengths are the tuning block at the
     top of that file. What stays here is the tone they add up to:
     --bg darkened by the wash, which the node markers and labels use
     to cut a clean hole in the paper behind them. */
  --paper-tone: #e4e4e3;
}

/* ==================================================================
   RESET
   ================================================================== */
* { box-sizing: border-box; }
html { height: 100%; margin: 0; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.6;
}
body.snap-page { height: 100%; overflow: hidden; }

h1, h2, h3 { font-family: var(--sans); font-weight: 600; margin: 0; }
a { color: var(--ink); }
dl { margin: 0; }
img { max-width: 100%; display: block; }

/* ==================================================================
   THE PAPER (landing page only)
   Squared paper with grain moving over it, sitting behind
   everything. It's keyed to --paper-in, which thread.js drives from
   the scroll position, so it arrives with the node map rather than
   being switched on. At full strength it should still be something
   you notice only if you look for it.
   ================================================================== */
.paper {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}
/* All three opacities are set frame by frame in paper.js, so the
   paper can come in later and more slowly than the map does. */
.paper-wash { position: absolute; inset: 0; background: #000; opacity: 0; }
.paper-grid, .paper-noise {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
}

/* ==================================================================
   THE THREAD (landing page only)
   One line down the whole page, drawn by thread.js. It sits above
   the slides so it stays visible where it crosses the node map's
   canvas — that crossing is the point of it.
   ================================================================== */
.thread {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  z-index: 2;
  pointer-events: none;
  overflow: visible;
}
.thread-line {
  fill: none;
  stroke: var(--ink);
  stroke-opacity: 0.26;
  stroke-width: 1;
  shape-rendering: crispEdges;
}
/* The wandering leg is a curve once it starts drifting, and its
   dashes become dots — both want round ends and smooth rendering. */
.thread-wander {
  stroke-width: 1.5;
  stroke-linecap: round;
  shape-rendering: auto;
}
/* The line that reforms on the way back up, from the sphere to the top
   of the screen. The collapse clears the page to white rather than
   darkening it, so this keeps the same ink as the rest of the thread —
   just drawn a little heavier, since for a moment it is the only thing
   on the page. */
.thread-reform {
  stroke-width: 1.5;
  shape-rendering: auto;
}

/* Present from the moment the page loads — this only stops it
   appearing at the same instant as the text above it. */
@media (prefers-reduced-motion: no-preference) {
  .thread-line { animation: thread-in 1.1s ease both 0.2s; }
}
@keyframes thread-in { from { opacity: 0; } to { opacity: 1; } }

/* ==================================================================
   SCROLL SNAP (landing page only)
   ================================================================== */
#scroll-container {
  position: relative;
  z-index: 1;
  height: 100vh;
  height: 100svh;
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  scroll-behavior: auto; /* custom easing is handled in JS instead */
}
.slide {
  height: 100vh;
  height: 100svh;
  width: 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
  display: flex;
  align-items: center;
  padding: 0 64px;
  position: relative;
}

/* ==================================================================
   SITE MENU (top-left "Menu" / "Close")
   The trigger sits on the normal light page, but the overlay itself
   stays black on purpose, regardless of the site's light theme —
   opening it dims the page behind it rather than just covering it.
   ================================================================== */

/* One shared easing curve for the whole menu, so every part of the
   transition moves with the same character. It starts almost
   imperceptibly and settles rather than stopping. */
:root { --menu-ease: cubic-bezier(0.32, 0.08, 0.24, 1); }

.menu-trigger {
  position: fixed;
  top: 22px;
  left: 22px;
  z-index: 30;
  background: none;
  border: none;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.02em;
  cursor: pointer;
  padding: 4px 0;
  transition: color 0.6s var(--menu-ease), opacity 0.2s ease;
}
.menu-trigger:hover { color: var(--brass); }
.menu-trigger.label-swap { opacity: 0; }
body.menu-open .menu-trigger { color: #f0efe8; }
body.menu-open .menu-trigger:hover { color: var(--brass); }

.menu-overlay {
  position: fixed;
  inset: 0;
  background: #0a0a0a;
  z-index: 25;
  display: flex;
  align-items: center;
  padding: 0 64px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.85s var(--menu-ease);
}
.menu-overlay.open { opacity: 1; pointer-events: auto; }

.menu-list { list-style: none; margin: 0; padding: 0; }
.menu-list li {
  margin: 6px 0;
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.6s var(--menu-ease), transform 0.7s var(--menu-ease);
}
/* Opening: the lines arrive one after another, just behind the
   backdrop. Closing: the .open class goes, so the delays go with it
   and they leave together — quicker than they came, which is what
   keeps closing from feeling sluggish. */
.menu-overlay.open .menu-list li { opacity: 1; transform: translateY(0); }
.menu-overlay.open .menu-list li:nth-child(1) { transition-delay: 0.14s; }
.menu-overlay.open .menu-list li:nth-child(2) { transition-delay: 0.20s; }
.menu-overlay.open .menu-list li:nth-child(3) { transition-delay: 0.26s; }
.menu-overlay.open .menu-list li:nth-child(4) { transition-delay: 0.32s; }
.menu-overlay.open .menu-list li:nth-child(5) { transition-delay: 0.38s; }
.menu-overlay.open .menu-list li:nth-child(6) { transition-delay: 0.44s; }
.menu-overlay.open .menu-list li:nth-child(7) { transition-delay: 0.50s; }
.menu-overlay.open .menu-list li:nth-child(n + 8) { transition-delay: 0.56s; }

.menu-list a {
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(28px, 5vw, 48px);
  text-decoration: none;
  color: #f0efe8;
  transition: color 0.35s var(--menu-ease);
}
.menu-list a:hover, .menu-list a:focus-visible { color: var(--brass); }
.menu-list .menu-current { color: var(--brass); }

/* The rest of the page gently dims while the menu is open, so the
   overlay reads as coming forward rather than just appearing.
   The transition sits on the base selector, not on .menu-open — when
   it lived on the open state only, closing the menu snapped the page
   back to full brightness with no fade at all. */
body > *:not(.menu-trigger):not(.menu-overlay):not(.cursor-ring):not(.cursor-dot) {
  transition: opacity 0.85s var(--menu-ease);
}
body.menu-open > *:not(.menu-trigger):not(.menu-overlay):not(.cursor-ring):not(.cursor-dot) {
  opacity: 0.1;
}

@media (prefers-reduced-motion: reduce) {
  .menu-list li { transform: none; }
  .menu-overlay, body > *:not(.menu-trigger):not(.menu-overlay) {
    transition-duration: 0.01s;
  }
  /* :nth-child(n) matches everything — it's here only to outweigh the
     stagger delays above, which are more specific than a plain li. */
  .menu-overlay.open .menu-list li:nth-child(n) {
    transition-duration: 0.01s;
    transition-delay: 0s;
    transform: none;
  }
}

/* ==================================================================
   TITLE SLIDE — centered
   ================================================================== */
.title-slide { justify-content: center; }
.title-content { text-align: center; }
.title-content h1 {
  font-size: clamp(32px, 5.6vw, 68px);
  line-height: 1.1;
  font-style: italic;
  font-weight: 500;   /* italics read heavier, so this drops a step to compensate */
  letter-spacing: -0.005em;
}
.title-sub { font-size: 17px; color: var(--muted); margin: 18px 0 0; }

.title-block {
  position: absolute;
  bottom: 28px;
  right: 28px;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 12px 16px;
  border: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 12px;
  color: var(--muted);
  text-align: right;
}
.scroll-cue {
  position: absolute;
  bottom: 28px;
  left: 28px;
  display: flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  color: var(--muted);
  font-family: var(--mono);
  font-size: 12px;
  cursor: pointer;
}
.scroll-cue:hover { color: var(--ink); }

@media (prefers-reduced-motion: no-preference) {
  .title-content, .title-block { animation: rise 0.9s ease both; }
  .title-block { animation-delay: 0.15s; }
}
@keyframes rise { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }

/* ==================================================================
   MIDDLE SLIDE — a single centered, italic line
   ================================================================== */
.intro-slide { justify-content: center; text-align: center; }
.intro-content { max-width: 680px; margin: 0 auto; }
.intro-lede {
  font-family: var(--sans);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(24px, 3.4vw, 38px);
  line-height: 1.5;
  color: var(--ink);
  margin: 0;
}

/* ==================================================================
   NODE MAP SLIDE — 3D scene built by node-scene.js
   ================================================================== */
/* The scene is full-bleed: it fills the whole slide, edge to edge, so
   the branches run off the sides of the screen rather than sitting in
   a box in the middle. .slide.node-slide is two classes deep on
   purpose — it has to outrank the plain .slide padding, including the
   one set inside the mobile media query at the bottom of this file. */
.slide.node-slide { padding: 0; gap: 0; align-items: stretch; }

.node-scene-wrap {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  /* pan-y, not none: vertical swipes still scroll the page on a phone,
     horizontal drags rotate the scene. */
  touch-action: pan-y;
  cursor: grab;
}
.node-scene-wrap.grabbing { cursor: grabbing; }
.node-scene-wrap canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; }

.node-labels { position: absolute; inset: 0; pointer-events: none; }
.node3d-label {
  position: absolute;
  left: 0;
  top: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  white-space: nowrap;
  text-decoration: none;
  color: var(--ink);
  pointer-events: auto;
  padding: 6px;
}
/* A registration mark rather than a dot: hollow square, centre
   point, on a ring of page colour so it keeps its edge wherever it
   crosses the grid or a branch. */
.node3d-mark {
  position: relative;
  width: 14px;
  height: 14px;
  border: 1.5px solid var(--ink);
  flex: none;
  box-shadow: 0 0 0 4px var(--paper-tone);
  transition: transform 0.3s ease;
}
.node3d-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 4px;
  height: 4px;
  background: var(--ink);
  transition: width 0.3s ease, height 0.3s ease;
}
.node3d-text {
  font-family: var(--sans);
  font-weight: 600;
  font-size: 13px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--ink);
  /* the label has to stay readable over grid, grain and branches */
  text-shadow: 0 0 7px var(--paper-tone), 0 0 14px var(--paper-tone), 0 1px 0 var(--paper-tone);
  transition: letter-spacing 0.3s ease;
}
.node3d-sub {
  position: absolute;
  left: 29px;
  top: 24px;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.45;
  color: var(--muted);
  opacity: 0;
  transition: opacity 0.35s ease;
  white-space: normal;
  width: 210px;
}

/* Hover and focus emphasise by weight, not by colour: the mark
   fills in, the text tightens and goes bold, and out in the scene
   the branch behind it thickens to match. */
.node3d-label:hover .node3d-sub, .node3d-label:focus-visible .node3d-sub { opacity: 1; }
.node3d-label:hover .node3d-mark, .node3d-label:focus-visible .node3d-mark { transform: scale(1.1); }
.node3d-label:hover .node3d-mark::after, .node3d-label:focus-visible .node3d-mark::after { width: 8px; height: 8px; }
.node3d-label:hover .node3d-text, .node3d-label:focus-visible .node3d-text { font-weight: 700; letter-spacing: 0.14em; }

/* Nodes drifting near the right edge flip their text to the other
   side of the mark so the sub-line doesn't run off screen. The mark
   itself stays exactly on the node either way. */
.node3d-label.flip { flex-direction: row-reverse; }
.node3d-label.flip .node3d-sub { left: auto; right: 29px; text-align: right; }

/* ==================================================================
   NODE CLICK PREVIEW (trial: Scent descriptions)
   The rest of the page defocuses behind it — same dark tone the
   menu overlay uses, for the same "coming forward" reason.
   ================================================================== */
body.preview-open #scroll-container,
body.preview-open .paper {
  filter: blur(2px);
  transition: filter 1.4s var(--menu-ease);
}
body.preview-open #scroll-container { opacity: 0.85; pointer-events: none; transition: filter 1.4s var(--menu-ease), opacity 1.4s var(--menu-ease); }
/* The grain gets a much heavier blur than everything else behind the
   window, and paper.js stops repainting it at the same time. Both are
   for the same reason: grain that is still churning away frame by
   frame keeps catching the eye through the defocus, and it reads as
   the page shaking rather than as depth of field. Blurred this far and
   held still, it becomes a texture instead of a movement. The blur is
   on top of the one .paper already carries. */
body.preview-open .paper-noise {
  filter: blur(6px);
  transition: filter 1.4s var(--menu-ease);
}
body.preview-open .menu-trigger { opacity: 0.15; pointer-events: none; transition: opacity 1.4s var(--menu-ease); }

.node-preview-backdrop { position: fixed; inset: 0; z-index: 50; background: transparent; cursor: pointer; }

/* The arm out to the window is the node's own branch, traced from the
   3D curve every frame by node-scene.js and carried on past the node.
   It's a filled shape rather than a stroked line so it can taper —
   almost nothing where it leaves the centre, full weight where it
   docks — with a gradient fading the same way, so it reads as coming
   forward out of the background. Both the shape and its fill are set
   from the script; what's here is only where it sits and what the
   fittings at the window end look like.

   z-index 65 puts it over the window (60), which is what lets the end
   mark straddle the window's edge. The ribbon itself stops short of
   the edge, so nothing is ever drawn across the panel. */
/* width/height are required here, not just inset: 0 — an <svg> is a
   replaced element, and without them it falls back to the CSS default
   replaced-element size (300x150) instead of filling the viewport, which
   silently clipped the whole arm to a corner box. */
.node-preview-arm-svg { position: fixed; inset: 0; width: 100%; height: 100%; z-index: 65; pointer-events: none; overflow: visible; }
.node-preview-arm-path { stroke: none; }

/* the notch cut into the window's dark edge */
.node-preview-dock-bar { stroke: #dcd8ce; stroke-width: 2; }
/* the short run from the end mark into that notch */
.node-preview-dock-stub { stroke: var(--ink); stroke-width: 1.5; }
/* and the same registration mark the nodes use, to end on */
.node-preview-dock-mark { stroke: var(--ink); stroke-width: 1.5; fill: var(--paper-tone); }
.node-preview-dock-dot { fill: var(--ink); stroke: none; }

.node-preview-modal {
  position: fixed;
  width: min(480px, 88vw);
  background: #0a0a0a;
  color: #f0efe8;
  padding: 32px;
  z-index: 60;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
  opacity: 0;
  transition: left 0.6s var(--menu-ease), top 0.6s var(--menu-ease), transform 0.6s var(--menu-ease), opacity 0.4s ease;
}
.node-preview-media {
  aspect-ratio: 4 / 3;
  border: 1px solid #333;
  background-color: #141414;
  background-image: repeating-linear-gradient(-45deg, #2a2a2a 0px, #2a2a2a 1px, transparent 1px, transparent 14px);
  margin-bottom: 22px;
}
.node-preview-desc { font-family: var(--sans); font-size: 14px; line-height: 1.6; color: #c9c6bb; margin: 0 0 26px; }
/* The node's own name, lifted out of the map and set above its window a
   beat after the window lands. It sits outside the dark panel, on the
   paper, in the map's own lettering — so it reads as the label having
   moved rather than as a heading the window came with. It arrives by
   settling: wide letter-spacing closing to the spacing the labels in
   the map are set at, and a hairline drawing out under it. */
.node-preview-title {
  position: absolute;
  left: 0;
  bottom: 100%;
  margin: 0 0 18px;
  font-family: var(--sans);
  font-weight: 600;
  font-size: 15px;
  letter-spacing: 0.36em;
  text-transform: uppercase;
  color: var(--ink);
  white-space: nowrap;
  opacity: 0;
  transform: translateY(7px);
  /* the paper behind it carries grid and grain, as under the map's labels */
  text-shadow: 0 0 7px var(--paper-tone), 0 0 14px var(--paper-tone), 0 1px 0 var(--paper-tone);
  transition: opacity 0.9s var(--menu-ease), letter-spacing 1.2s var(--menu-ease),
              transform 0.9s var(--menu-ease);
}
.node-preview-title::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -9px;
  height: 1px;
  width: 0;
  background: var(--ink);
  opacity: 0.45;
  transition: width 1.2s var(--menu-ease);
}
.node-preview-title.lifted { opacity: 1; letter-spacing: 0.11em; transform: translateY(0); }
.node-preview-title.lifted::after { width: 100%; }
/* Was a bare word in the corner. It's the same registration mark the
   nodes use, with the word beside it, so the way out of the window is
   built from the same vocabulary as the way in. */
.node-preview-close {
  position: absolute;
  top: 18px;
  right: 18px;
  display: flex;
  align-items: center;
  gap: 9px;
  background: none;
  border: none;
  padding: 6px;
  color: #8f8c81;
  cursor: pointer;
  transition: color 0.3s ease;
}
.node-preview-close:hover, .node-preview-close:focus-visible { color: #f0efe8; }
.node-preview-close-mark {
  position: relative;
  width: 12px;
  height: 12px;
  border: 1.4px solid currentColor;
  flex: none;
}
.node-preview-close-mark::before, .node-preview-close-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 7px;
  height: 1.4px;
  background: currentColor;
}
.node-preview-close-mark::before { transform: rotate(45deg); }
.node-preview-close-mark::after { transform: rotate(-45deg); }
.node-preview-close-word {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
/* Brass on black read as an illuminated exit sign — the one place the
   accent was doing something the rest of the site doesn't. Monochrome,
   with a rule that extends on hover instead of a glow. */
.node-preview-button {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-left: auto;
  width: fit-content;
  border: 1px solid #57544c;
  background: transparent;
  color: #e8e6e0;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 13px 22px;
  transition: background 0.35s ease, color 0.35s ease, border-color 0.35s ease;
}
.node-preview-button::after {
  content: "";
  width: 16px;
  height: 1px;
  background: currentColor;
  transition: width 0.35s ease;
}
.node-preview-button:hover, .node-preview-button:focus-visible {
  background: #e8e6e0;
  color: #16160f;
  border-color: #e8e6e0;
}
.node-preview-button:hover::after { width: 26px; }

/* ==================================================================
   THE CURSOR
   Built by nav.js, so it appears on every page.
   nav.js checks what's actually under the pointer each move and
   toggles .on-dark — more reliable across browsers than relying on
   mix-blend-mode, which didn't composite correctly against the
   WebGL canvas and the dark surfaces here. Any future full-bleed
   dark panel should get the .dark-surface class to be seen.
   ================================================================== */
html.has-cursor, html.has-cursor * { cursor: none !important; }
.cursor-ring, .cursor-dot {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 900;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.25s ease, width 0.22s ease, height 0.22s ease, border-color 0.2s ease, background 0.2s ease;
}
html.cursor-awake .cursor-ring, html.cursor-awake .cursor-dot { opacity: 1; }
.cursor-ring {
  width: 18px;
  height: 18px;
  border: 1px solid var(--ink);
  background: transparent;
}
.cursor-ring.near { width: 11px; height: 11px; }
.cursor-ring.on-dark { border-color: #f0efe8; }
.cursor-dot {
  width: 3px;
  height: 3px;
  background: var(--ink);
}
.cursor-dot.near { width: 5px; height: 5px; }
.cursor-dot.on-dark { background: #f0efe8; }

/* Plain-link fallback, shown only if the 3D library fails to load */
.node-fallback-list { display: flex; flex-direction: column; gap: 1px; background: var(--line); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); width: min(640px, 92vw); }
.node-fallback-list a { display: flex; flex-direction: column; gap: 4px; background: var(--bg); padding: 18px 16px; text-decoration: none; color: var(--ink); }
.node-fallback-list a:hover { background: var(--bg-2); color: var(--brass); }
.node-fallback-list a span { font-family: var(--mono); font-size: 12px; color: var(--muted); }

/* ==================================================================
   CATEGORY PAGES
   ================================================================== */
.page-content { max-width: 900px; margin: 0 auto; padding: 140px 64px 120px; }
.category-lede { font-size: clamp(22px, 2.8vw, 28px); line-height: 1.45; max-width: 640px; margin: 24px 0 64px; }

.work-list { display: flex; flex-direction: column; gap: 1px; background: var(--line); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
.work-row { display: flex; align-items: center; gap: 32px; background: var(--bg); padding: 24px 8px; text-decoration: none; color: var(--ink); }
.work-row:hover { background: var(--bg-2); }
.work-row:hover .work-row-title { color: var(--brass); }
.work-thumb { width: 96px; height: 72px; flex: none; border: 1px solid var(--line); background-image: repeating-linear-gradient(-45deg, var(--line) 0px, var(--line) 1px, transparent 1px, transparent 10px); }
.work-row-title { font-family: var(--sans); font-weight: 500; font-size: 19px; transition: color 0.2s ease; }
.work-row-meta { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-top: 6px; }

/* ==================================================================
   CONTACT SHEET CATEGORY PAGE
   A category laid out as a sheet of frames, and then as a map of
   them. Without JavaScript it is a plain grid of square pictures
   with their captions — a working page. contact-sheet.js puts
   .scripted on the sheet and takes over: one window in the middle,
   every picture flicking through it, then lines reaching out across
   the page. Everything below is how it looks; the timing and the
   arithmetic are in that file.
   ================================================================== */
/* The page grows a lot taller the moment the sheet lands, and on a
   browser with ordinary scrollbars that makes one appear — which takes
   15px off the width and shifts everything centred on the page sideways
   at exactly the moment the flick stops, so the whole thing looks like
   it twitched. Keeping room for the scrollbar from the start means
   nothing moves. :has() so it only applies to pages like this one; the
   landing page scrolls inside its own element and must not get a gutter
   it would never use. */
html:has(.sheet-page) { scrollbar-gutter: stable; }
.sheet-page { background: #fff; }

/* There is no title on this page any more. The heading stays in the
   markup for anything reading the page rather than looking at it — a
   page with no heading at all is a page nothing can announce. */
.sheet-head h1 {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The two ways of looking at the category, and the whole of this
   page's chrome now that the title has gone: fixed across the top
   between the Menu on the left and the Search on the right, so they
   sit where they sit whatever the view below them is doing. They
   arrive once the page has finished drawing itself.

   They sit INSIDE .sheet-head rather than loose in the page, and have
   to: every direct child of <body> is given an opacity transition by
   the rule that dims the page behind the menu, and that rule outranks
   anything written here — so a nav loose in the page could not be
   hidden without being seen fading away first. Inside the head block
   it is no longer a child of body, and this works as written. */
.sheet-filters {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 30;
  display: flex;
  gap: 8px;
}
.sheet-filter {
  background: none;
  border: 1px solid var(--line);
  color: var(--muted);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 8px 16px;
  cursor: pointer;
  transition: color 0.3s ease, border-color 0.3s ease;
}
.sheet-filter:hover { color: var(--ink); border-color: var(--ink); }
.sheet-filter.chosen { color: var(--ink); border-color: var(--ink); }

/* Search, top right, mirroring the Menu button top left. */
.sheet-search {
  position: fixed;
  top: 22px;
  right: 22px;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 8px;
}
.sheet-search-trigger {
  display: flex;
  align-items: center;
  gap: 8px;
  background: none;
  border: none;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: 0.02em;
  cursor: pointer;
  padding: 4px 0;
  transition: color 0.6s var(--menu-ease);
}
.sheet-search-trigger:hover { color: var(--brass); }
/* The same registration mark the map's nodes and the preview's dock
   are marked with, so the way in to searching is built out of the
   site's own vocabulary. */
.sheet-search-mark {
  position: relative;
  width: 11px;
  height: 11px;
  border: 1.5px solid currentColor;
  flex: none;
}
.sheet-search-mark::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 3px;
  height: 3px;
  background: currentColor;
}
.sheet-search-field {
  width: 0;
  opacity: 0;
  padding: 5px 0;
  border: none;
  border-bottom: 1px solid var(--line);
  background: none;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink);
  transition: width 0.5s var(--menu-ease), opacity 0.4s var(--menu-ease);
}
.sheet-search-field:focus { outline: none; border-bottom-color: var(--ink); }
.sheet-search.open .sheet-search-field { width: min(230px, 42vw); opacity: 1; }
.sheet-search.open .sheet-search-word { color: var(--brass); }

.sheet {
  position: relative;
  width: min(1240px, calc(100% - 80px));
  /* Hanging a little below the chrome rather than starting right under
     it: the Menu, the two buttons and the Search are fixed across the
     top and stay where they are — the map is what sits lower. */
  margin: clamp(18px, 3.4vh, 46px) auto 0;
  padding-bottom: 90px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 46px 24px;
}
/* Taken over by the script: every frame is placed by hand, so the
   sheet is given its height rather than deriving one. */
.sheet.scripted { display: block; }

.sheet-lines { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.sheet-route {
  fill: none;
  stroke: var(--ink);
  stroke-width: 1;
  stroke-opacity: 0.42;
}
/* The date a line carries, riding along it. Knocked out of the line
   rather than printed over it — the same thing a map does where a road
   number crosses the road. */
.sheet-date {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  fill: var(--muted);
  paint-order: stroke;
  stroke: #fff;
  stroke-width: 4px;
  stroke-linejoin: round;
  /* Written on rather than faded up: the date is uncovered from its
     first character to its last, in the direction it is read. The line
     it rides on has just been drawn the same way. */
  clip-path: inset(0 100% 0 0);
  transition: clip-path 0.55s var(--menu-ease), opacity 0.4s var(--menu-ease);
}
.sheet-date.shown { clip-path: inset(0 -2px 0 0); }

.sheet-frame {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  border: 1px solid var(--line);
  background-color: var(--bg-2);
  /* Hatching, as everywhere else on this site that a picture is still
     to come. The angle and spacing are set per frame by the script so
     that flicking through them reads as different pictures going past
     rather than as one still image. */
  background-image: repeating-linear-gradient(
    var(--hatch-angle, -45deg),
    var(--line) 0px, var(--line) 1px,
    transparent 1px, transparent var(--hatch-gap, 12px)
  );
  text-decoration: none;
  color: var(--ink);
}
.sheet.scripted .sheet-frame {
  position: absolute;
  left: 0;
  top: 0;
  aspect-ratio: auto;
  visibility: hidden;
  /* Placed by --x and --y, then tipped in place by --turn-*: the tip
     comes after the perspective, so it is a real turn towards the
     cursor rather than a squash, and the picture's middle never moves
     off the spot the map gave it. */
  transform: translate(var(--x, 0px), var(--y, 0px))
             perspective(760px)
             rotateX(var(--turn-x, 0deg)) rotateY(var(--turn-y, 0deg))
             scale(var(--lift, 1));
  transition: transform 0.28s var(--menu-ease), opacity 0.45s var(--menu-ease);
}
/* The first layout is where the pictures START, not somewhere they
   move to: with the transition above running, every one of them slid
   in from the corner of the sheet as the page opened. contact-sheet.js
   takes this off again on the frame after that first layout. */
.sheet.placing .sheet-frame { transition: none; }
/* While one picture is being pointed at, the rest of the sheet steps
   back — only just, enough to notice and not enough to interrupt the
   map. All three of these are written under .settled as well: nothing
   on the sheet answers the pointer while the page is still drawing
   itself, and during the flick every picture in turn is the one in the
   middle window. */
.sheet.scripted.settled.peeking .sheet-frame:not(:hover) { opacity: 0.62; }
.sheet.scripted.settled.peeking .sheet-route,
.sheet.scripted.settled.peeking .sheet-date { opacity: 0.45; }
.sheet.scripted.settled .sheet-frame:hover {
  z-index: 6;
  box-shadow: 0 8px 20px rgba(20, 20, 14, 0.1);
}
.sheet-frame:has(img) { background-image: none; background-color: transparent; }
.sheet-frame img { width: 100%; height: 100%; object-fit: cover; display: block; }
.sheet-number {
  position: absolute;
  top: 7px;
  left: 8px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--muted);
  background: #fff;
  padding: 1px 4px;
}
/* Captions are printed on white so that a line passing behind one is
   knocked out by it, the way a name on a map knocks out what it
   crosses. */
.sheet-frame .sheet-caption {
  position: absolute;
  left: 0;
  top: calc(100% + 8px);
  max-width: 150%;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.35;
  color: var(--muted);
  background: #fff;
  padding: 2px 5px 2px 0;
}
/* A picture comes up as its line reaches it, over a moment rather than
   in one frame: the map draws itself rather than switching itself on.
   ONLY once the sheet has settled, though. Every picture takes its turn
   in the middle window during the flick, and hiding the ones that have
   not landed yet hides the flick itself — which is exactly what this
   rule did when it was written without .settled on the front of it. */
.sheet.scripted.settled .sheet-frame:not(.landed) {
  opacity: 0;
}
.sheet.scripted.settled .sheet-frame {
  transition: opacity 0.45s var(--menu-ease);
}
/* While the pictures are still stacked in the middle window they have
   no captions — a caption belongs to a picture on the map, not to the
   flick. */
.sheet.scripted .sheet-frame .sheet-caption { opacity: 0; transition: opacity 0.5s var(--menu-ease) 0.15s; }
.sheet.scripted .sheet-frame.landed .sheet-caption { opacity: 1; }
.sheet-frame:hover { border-color: var(--ink); }
.sheet-frame:hover .sheet-caption { color: var(--ink); }
/* Whatever is in the middle window carries the window's own edge, so
   flicking through the pictures doesn't flicker the border along with
   them. Once a picture has landed on the map it goes back to the
   hairline every other frame on the site is drawn with. */
.sheet.scripted .sheet-frame:not(.landed) { border-color: var(--ink); }
.sheet-frame.is-plate { border-color: var(--ink); }
/* The settled picture's caption is printed inside its own bottom
   corner, the way a caption is written on a print — outside it is
   where the lines leave from. */
.sheet.scripted .sheet-frame.is-plate .sheet-caption {
  top: auto;
  bottom: 9px;
  left: 9px;
  max-width: calc(100% - 18px);
  padding: 3px 7px;
  color: var(--ink);
}
/* Searching: everything that doesn't match steps back. */
.sheet-frame.dimmed { opacity: 0.16; }
.sheet.searching .sheet-route, .sheet.searching .sheet-date { opacity: 0.12; }

/* With the script running the page opens white, so the name, the lede
   and the three buttons are not there to begin with — and must not be
   seen leaving either, hence transition: none on the way out. They
   arrive once the sheet has landed, the name the same way a node's
   name arrives above its window on the landing page: wide letters
   closing up, a hairline drawing out. */
body.sheet-scripted .sheet-head h1 {
  opacity: 0;
  letter-spacing: 0.08em;
  transform: translateY(7px);
  transition: none;
}
body.sheet-scripted .sheet-head h1::after { width: 0; transition: none; }
body.sheet-scripted .sheet-head .category-lede { opacity: 0; transition: none; }
body.sheet-scripted .sheet-filters { opacity: 0; transition: none; pointer-events: none; }

/* What this page is, written small beside the Menu: a rule, then the
   name of the category in the same mono the rest of the chrome is set
   in. On the contact sheet it is the only place the category is named
   now that the title has gone, and it is meant to be read second —
   after the Menu, and only if you are looking for it. */
.page-where,
.sheet-where {
  position: fixed;
  top: 22px;
  left: 76px;
  z-index: 30;
  margin: 0;
  padding: 4px 0 4px 15px;
  border-left: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 11px;
  line-height: 15px;
  letter-spacing: 0.1em;
  color: var(--muted);
  pointer-events: none;
}
body.sheet-scripted .sheet-where { opacity: 0; transition: none; }
/* On a narrow window the buttons in the middle of the chrome reach
   back to the Menu, and the name was printed underneath them. It is
   the second thing on the page to read, so it is the first to go. */
@media (max-width: 720px) {
  .page-where, .sheet-where { display: none; }
}

body.sheet-named .sheet-head h1 {
  opacity: 1;
  letter-spacing: -0.01em;
  transform: translateY(0);
  transition: opacity 0.9s var(--menu-ease), letter-spacing 1.2s var(--menu-ease),
              transform 0.9s var(--menu-ease);
}
body.sheet-named .sheet-head h1::after {
  width: 100%;
  transition: width 1.2s var(--menu-ease) 0.2s;
}
body.sheet-named .sheet-head .category-lede {
  opacity: 1;
  transition: opacity 1.1s var(--menu-ease) 0.3s;
}
body.sheet-named .sheet-filters {
  opacity: 1;
  pointer-events: auto;
  transition: opacity 0.9s var(--menu-ease);
}
body.sheet-named .sheet-where {
  opacity: 1;
  transition: opacity 1.1s var(--menu-ease) 0.15s;
}

/* ==================================================================
   FAVORITES — the other half of a contact sheet page
   The screen flickers once and what comes up is the REGISTER: a page
   ruled edge to edge with fine horizontal tracks, a small square
   travelling along each one, and a glitch that tears the whole thing
   sideways every few seconds. favorites.js does the reading, the
   drawing and the switching; this is how it looks.
/* Only one of the two views is on the page at a time. The one being
   left fades away first and the other arrives after it — favorites.js
   holds the swap until the fade is over, so they are never both up.
   The one not showing is taken out of the layout altogether, or the
   page would keep the height of whichever is taller.

   They are wrapped in .views rather than sitting loose in the page,
   and have to be: every direct child of <body> is given a transition
   by the rule that dims the page behind the menu, and that rule
   outranks anything written here — the fades below would be that
   rule's, not these. Inside a wrapper they are these. */
/* HELD BACK UNTIL THE SCRIPT HAS TAKEN OVER. Both of the pages that
   replace their own contents (this one and the theories drawing) carry
   a line in their <head> that marks the document while their script is
   on its way, and each script clears it the moment it has laid itself
   out. Without it the browser paints the no-script version first and
   then has it swept away — the page blinking its whole contents at you
   before it begins. `visibility` rather than `display` or `opacity`:
   the sheet measures its own captions on the first layout, and
   something with no layout box measures nothing. */
html.js-coming .views { visibility: hidden; }

.views { position: relative; padding-top: clamp(64px, 9vh, 96px); }
.sheet, .gallery { transition: opacity 0.5s var(--menu-ease); }
body.view-switching .sheet,
body.view-switching .gallery,
body.view-switching .sheet-head .category-lede { opacity: 0; }

.gallery { position: relative; width: 100%; margin: 0; padding: 0; display: none; }
body.view-favorites .gallery { display: block; }
body.view-favorites .sheet { display: none; }
body.view-favorites .sheet-head .category-lede { opacity: 0; }
/* Without the script both views are simply on the page, one under the
   other, which is the right fallback: everything is reachable. The
   entries are a plain list of links until then. */
body:not(.sheet-scripted) .gallery { display: block; }
.gallery-entry {
  display: block;
  padding: 10px 0;
  border-bottom: 1px solid var(--line);
  font-family: var(--sans);
  color: var(--ink);
  text-decoration: none;
}
body.sheet-scripted .gallery-entry { display: none; }

/* ------------------------------------------------------------------
   THE REGISTER
   One screen, never scrolled. The tracks are drawn on the canvas
   underneath, and the writing is laid over it — so a square running
   along a favourite's own track passes BEHIND that favourite's line of
   writing without anything having to be clipped.
   ------------------------------------------------------------------ */
.chapters {
  position: relative;
  width: 100%;
  height: calc(100vh - clamp(64px, 9vh, 96px));
  overflow: hidden;
}
/* The width and the height have to be written out: a canvas is a
   replaced element with an intrinsic size of its own, so `inset: 0`
   alone leaves it 300 by 150 in the corner rather than filling the
   page. */
.chapters-field {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* Coming up. A panel being switched on does not simply appear: it
   catches, drops, and settles. Two flickers is enough to read as one
   — any more and it reads as a fault rather than as a switch. */
@keyframes chapters-flicker {
  0% { opacity: 0; }
  12% { opacity: 0.85; }
  18% { opacity: 0.1; }
  32% { opacity: 1; }
  38% { opacity: 0.35; }
  55% { opacity: 1; }
  62% { opacity: 0.7; }
  100% { opacity: 1; }
}
.chapters-head, .chapters-body { opacity: 0; }
.chapters.flicker .chapters-head,
.chapters.flicker .chapters-body {
  animation: chapters-flicker 0.46s steps(1, end) both;
}
.chapters.lit .chapters-head,
.chapters.lit .chapters-body {
  opacity: 1;
  transition: opacity 0.5s var(--menu-ease);
}

/* --- the head line, and the code the glitch scrambles */
.chapters-head {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 20px;
  padding: 0 0 12px;
  border-bottom: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.2em;
  color: var(--muted);
}
.chapters-head p { margin: 0; }
.chapters-sig { color: var(--brass); }

/* --- the two columns underneath it */
.chapters-body {
  position: relative;
  display: grid;
  grid-template-columns: minmax(190px, 21%) 1fr;
  gap: clamp(28px, 5vw, 76px);
  padding-top: clamp(26px, 4.2vh, 52px);
}

/* THE INDEX. The chapters, stacked. Vertical rather than a strip
   across the top: the register is read down the page, and the index
   is the thing you read down it with. */
.chapters-rail {
  display: flex;
  flex-direction: column;
  border-top: 1px solid var(--line);
}
.chapters-tab {
  display: grid;
  grid-template-columns: 9px 2.4em 1fr auto;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 11px 2px;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: none;
  cursor: pointer;
  text-align: left;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  color: var(--muted);
  transition: color 0.3s var(--menu-ease), padding-left 0.3s var(--menu-ease);
}
.chapters-tab:hover, .chapters-tab:focus-visible { color: var(--ink); padding-left: 8px; }
.chapters-reg {
  width: 7px;
  height: 7px;
  border: 1px solid var(--line);
  transition: background 0.3s var(--menu-ease), border-color 0.3s var(--menu-ease);
}
.chapters-tab .chapters-name {
  font-family: var(--sans);
  font-size: 14px;
  letter-spacing: 0;
}
.chapters-count-small { color: var(--muted); }
.chapters-tab.open { color: var(--ink); }
.chapters-tab.open .chapters-reg { background: var(--brass); border-color: var(--brass); }
.chapters-tab.open .chapters-name { color: var(--ink); }

/* The reading under it, set out the way the rest of the site sets out
   a reading: a label, a rule, a figure. */
.chapters-spec {
  margin: 26px 0 0;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--muted);
}
.chapters-spec > div {
  display: flex;
  justify-content: space-between;
  gap: 12px;
  padding: 6px 2px;
  border-bottom: 1px solid var(--line);
}
.chapters-spec dt { text-transform: uppercase; }
.chapters-spec dd { margin: 0; color: var(--ink); }

/* THE LOG. The open chapter, and its favourites as full-width ruled
   rows — full width because each row is a TRACK, and a track that
   stopped short of the margin would not be one. */
.chapters-log { position: relative; min-width: 0; }
.chapters-title {
  margin: 0 0 clamp(18px, 3vh, 34px);
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(30px, 5.2vw, 62px);
  line-height: 1;
  letter-spacing: -0.015em;
  color: var(--ink);
}
.chapters-panel { display: none; }
.chapters-panel.open { display: block; border-top: 1px solid var(--line); }

.chapters-item {
  position: relative;
  display: grid;
  grid-template-columns: 2.4em 7.2em 1fr 1.6em;
  align-items: baseline;
  gap: clamp(10px, 2vw, 28px);
  padding: clamp(11px, 1.8vh, 19px) 2px;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: var(--ink);
  transition: padding-left 0.32s var(--menu-ease), background 0.32s var(--menu-ease);
}
.chapters-item-no, .chapters-date {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  color: var(--muted);
}
.chapters-item-name {
  font-family: var(--sans);
  font-size: clamp(15px, 1.5vw, 19px);
}
.chapters-go {
  font-family: var(--mono);
  color: var(--brass);
  opacity: 0;
  transition: opacity 0.32s var(--menu-ease);
}
/* A favourite lights its own track right across the page, so what
   answers the pointer here is deliberately quiet: the drawing is doing
   the talking. */
.chapters-item:hover, .chapters-item:focus-visible {
  padding-left: 10px;
  background: rgba(23, 23, 15, 0.025);
}
.chapters-item:hover .chapters-go,
.chapters-item:focus-visible .chapters-go { opacity: 1; }
.chapters-item:hover .chapters-date,
.chapters-item:focus-visible .chapters-date { color: var(--brass); }

/* ------------------------------------------------------------------
   THE TEAR
   What the glitch does to the WRITING. Only ever where a line is
   drawn, never what it says: the row slips sideways and is fringed in
   the two accents for a tenth of a second, and no character of any
   name, date or number is ever anything but itself.
   ------------------------------------------------------------------ */
.chapters-item.torn, .chapters-title.torn {
  transform: translateX(var(--tear, 0px));
  text-shadow: 2px 0 rgba(156, 111, 53, 0.55), -2px 0 rgba(47, 95, 150, 0.5);
}

@media (max-width: 560px) {
  .chapters-head { font-size: 9px; letter-spacing: 0.1em; }
}

@media (max-width: 860px) {
  .chapters-body { grid-template-columns: 1fr; gap: 26px; }
  .chapters-spec { margin-top: 16px; }
  .chapters-item { grid-template-columns: 2.2em 6.6em 1fr 1.2em; }
}

/* Nothing on this page moves when animation is turned off: the
   register is ruled once and left, and nothing ever tears. */
@media (prefers-reduced-motion: reduce) {
  .chapters-head, .chapters-body { opacity: 1; }
  .chapters-item.torn, .chapters-title.torn { transform: none; text-shadow: none; }
  .chapters-tab, .chapters-item, .chapters-go, .chapters-reg { transition: none; }
}

/* ==================================================================
   THE CHAMBER — categories/favorites.html
   Four injectors, one at each corner of the window, firing streams of
   square particles inward on white; they settle into a tilted ring,
   and the word FAVORITES stands in the middle of it. Pressing the word
   throws the ring out to the borders and opens the menu out of it.
   Two canvases: everything further than the middle of the chamber is
   drawn under the writing, everything nearer over it, so the near half
   of the ring passes in front of the word. Everything here is written
   under `body.chambered`, which chamber.js adds; without it the page
   is the plain list of favourites, and stays that way.
   ================================================================== */
body.chambered { background: var(--bg); }
body.chambered .page-content { padding: 0; margin: 0; max-width: none; }
/* The list is still in the page for anything reading rather than
   looking — it is the menu's own index — but it is not drawn. */
body.chambered .work-list,
body.chambered .page-content > h1,
body.chambered .page-content > .category-lede {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Before chamber.js has taken over — see the note in the page's head. */
html.js-coming body.favorites-page .page-content { visibility: hidden; }

/* THE STEP BETWEEN THE TWO STATES — one curve and one length, and
   everything that moves when the menu opens is eased on them: the
   lettering here, and the orbit itself in chamber.js, which solves
   this same bezier rather than easing on one of its own. They used to
   be two different curves over the same 1.7s — the drawing on a
   symmetrical S and the writing on the site's `--menu-ease`, which
   sets off quicker and has a longer tail — so the two arrived at
   noticeably different moments and the step read as several things
   happening at once rather than as one. Change either number here and
   change OPEN_MS / STEP_EASE in chamber.js with it. */
.chamber {
  --chamber-step: cubic-bezier(0.45, 0, 0.45, 1);
  --chamber-step-ms: 1.9s;
  position: fixed;
  inset: 0;
  z-index: 0;
  background: var(--bg);
}
.chamber-field, .chamber-front {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}
/* The near half of the chamber is drawn over the writing, so it must
   never be the thing you click. */
.chamber-front { z-index: 3; pointer-events: none; }

/* What stands in the middle of the ring: the word alone to begin with,
   and the menu it opens into. Both are centred on the same spot, so
   the menu reads as the word opening out rather than as a panel
   arriving from somewhere else. */
.chamber-plate {
  position: fixed;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: center;
  width: min(94vmin, calc(100% - 28px));
  max-height: calc(100vh - 140px);
  /* The plate is only somewhere to stand: it is wider than the menu
     it holds, so anything it caught either side of the menu would be
     a click that looked like it landed on the page and did nothing. */
  pointer-events: none;
}
.chamber-word, .chamber-panel { pointer-events: auto; }

/* THE WORD. Technical, in the mono the rest of the site keeps for its
   readings, bracketed by the same hollow registration square the site
   marks a point with.

   IT IS SET WIDER THAN THE RING IS, and that is the whole reason it is
   this size. The ring is centred on the word, so a smaller word would
   sit in the hole of the ellipse and nothing would ever cross it. Set
   a little wider, the ring's left and right rims fall ACROSS the ends
   of the lettering — and because one of those rims is nearer than the
   middle of the chamber and the other further, one crosses in front of
   the word and the other passes behind it. That is what gives the word
   a place in the volume instead of making it a caption printed over a
   picture of one. Sized against `vmin` because the ring is too (it is
   drawn against the smaller side of the window), so the two keep their
   proportion at every window size. */
.chamber-word {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.18em;
  padding: 0.3em 0.62em 0.36em;
  border: 0;
  background: none;
  cursor: pointer;
  white-space: nowrap;
  font-family: var(--mono);
  /* Against `vmin` because the orbit is, and capped against `vw` as
     well so the lettering cannot run off the sides of a narrow window
     — on a phone the orbit is drawn against the short side and the
     word against the long one, and only the cap keeps the two apart.

     It is set SMALLER than it was, and `RING` in chamber.js came down
     with it: the word still has to be a little wider than the orbit or
     the orbit's rims cannot cross the ends of the lettering, so the
     two sizes are one decision and neither moves alone. */
  font-size: clamp(17px, min(6vmin, 5.4vw), 68px);
  letter-spacing: 0.55em;
  text-transform: uppercase;
  color: var(--ink);
  /* ONE THING MOVES, AND IT MOVES ONCE. The whole step from the word
     to the menu's heading is the lettering getting smaller, and every
     other measurement on it is written in `em` so it simply comes with
     — the letter-spacing, the padding, the crop marks, the gap either
     side of the marks. Those used to be animated in their own right,
     each with its own duration, and several of them in pixels, so they
     arrived at slightly different moments and the word appeared to
     settle in stages. One property, one duration, and it is smooth
     because there is nothing left to fall out of step with.

     That duration and that curve are the orbit's own step out
     (OPEN_MS and STEP_EASE in chamber.js), so the lettering and the
     particles set off together, travel together and arrive together.
     They used to share only the duration. */
  transition: font-size var(--chamber-step-ms) var(--chamber-step);
}
.chamber-word-line {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.52em;
}
/* Hovering it does not tint it: what the hand does to the word is
   close the crop marks round it, which is the gesture that says it is
   a control. Nothing on this page answers in colour any more. */
.chamber-word:hover .chamber-reg, .chamber-word:focus-visible .chamber-reg {
  background: var(--ink);
}

/* THE CROP MARKS. Four corner brackets round the word, the way a
   drawing brackets the thing it is about — and the first half of
   saying "this is a control": closed, the word is the only thing on
   the page, so it has to read as something you press rather than as a
   heading somebody centred. */
.chamber-crop {
  position: absolute;
  width: 0.34em;
  height: 0.34em;
  border: 0 solid var(--line);
  /* The brackets are sized in `em`, so they come down with the
     lettering on their own. This transition is only for the growth on
     hover, and it is kept SHORT for a reason: a transition on a length
     written in `em` is also a transition on the lettering getting
     smaller, and at 0.4s the brackets were still closing for most of
     half a second after the word and the menu had both landed. That
     tail is the one thing you could still point at in the step. */
  transition: border-color 0.35s var(--menu-ease), width 0.15s var(--menu-ease),
              height 0.15s var(--menu-ease);
}
.chamber-crop.tl { top: 0; left: 0; border-top-width: 1px; border-left-width: 1px; }
.chamber-crop.tr { top: 0; right: 0; border-top-width: 1px; border-right-width: 1px; }
.chamber-crop.bl { bottom: 0; left: 0; border-bottom-width: 1px; border-left-width: 1px; }
.chamber-crop.br { bottom: 0; right: 0; border-bottom-width: 1px; border-right-width: 1px; }
/* Pressed-looking on the way in: the brackets run out towards each
   other so the four of them read as one frame closing round the word,
   which is as near as this page comes to saying "button" without
   drawing a box permanently round the only thing on it. */
.chamber-word:hover .chamber-crop, .chamber-word:focus-visible .chamber-crop {
  border-color: var(--ink);
  width: 0.9em;
  height: 0.55em;
}
.chamber-word:active .chamber-crop { width: 1.3em; height: 0.7em; }

/* THE CUE. The other half: a small boxed label under the lettering
   saying what pressing it does, and which way it will go. It is set in
   pixels rather than with the word, so it stays a control's size
   however large the lettering is drawn. */
.chamber-cue {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  margin-top: 0.12em;
  padding: 6px 14px;
  border: 1px solid var(--line);
  background: var(--bg);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.26em;
  text-transform: uppercase;
  color: var(--muted);
  transition: border-color 0.35s var(--menu-ease), color 0.35s var(--menu-ease),
              background 0.35s var(--menu-ease);
}
.chamber-cue-name { margin-right: -0.26em; }
.chamber-cue-mark {
  width: 5px;
  height: 5px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform 0.55s var(--menu-ease);
}
.chamber-word:hover .chamber-cue, .chamber-word:focus-visible .chamber-cue {
  border-color: var(--ink);
  color: var(--ink);
  background: var(--bg-2);
}
.chamber-word:active .chamber-cue {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bg);
}
.chamber-plate.open .chamber-cue-mark { transform: translateY(1px) rotate(225deg); }
.chamber-reg {
  width: 0.34em;
  height: 0.34em;
  border: 1px solid var(--ink);
  flex: none;
  transition: background 0.35s var(--menu-ease),
              border-color var(--chamber-step-ms) var(--chamber-step);
}
/* The trailing letter-space would otherwise push the lettering off
   centre against the two marks. */
.chamber-word-name { margin-right: -0.55em; }
/* Open, the word is the menu's own heading. Only the size is set: the
   spacing, the padding, the marks and the brackets are all in `em` and
   come down with it, which is what makes the step one movement rather
   than five. */
.chamber-plate.open .chamber-word { font-size: 13px; }
.chamber-plate.open .chamber-reg { border-color: var(--muted); }

.chamber-panel {
  width: min(460px, 100%);
  margin-top: 10px;
  padding: 18px 22px 22px;
  border: 1px solid var(--line);
  background: var(--bg);
  overflow: auto;
  /* It comes up over the same moment the orbit takes to widen, and
     waits out the first third of it: the word has to have begun coming
     down to a heading before the menu appears under it, or the two
     read as two separate things happening at once.

     It fades and rises a few pixels, and does nothing else. It used to
     be squashed flat and stretched out (`scaleY(0.72)`), which carries
     every line of writing in it with it: the lettering was drawn at
     the wrong height and then corrected, which is the opposite of
     minimal however smoothly it is eased. */
  animation: chamber-open 1.25s var(--chamber-step) 0.65s both;
}
@keyframes chamber-open {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: none; }
}
/* AND IT LEAVES ON THE SAME STEP IT ARRIVED ON. It used to be taken
   off the page in the frame the word was pressed, leaving the orbit to
   spend the next two seconds coming back in on its own — the one half
   of this page's only movement that was a cut rather than a movement.
   It goes back into the word instead, over the first part of the step,
   and chamber.js takes it off the page at the end of THAT rather than
   at the end of the step — `SHUT_MS` there is this duration, and must
   stay it. Nothing in it answers the hand on the way out. */
.chamber-plate.shutting .chamber-panel {
  pointer-events: none;
  animation: chamber-shut 1.1s var(--chamber-step) both;
}
@keyframes chamber-shut {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: translateY(-8px); }
}

.chamber-head {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 8px;
  padding-bottom: 12px;
  border-bottom: 1px solid var(--line);
}
.chamber-back {
  flex: none;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.chamber-back[hidden] { display: none; }
.chamber-back:hover, .chamber-back:focus-visible { color: var(--ink); }
.chamber-spec {
  margin: 0;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.chamber-level[hidden] { display: none; }
.chamber-level { animation: chamber-level 0.4s var(--menu-ease) both; }
@keyframes chamber-level {
  from { opacity: 0; transform: translateX(8px); }
  to { opacity: 1; transform: none; }
}

/* ONE COLUMN, and no strip across the top: the chapters stand in it
   first, and opening one puts its favourites in the same column in
   their place. A row is a stack — its number and reading above, its
   name under them — whether it is a chapter or a favourite, so the two
   levels read as one thing going deeper rather than as two designs. */
.chamber-row {
  position: relative;
  display: block;
  width: 100%;
  padding: 15px 22px 15px 0;
  border: none;
  background: none;
  text-align: left;
  text-decoration: none;
  color: var(--ink);
  cursor: pointer;
}
/* THE RULE UNDER A ROW IS WHAT ANSWERS THE HAND, and it answers by
   GETTING SHORTER. The rows are ruled off from each other edge to
   edge; point at one and its own rule draws back from the right,
   press it and it draws back further still. It is drawn as a layer of
   its own rather than as the row's `border-bottom`, because a border
   cannot be shortened without making the row itself narrower — and
   `transform` is the one thing that can be animated here without the
   browser laying the menu out again on every frame.

   This replaced an indent, where the whole row stepped sideways under
   the pointer. Both say "this one", but a line getting shorter is the
   drawing's own language and moves nothing that anyone is reading. */
.chamber-row::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--line);
  transform-origin: left center;
  transition: transform 0.5s var(--menu-ease), background 0.5s var(--menu-ease);
}
.chamber-row:hover::after, .chamber-row:focus-visible::after {
  transform: scaleX(0.3);
  background: var(--ink);
}
.chamber-row:active::after { transform: scaleX(0.13); }
.chamber-file {
  display: flex;
  gap: 12px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  color: var(--muted);
}
.chamber-no { color: var(--ink); }
.chamber-name {
  display: block;
  margin-top: 5px;
  font-family: var(--sans);
  font-size: clamp(16px, 1.5vw, 19px);
  letter-spacing: -0.01em;
}
.chamber-go {
  position: absolute;
  right: 0;
  bottom: 15px;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--muted);
  opacity: 0;
  transform: translateX(-6px);
  transition: opacity 0.3s var(--menu-ease), transform 0.3s var(--menu-ease);
}
.chamber-row:hover .chamber-go,
.chamber-row:focus-visible .chamber-go { opacity: 1; transform: none; color: var(--ink); }

@media (max-width: 720px) {
  .chamber-plate { width: calc(100% - 44px); }
  .chamber-panel { padding: 16px 16px 18px; }
  .chamber-head { flex-wrap: wrap; gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  .chamber-panel, .chamber-level { animation-duration: 0.01s; }
  .chamber-word, .chamber-row::after { transition-duration: 0.01s; }
}

/* ==================================================================
   THE STRUCTURE — categories/theories.html
   A technical drawing in three dimensions that you scroll *into*: a
   frame of ribs and rails running away into the depth, a ruled spine
   along the floor, and a fine swarm of particles in the air. It is
   drawn on a fixed canvas; the page itself is made tall enough to
   hold the road, and the scroll position is the travel. Everything
   here is written under `body.structured`, which structure.js adds —
   without it the page is the plain list of rows every other category
   uses, and stays that way.
   ================================================================== */
body.structured { background: #0a0b0e; }
/* Before structure.js has taken over: the ground is already the
   drawing's, so what follows is the drawing arriving rather than one
   page being swapped for another. */
html.js-coming body.theories-page { background: #0a0b0e; }
html.js-coming body.theories-page .page-content { visibility: hidden; }
html.js-coming body.theories-page .menu-trigger { color: #dce6f7; }
body.structured .page-content { padding: 0; margin: 0; max-width: none; }
/* The list is still in the page for anything reading rather than
   looking — it is the structure's own index — but it is not drawn.
   Pinned to the corner with its margins taken off: left where it was
   laid out it adds to the height of a page whose height is the road. */
body.structured .work-list,
body.structured .page-content > h1,
body.structured .page-content > .category-lede {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  margin: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The Menu is written on the drawing now, so it has to be light. */
body.structured .menu-trigger { color: #dce6f7; }

/* What you scroll down. It has no content of its own: it is there to
   give the page a height, so that the scrollbar, the arrow keys, Page
   Down and a finger on a phone all drive the travel without a line of
   code catching them. */
.structure-road { width: 100%; }

.structure {
  position: fixed;
  inset: 0;
  z-index: 0;
  background: #0a0b0e;
}
.structure-field { display: block; width: 100%; height: 100%; }

.structure-readout {
  position: fixed;
  top: 22px;
  right: 26px;
  margin: 0;
  padding: 4px 0;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 15px;
  letter-spacing: 0.12em;
  color: rgba(226, 232, 242, 0.6);
}
body.structured .page-where { color: rgba(226, 232, 242, 0.55); border-left-color: rgba(226, 232, 242, 0.2); }

/* THE OPENING. The drawing sets itself up when the page loads — the
   rails shoot out, the ribs come up out of the depth, the rule writes
   itself along the floor — and the chrome arrives with it rather than
   standing there first over an empty screen. structure.js adds `lit`
   when the setting up has finished. */
.structure:not(.lit) .structure-readout,
.structure:not(.lit) .page-where,
.structure:not(.lit) .structure-cue { opacity: 0; }
.structure .structure-readout,
.structure .page-where { transition: opacity 0.9s var(--menu-ease); }

/* Said once, at the top of the road, and gone as soon as you have
   started down it. */
.structure-cue {
  position: fixed;
  left: 50%;
  bottom: 34px;
  transform: translateX(-50%);
  margin: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  color: rgba(226, 232, 242, 0.42);
  transition: opacity 0.7s var(--menu-ease);
  pointer-events: none;
  z-index: 2;
}
.structure-cue.gone { opacity: 0; }
.structure-cue-mark {
  width: 9px;
  height: 9px;
  border: 1px solid rgba(226, 232, 242, 0.42);
}

/* THE WHEEL. The rule down the middle is drawn on the canvas; this is
   the piece of the screen that answers the hand, laid over the near
   end of it where the rule is widest and easiest to catch. Dragging
   it writes the page's own scroll position, so it and the scrollbar
   can never disagree about where you are.

   `touch-action: none` is what stops a finger dragging it from also
   scrolling the page underneath — which would travel twice as far as
   the hand asked for. */
.structure-spine {
  position: fixed;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: min(340px, 36vw);
  height: 42vh;
  border: none;
  background: linear-gradient(to top, rgba(205, 218, 235, 0.05), rgba(205, 218, 235, 0));
  opacity: 0;
  transition: opacity 0.4s var(--menu-ease);
  touch-action: none;
  pointer-events: auto;
  z-index: 1;
}
.structure-spine:hover, .structure-spine.held, .structure-spine:focus-visible { opacity: 1; }
.structure-spine:focus-visible { outline: 1px solid rgba(142, 180, 226, 0.5); outline-offset: -2px; }

/* A station is the thing you click: the link is laid over the
   assembly rather than beside it, and structure.js sizes it to
   whatever that assembly is doing on the screen this frame — so
   nothing here may give one a size or a transform of its own. */
.structure-marks { position: fixed; inset: 0; pointer-events: none; z-index: 2; }
.structure-stop {
  position: absolute;
  display: block;
  text-decoration: none;
  pointer-events: auto;
  border: 1px solid transparent;
  transition: border-color 0.3s var(--menu-ease);
}
.structure-stop.gone { display: none; }
.structure-stop:hover, .structure-stop:focus-visible { border-color: rgba(142, 180, 226, 0.4); }
.structure-say {
  position: absolute;
  left: 0;
  top: 100%;
  margin-top: 8px;
  display: block;
  white-space: nowrap;
}
.structure-no {
  display: block;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  color: rgba(142, 180, 226, 0.9);
}
.structure-name {
  display: block;
  margin-top: 3px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: 16px;
  letter-spacing: -0.01em;
  color: #eef1f6;
}
.structure-meta {
  display: block;
  margin-top: 3px;
  font-family: var(--mono);
  font-size: 10px;
  color: rgba(226, 232, 242, 0.42);
  opacity: 0;
  transition: opacity 0.3s var(--menu-ease);
}
/* The line under the name is only worth reading once you are among
   it, so it waits until you are close and until you point at it. */
.structure-stop.close:hover .structure-meta,
.structure-stop.close:focus-visible .structure-meta { opacity: 1; }
.structure-stop:hover .structure-name,
.structure-stop:focus-visible .structure-name { color: #9ecbff; }

/* THE CARD. Clicking a station lays it out on the window and writes
   this beside it — the preview of the theory. It is INSIDE the
   station's own link, so the card is more of the station rather than a
   second thing to aim at, and the click that follows opens the piece.

   It waits for the figure to finish coming forward before it writes
   itself in: the station is set out first, and only then is there
   something beside it to read. */
.structure-card {
  position: absolute;
  left: calc(100% + 54px);
  top: 50%;
  width: min(360px, 30vw);
  padding: 18px 20px 0;
  border-left: 1px solid rgba(142, 180, 226, 0.55);
  background: rgba(10, 11, 14, 0.66);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-50%) translateX(-12px);
  transition: opacity 0.35s var(--menu-ease), transform 0.35s var(--menu-ease),
              visibility 0s linear 0.35s;
}
/* The leader from the figure to the card, the way the drawing joins
   anything to the note about it. */
.structure-card::before {
  content: "";
  position: absolute;
  right: 100%;
  top: 30px;
  width: 54px;
  height: 1px;
  background: rgba(142, 180, 226, 0.4);
}
.structure-stop.open .structure-card {
  opacity: 1;
  visibility: visible;
  transform: translateY(-50%) translateX(0);
  transition: opacity 0.5s var(--menu-ease) 0.3s, transform 0.5s var(--menu-ease) 0.3s,
              visibility 0s;
}
.structure-card > span { display: block; }
.structure-card-kicker {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  color: rgba(142, 180, 226, 0.9);
}
.structure-card-name {
  margin-top: 10px;
  font-family: var(--sans);
  font-weight: 500;
  font-size: clamp(21px, 2.1vw, 30px);
  letter-spacing: -0.015em;
  line-height: 1.1;
  color: #eef1f6;
  white-space: normal;
}
.structure-card-meta {
  margin-top: 12px;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.6;
  color: rgba(226, 232, 242, 0.5);
  white-space: normal;
}
.structure-card-note {
  margin-top: 12px;
  font-family: var(--sans);
  font-size: 13px;
  line-height: 1.6;
  color: rgba(226, 232, 242, 0.75);
  white-space: normal;
}
.structure-card-spec {
  margin-top: 16px;
  padding-top: 12px;
  border-top: 1px solid rgba(226, 232, 242, 0.14);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  color: rgba(150, 160, 176, 0.8);
}
.structure-card-open {
  margin-top: 14px;
  padding-bottom: 18px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(142, 180, 226, 0.9);
}
.structure-stop.open:hover .structure-card-open,
.structure-stop.open:focus-visible .structure-card-open { color: #9ecbff; }

/* One name, one place: the station's name is in the card while it is
   open, so the copy under the figure goes — and the names of the
   stations it is standing in front of go with it. */
.structure-stop.open .structure-say { opacity: 0; }
.structure-marks.holding .structure-stop:not(.open) .structure-say { opacity: 0; }
.structure-say { transition: opacity 0.35s var(--menu-ease); }

@media (max-width: 860px) {
  /* The two numbers stay: they are how you know where you are. */
  .structure-readout { top: 16px; right: 16px; font-size: 10px; letter-spacing: 0.08em; }
  .structure-spine { width: min(260px, 60vw); }
}

/* Where there is no room beside the figure the card goes under it, and
   structure.js lays the figure higher up the window to make that room.
   The two have to agree: this is the same condition. */
@media (max-width: 860px), (max-aspect-ratio: 95/100) {
  .structure-card {
    left: 50%;
    top: calc(100% + 44px);
    width: min(420px, 84vw);
    padding: 16px 18px 0;
    transform: translate(-50%, 10px);
  }
  .structure-stop.open .structure-card { transform: translate(-50%, 0); }
  .structure-card::before {
    right: auto;
    left: -1px;
    top: -44px;
    width: 1px;
    height: 44px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Held still rather than switched off, like the rest of the drawing:
     the card is simply there as soon as the station is open. */
  .structure-card,
  .structure-stop.open .structure-card { transition-duration: 0.01s; transition-delay: 0s; }
  .structure-say { transition-duration: 0.01s; }
}

/* ==================================================================
   GALLERY WORK PAGE
   ================================================================== */
.work-header { max-width: 640px; margin: 0 auto; padding: 140px 64px 80px; }
.work-header .work-kicker { font-family: var(--mono); font-size: 12px; color: var(--muted); margin-bottom: 12px; }
.work-header h1 { font-size: clamp(30px, 4.5vw, 48px); }
.work-header .work-intro { color: var(--muted); margin-top: 20px; max-width: 56ch; }

.gallery-entry { max-width: 900px; margin: 0 auto; padding: 48px 64px; display: flex; gap: 48px; align-items: flex-start; border-top: 1px solid var(--line); }
.gallery-entry .entry-number { font-family: var(--mono); font-size: 12px; color: var(--brass); flex: none; width: 32px; padding-top: 4px; }
.gallery-entry .entry-media { flex: 1 1 55%; aspect-ratio: 4 / 3; border: 1px solid var(--line); background-color: var(--bg-2); background-image: repeating-linear-gradient(-45deg, var(--line) 0px, var(--line) 1px, transparent 1px, transparent 14px); position: relative; }
.gallery-entry .entry-media::after { content: "your image here"; position: absolute; bottom: 12px; left: 12px; font-family: var(--mono); font-size: 11px; color: var(--muted); background: var(--bg); padding: 3px 7px; border: 1px solid var(--line); }
.gallery-entry .entry-media:has(img) { background-image: none; }
.gallery-entry .entry-media:has(img)::after { display: none; }
.gallery-entry .entry-media img { width: 100%; height: 100%; object-fit: cover; }
.gallery-entry .entry-text { flex: 1 1 45%; color: var(--muted); max-width: 48ch; }

/* ==================================================================
   ARTICLE WORK PAGE
   ================================================================== */
.article-body { max-width: 640px; margin: 0 auto; padding: 0 64px 120px; }
.article-body h2 { font-size: 21px; margin: 48px 0 16px; }
.article-body p { margin: 0 0 18px; color: var(--muted); }
.article-body p + h2 { margin-top: 56px; }

.infobox { float: right; width: 260px; margin: 0 0 24px 32px; border: 1px solid var(--line); background: var(--bg-2); padding: 16px; }
.infobox .infobox-media { aspect-ratio: 4/3; border: 1px solid var(--line); background-image: repeating-linear-gradient(-45deg, var(--line) 0px, var(--line) 1px, transparent 1px, transparent 10px); margin-bottom: 12px; }
.infobox dl { font-family: var(--mono); font-size: 12px; }
.infobox > dl > div { display: flex; gap: 8px; padding: 6px 0; border-top: 1px solid var(--line); }
.infobox dt { color: var(--muted); min-width: 74px; }
.infobox dd { margin: 0; }

/* ==================================================================
   TEST NODE PAGES (sandbox)
   ================================================================== */
.test-tag { display: inline-block; font-family: var(--mono); font-size: 12px; color: var(--brass); border: 1px solid var(--line); padding: 3px 8px; margin-bottom: 20px; }

/* ==================================================================
   CONTACT
   ================================================================== */
.contact-email { display: inline-block; font-size: 19px; text-decoration: none; border-bottom: 1px solid var(--brass); padding-bottom: 2px; }
.contact-email:hover { color: var(--brass); }
.contact-links { margin-top: 32px; display: flex; gap: 24px; }
.contact-links a { font-family: var(--mono); color: var(--muted); text-decoration: none; font-size: 13px; }
.contact-links a:hover { color: var(--ink); }

/* ==================================================================
   FOCUS STATES
   ================================================================== */
a:focus-visible, button:focus-visible { outline: 1px solid var(--brass); outline-offset: 3px; }

/* ==================================================================
   RESPONSIVE
   ================================================================== */
@media (max-width: 860px) {
  .menu-overlay { padding: 0 24px; }
  .slide { padding: 0 24px; flex-direction: column !important; justify-content: center; gap: 20px; }
  .title-block { right: 16px; bottom: 16px; }
  .scroll-cue { left: 16px; bottom: 16px; }
  .page-content, .work-header, .article-body { padding-left: 24px; padding-right: 24px; }
  .gallery-entry { flex-direction: column; padding: 32px 24px; gap: 20px; }
  .gallery-entry .entry-text { max-width: none; }
  .infobox { float: none; width: 100%; margin: 0 0 32px; }
  .node3d-text { font-size: 14px; }
  .node3d-sub { width: 160px; }
  /* 80px of margin is a quarter of a phone's screen: the ring needs
     the room more than the page needs the gutter. */
  .gallery { width: calc(100% - 24px); }
}
