/* Two families, no more. Switzer is the whole readable system — body, index, labels,
   buttons; a clean neutral grotesque, which is where the slickness comes from. Antic Didone
   carries display: a light, geometric Didone that sits with the lockup's thick/thin
   without borrowing its ornament. Fragment Mono survives only inside the
   screenplay, where monospace is the format's own convention rather than a choice.
   The name itself is Nearvana, shipped as traced outlines, not a webfont. */
@font-face { font-family: Switzer; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/switzer-400.woff2") format("woff2"); }
@font-face { font-family: Switzer; font-style: normal; font-weight: 500; font-display: swap; src: url("/fonts/switzer-500.woff2") format("woff2"); }
@font-face { font-family: Switzer; font-style: normal; font-weight: 600; font-display: swap; src: url("/fonts/switzer-600.woff2") format("woff2"); }
@font-face { font-family: "Antic Didone"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/antic-didone-400.woff2") format("woff2"); }
@font-face { font-family: "Fragment Mono"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/fragment-mono-400.woff2") format("woff2"); }

/* Willy Stagg — rebuild 2026-08-21
   Rail index + scrolling stream. Monochrome inversion. Engraved instrument motifs. */

:root {
  --night: #000000;
  --ink: #000000;
  --paper: #f4f1ea;
  /* The rail is sized to be the largest the name can be without eating into the
     reading column. Measured at 1440: the prose measure holds at its 62ch cap until
     the rail passes ~500px, then starts losing characters. 35vw tracks that ceiling
     across widths, and above ~1500px it is free — .band-inner caps at 1080px, so the
     extra rail width comes out of dead margin and the reel never changes size. */
  --rail: clamp(420px, 35vw, 680px);
  --gutter: clamp(1.5rem, 4vw, 5rem);
  --measure: 62ch;

  --text: Switzer, "Helvetica Neue", Inter, system-ui, sans-serif;
  /* The name is set in Nearvana and shipped as traced outlines, not a webfont. */
  --mono: "Fragment Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --display: "Antic Didone", Didot, "Bodoni 72", Georgia, serif;

  --grain: url("/grain.svg");
  --grain-type: url("/grain-type.svg");
}

* { box-sizing: border-box; }
html { background: var(--night); scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
body { background: var(--night); color: var(--paper); font-family: var(--text); font-size: 16px; margin: 0; overflow-x: hidden; position: relative; }

/* Film stock over the whole frame, in two passes.

   One pass cannot cover both polarities: soft-light and overlay leave pure black
   untouched (soft-light(0, x) = 0), so a single overlay grains the cream bands and
   does nothing at all to the ink ones. So the highlights get soft-light with grey-
   centred noise, and the shadows get a sparse speck field screened on top — which is
   almost inert against cream (it lifts it by ~1/255) and carries the whole ink band.
   Measured contribution: sigma ~2.4 on paper, ~3.3 on ink, black lifted to #040404.

   Both layers are absolutely positioned against the body rather than fixed to the
   viewport, so the grain is printed onto the page and travels with it under scroll —
   a still frame, not a dirty lens. It also stops the two blended layers recompositing
   on every scroll tick. */
body::before {
  background-image: url("/grain.svg"); background-repeat: repeat; background-size: 180px 180px;
  content: ""; inset: 0; mix-blend-mode: soft-light; opacity: 0.75; pointer-events: none;
  position: absolute; z-index: 200;
}
body::after {
  background-image: url("/grain-shadow.svg"); background-repeat: repeat; background-size: 180px 180px;
  content: ""; inset: 0; mix-blend-mode: screen; opacity: 0.35; pointer-events: none;
  position: absolute; z-index: 201;
}

/* Filter definitions only. Pinned to zero in every dimension: left to its intrinsic
   replaced-element size some engines paint a 300x150 box at the top-left corner. */
.defs { height: 0; left: 0; overflow: hidden; pointer-events: none; position: absolute; top: 0; visibility: hidden; width: 0; }

a { color: inherit; text-decoration: none; }
a:focus-visible { outline: 1px solid currentColor; outline-offset: 4px; }
p { margin: 0; }
h1, h2, h3 { font-weight: 400; margin: 0; }

/* Grain printed into display letterforms, both polarities. background-clip: text only
   paints glyphs inside the element's own box, so on a tight line-height a descender
   that hangs below the box is cut off (the g of "good" lost 2-4px). The padding gives
   descenders room to paint; the matching negative margin keeps layout where it was. */
.grain-type {
  background-blend-mode: multiply, normal;
  background-image: var(--grain-type), linear-gradient(var(--type-fill, currentColor), var(--type-fill, currentColor));
  background-repeat: repeat, repeat;
  margin-bottom: -0.2em; padding-bottom: 0.2em;
}
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .grain-type { -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
}

/* ---------------------------------------------------------------- RAIL */

.rail {
  bottom: 0; color: #ffffff; display: flex; flex-direction: column;
  left: 0; mix-blend-mode: difference; overflow-y: auto; padding: clamp(1.75rem, 2.4vw, 2.6rem) 12px;
  padding-left: 56px; position: fixed; scrollbar-width: none; top: 0; width: var(--rail); z-index: 60;
}
/* Last-resort valve for very short windows; the rhythm rules below normally prevent it. */
.rail::-webkit-scrollbar { display: none; }

/* The name, once, fixed in the corner — no second copy in the hero. The artwork is a
   10:1 single line, which at rail width would be 30px tall, so it is sliced into two
   stacked lines instead: one element shows the mask's left half, the other its right.
   Rasterising the traced outlines showed a clean 127px empty column at 47.2–53.3% of
   the artwork, so a cut at exactly 50% lands inside the word space and touches no
   letterform. Each half is 2077x414 of the 4154x414 whole, hence the aspect ratio;
   at mask-size 200% the mask's own height then matches the element's exactly.
   currentColor plus the rail's difference blend is what makes it invert on scroll. */
.lockup {
  aspect-ratio: 2077 / 414; background: currentColor; display: block; width: 100%;
}
.lockup--a {
  -webkit-mask: url("/name-lockup.svg") no-repeat left center / 200% auto;
  mask: url("/name-lockup.svg") no-repeat left center / 200% auto;
}
.lockup--b {
  -webkit-mask: url("/name-lockup.svg") no-repeat right center / 200% auto;
  mask: url("/name-lockup.svg") no-repeat right center / 200% auto;
  /* Percentage margins resolve against the containing block's width, so the gap
     between the two lines scales with the lockup instead of pinning to a font size
     the lockup does not use. */
  margin-top: 0.8%;
}
/* Fills the rail, except on short windows: the index and the contact block below it
   need about 530px between them, so past that point the name has to give. 56vh is the
   largest cap that never binds at 1920x1080 and still clears a 1920x760 laptop. */
.wordmark { display: block; font-size: inherit; font-weight: 400; margin: 0; width: min(100%, 56vh); }
.wordmark a { display: block; }
.index { margin-block: auto; padding-block: 2.5rem; }
.index-group { display: grid; grid-template-columns: 2.6rem 1fr; margin-bottom: 2rem; }
.numeral {
  font-family: var(--display); font-size: 0.88rem; font-weight: 400; letter-spacing: 0.1em;
  opacity: 0.6; padding-top: 0.3rem;
}
.index-head {
  font-size: 1.04rem; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase;
  transition: opacity 200ms ease;
}
.index-children { font-size: 0.99rem; grid-column: 2; list-style: none; margin: 0.95rem 0 0; padding: 0; }
.index-children li { margin-bottom: 0.6rem; }
.index-children a { font-weight: 500; opacity: 0.58; transition: opacity 200ms ease; }
.index a:hover { opacity: 1; }
.index a.is-current { opacity: 1; }
.index-children a.is-current::before,
.index-head.is-current::before {
  content: ""; display: inline-block; height: 1.5px; margin-right: 0.6rem;
  vertical-align: middle; width: 18px; background: currentColor;
}

.rail-foot {
  border-top: 1px solid rgba(255, 255, 255, 0.3); display: grid; font-family: var(--text);
  font-size: 0.92rem; gap: 0.5rem; letter-spacing: 0.02em; opacity: 0.82; padding-top: 1.35rem;
}
.rail-foot a { transition: opacity 200ms ease; }
.rail-foot a:hover { opacity: 0.6; }

/* -------------------------------------------------------------- STREAM */

/* No z-index here on purpose: it used to make the stream a stacking context, which
   capped everything inside it below the grain layers and made the reel impossible to
   lift clear of them. Nothing needs it — the rail still out-ranks the stream. */
.stream { position: relative; }
.band { padding-left: var(--rail); position: relative; }
.band--ink { --flip-bg: var(--paper); --flip-fg: var(--ink); --hair: rgba(244, 241, 234, 0.18); --wash: rgba(244, 241, 234, 0.05); background: var(--ink); color: var(--paper); }
.band--paper { --flip-bg: var(--ink); --flip-fg: var(--paper); --hair: rgba(11, 11, 12, 0.2); --wash: rgba(11, 11, 12, 0.035); background: var(--paper); color: #0b0b0c; }
/* Constellation field on the dark bands. */
.band--ink::before {
  background-image:
    radial-gradient(circle, rgba(255, 255, 255, 0.55) 0.6px, transparent 0.7px),
    radial-gradient(circle, rgba(255, 255, 255, 0.32) 0.5px, transparent 0.6px);
  background-position: 0 0, 43px 61px;
  background-size: 137px 149px, 89px 97px;
  content: ""; inset: 0; opacity: 0.55; pointer-events: none; position: absolute;
}
.band-inner { margin-inline: auto; max-width: 1080px; padding-inline: var(--gutter); position: relative; width: 100%; z-index: 2; }

.tag {
  font-family: var(--text); font-size: 0.76rem; font-weight: 600; letter-spacing: 0.22em;
  opacity: 0.6; text-transform: uppercase;
}

/* The meta never breaks inside itself. When a phone is too narrow for it to share the
   title's line, the head wraps and the meta drops whole onto a line of its own. */
.band-head {
  align-items: baseline; border-bottom: 1px solid currentColor; display: flex; flex-wrap: wrap;
  gap: 0.55rem 1.4rem; padding-bottom: 1.15rem;
}
.band-head .numeral { opacity: 0.55; padding: 0; }
.section-title {
  font-family: var(--display); font-size: clamp(1.42rem, 2.1vw, 1.95rem); font-weight: 400;
  letter-spacing: 0.12em; line-height: 1; margin-right: auto; text-transform: uppercase;
}
.band-meta { font-family: var(--text); font-size: 0.78rem; font-weight: 600; letter-spacing: 0.16em; opacity: 0.58; text-transform: uppercase; white-space: nowrap; }

/* ------------------------------------------------------------ MASTHEAD */

.masthead { display: flex; flex-direction: column; justify-content: center; min-height: 100vh; overflow: hidden; padding-block: clamp(5rem, 10vh, 9rem); }
.masthead-inner { max-width: 1180px; }
.disciplines {
  display: flex; flex-wrap: wrap; font-family: var(--text);
  font-size: clamp(0.86rem, 1.02vw, 1.02rem); font-weight: 600; gap: 0.9rem clamp(1.6rem, 2.6vw, 2.6rem);
  letter-spacing: 0.17em; list-style: none; margin: 0 0 clamp(1.8rem, 3.2vw, 2.7rem);
  opacity: 0.88; padding: 0; text-transform: uppercase;
}
.disciplines li { align-items: center; display: flex; gap: 0.62em; white-space: nowrap; }
/* Sized in em so the mark tracks the label, and optically a shade larger than the
   cap height so it reads as a companion rather than a bullet. */
.disc-mark { flex: none; height: 1.75em; opacity: 0.82; width: 1.75em; }
.sr-only {
  clip-path: inset(50%); height: 1px; overflow: hidden; position: absolute;
  white-space: nowrap; width: 1px;
}
/* The hero statement is the page's other big line, so it takes the display face for
   the same reason the section titles and the closing line do. The measure is set to
   break it after "Materials" rather than after "and" — 16ch to 18ch all land there,
   so 17ch sits mid-window and survives font-loading variance. */
.statement {
  font-family: var(--display); font-size: clamp(1.5rem, 3vw, 2.85rem); font-weight: 400;
  letter-spacing: 0.002em; line-height: 1.16; margin-top: 0;
  max-width: 17ch;
}
.lede {
  font-size: clamp(1rem, 1.14vw, 1.11rem); line-height: 1.68; margin-top: clamp(1.5rem, 2.4vw, 2.1rem);
  max-width: 46ch; opacity: 0.62;
}

/* A dot that breathes rather than blinks: the halo is grown out of the dot and faded
   over 2.4s, which is slow enough to read as a pulse. The dot itself never moves. */
.avail {
  align-items: center; display: flex; font-family: var(--text); font-size: 0.76rem;
  font-weight: 600; gap: 0.65rem; letter-spacing: 0.18em;
  margin-top: clamp(1.4rem, 2.2vw, 1.9rem); opacity: 0.75; text-transform: uppercase;
}
.avail-dot {
  background: currentColor; border-radius: 50%;
  flex: none; height: 6px; position: relative; width: 6px;
}
.avail-dot::after {
  animation: avail-pulse 2400ms cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
  background: currentColor; border-radius: 50%; content: ""; inset: 0; position: absolute;
}
@keyframes avail-pulse {
  0%   { opacity: 0.5; transform: scale(1); }
  70%  { opacity: 0;   transform: scale(3.6); }
  100% { opacity: 0;   transform: scale(3.6); }
}
/* The manifest closes the hero. Eight tags, two per discipline above it, set as an even
   grid: four across (writing and film over AI and research) where there is room, pairs
   on phones. A wrapping row left the last two stranded. Set at the section strips' size
   and tracking so it reads as an index. */
.hero-tags {
  border-top: 1px solid var(--hair); display: grid; font-family: var(--text);
  font-size: 0.72rem; font-weight: 600; gap: 0.55rem 1.5rem; grid-template-columns: repeat(2, auto); justify-content: start;
  letter-spacing: 0.18em; list-style: none; margin: clamp(1.6rem, 2.6vw, 2.2rem) 0 0; max-width: min(100%, 880px);
  opacity: 0.5; padding: clamp(1rem, 1.6vw, 1.35rem) 0 0; text-transform: uppercase;
}
@media (max-width: 400px) { .hero-tags { column-gap: 0.9rem; letter-spacing: 0.14em; } }
@media (min-width: 700px) { .hero-tags { grid-template-columns: repeat(4, auto); } }
.scroll-cue {
  bottom: clamp(1.5rem, 3vw, 2.5rem); font-family: var(--text); font-size: 0.76rem; font-weight: 600;
  left: calc(var(--rail) + var(--gutter)); letter-spacing: 0.3em; opacity: 0.45;
  position: absolute; text-transform: uppercase; z-index: 2;
}

/* Moving water behind the hero. Graded to near-black so only the caustics survive —
   the plate's own median sits at 5/255 — which is why it reads as light on obsidian
   rather than as a video playing. No #dimple filter here: the water already refracts,
   and displacing it again only muddied the threads. The page's grain layers fall on
   it normally; unlike the reel, this is scenery, not footage. */
/* The gradient is the floor, not decoration: if the video and its poster are missing,
   or the codecs unsupported, the <video> box renders empty and this shows through, so
   the hero is never a black hole. With the plate at 0.72 the two also sit together —
   the gradient reads as depth behind the water rather than as a separate layer. */
.field {
  background:
    radial-gradient(ellipse 52% 62% at 68% 38%, #3a3a3a 0%, #1c1c1c 34%, #0a0a0a 62%, #000 84%),
    radial-gradient(ellipse 64% 48% at 92% 82%, #262626 0%, #0d0d0d 48%, #000 78%),
    #000;
  inset: 0; overflow: hidden; pointer-events: none; position: absolute; z-index: 1;
}
/* The poster, painted under the plate. The plate stays invisible until its clock is
   seen moving, so whatever WebKit draws over a paused video (the play badge) is never
   seen; this frame stands in until then. It is frame 0 of the loop, so the hand-off is
   seamless, and it steps aside only once the plate has faded fully in. */
.field::before {
  background: url("/hero-water-poster.jpg") center center / cover no-repeat;
  content: ""; inset: 0; opacity: 0.72; position: absolute; z-index: 0;
}
.field.is-live::before, .field.has-still::before { opacity: 0; transition: opacity 0s linear 0.6s; }
.field-plate {
  display: block; height: 100%; object-fit: cover; object-position: center center;
  opacity: 0; position: relative; transition: opacity 0.6s ease; width: 100%; z-index: 1;
}
.field.is-live .field-plate { opacity: 0.72; }
/* Safari's fallback when autoplay is refused: the same MP4 as an animated image. */
.field-still {
  height: 100%; inset: 0; object-fit: cover; object-position: center center;
  opacity: 0.72; position: absolute; transition: opacity 0.6s ease; width: 100%; z-index: 1;
}
.field.is-live .field-still { opacity: 0; }
/* Belt and braces for older WebKit, which still honours these selectors. Current iOS
   does not, which is why the plate is hidden while paused rather than relying on this. */
.field-plate::-webkit-media-controls,
.field-plate::-webkit-media-controls-enclosure,
.field-plate::-webkit-media-controls-panel,
.field-plate::-webkit-media-controls-overlay-play-button,
.field-plate::-webkit-media-controls-start-playback-button {
  -webkit-appearance: none; appearance: none; display: none !important; opacity: 0 !important;
}
/* Wipe the caustics off the reading side rather than dimming the whole plate. */
.field::after {
  background: linear-gradient(100deg, #000 0%, rgba(0, 0, 0, 0.94) 26%, rgba(0, 0, 0, 0.55) 52%, rgba(0, 0, 0, 0.22) 78%, rgba(0, 0, 0, 0.12) 100%);
  content: ""; inset: 0; position: absolute; z-index: 2;
}

/* ---------------------------------------------------------------- FILM */

#film { padding-block: clamp(5rem, 9vw, 8.5rem); }
/* The reel carries its own grain; the page's shouldn't sit on top of it. The film band
   has no .field plate to out-rank, so its .band-inner can drop back to z-index auto —
   which stops it forming a stacking context and lets the frame alone climb past the
   grain. Everything else in the band stays under it. */
#film .band-inner { z-index: auto; }
.reel-frame {
  aspect-ratio: 1280 / 546; background: #000; margin-top: clamp(2.2rem, 4vw, 3.4rem);
  overflow: hidden; position: relative; z-index: 210;
}
.reel-frame video { background: #000; display: block; height: 100%; object-fit: cover; object-position: center center; width: 100%; }
.reel-frame::after {
  border: 1px solid var(--hair); content: ""; inset: 0; pointer-events: none; position: absolute;
}
/* The caption answers the band head: a Didone line where the head has the title, and
   the roles as a spaced row like the hero tags. Balanced so nothing is orphaned. */
.reel-caption { display: grid; gap: 0.85rem; margin-top: clamp(1.4rem, 2.4vw, 1.9rem); }
.reel-line {
  font-family: var(--display); font-size: clamp(1.14rem, 1.55vw, 1.42rem); font-weight: 400;
  letter-spacing: 0.01em; line-height: 1.35; text-wrap: balance;
}
.reel-credits {
  display: flex; flex-wrap: wrap; font-family: var(--text); font-size: 0.72rem; font-weight: 600;
  gap: 0.45rem 1.5rem; letter-spacing: 0.16em; list-style: none; margin: 0; opacity: 0.55;
  padding: 0; text-transform: uppercase;
}
/* Five roles on one line down to a 360px phone, rather than GRADE alone on a second. */
@media (max-width: 480px) { .reel-credits { column-gap: 0.75rem; font-size: 0.68rem; letter-spacing: 0.12em; } }

/* -------------------------------------------------------------- PIECES */

.piece { padding-block: clamp(4rem, 7vw, 7rem); }
/* The tighter rhythm is for pieces stacked against each other. The Writing section's
   outer edges take the same padding as every other section, so its head lands in the
   same place when the index jumps to it. */
#writing > .piece:first-child { padding-top: clamp(5rem, 9vw, 8.5rem); }
#writing > .piece:last-child { padding-bottom: clamp(5rem, 9vw, 8.5rem); }
/* Every article now carries a section-style head of its own, so the grid below it
   picks up the same offset the reel and the window use under theirs. */
.piece .piece-inner { margin-top: clamp(2.2rem, 4vw, 3.4rem); }
/* The index and kicker moved up into that head, so the display title is now the first
   thing in the body and no longer needs to clear anything above it. */
.piece-body > .piece-title:first-child { margin-top: 0; }
.piece-inner { display: grid; gap: clamp(2rem, 4vw, 3.5rem); grid-template-columns: 190px minmax(0, 1fr); }
.piece-plate { aspect-ratio: 3 / 4; box-shadow: inset 0 0 0 1px var(--hair); overflow: hidden; position: relative; }
.plate-fill {
  background: radial-gradient(ellipse 74% 64% at 42% 32%, #ffffff 0%, #cfcfcf 20%, #8e8e8e 42%, #454545 66%, #101010 88%);
  filter: url(#dimple-soft); inset: -18%; position: absolute;
}
.plate-b { background: radial-gradient(ellipse 84% 74% at 68% 62%, #f4f4f4 0%, #bdbdbd 22%, #737373 46%, #333 70%, #0d0d0d 90%); }
.plate-c { background: radial-gradient(ellipse 64% 84% at 32% 68%, #ffffff 0%, #c4c4c4 18%, #7e7e7e 44%, #2e2e2e 72%, #0b0b0b 92%); }
.plate-d { background: radial-gradient(ellipse 92% 56% at 54% 22%, #ffffff 0%, #c8c8c8 20%, #7a7a7a 48%, #262626 74%, #090909 92%); }
.plate-e { background: radial-gradient(ellipse 70% 88% at 60% 40%, #ffffff 0%, #d4d4d4 16%, #909090 40%, #3a3a3a 68%, #0c0c0c 90%); }

.piece-index { font-family: var(--text); font-size: 0.8rem; font-weight: 600; letter-spacing: 0.18em; margin-bottom: 1.1rem; opacity: 0.55; }
.piece-title {
  font-family: var(--display); font-size: clamp(1.62rem, 2.75vw, 2.5rem); font-weight: 400;
  letter-spacing: 0.002em; line-height: 1.18; margin-top: 0.9rem; max-width: 23ch;
}
.piece-deck { font-size: clamp(1rem, 1.1vw, 1.09rem); line-height: 1.62; margin-top: 1.1rem; max-width: 54ch; opacity: 0.66; }

.prose { margin-top: clamp(2rem, 3.5vw, 3rem); max-width: var(--measure); }
.prose p { font-size: 1.03rem; line-height: 1.72; margin-bottom: 1.35rem; opacity: 0.86; }
.prose p:last-child { margin-bottom: 0; }
.piece-context, .piece-actions { font-family: var(--text); font-size: 0.92rem; letter-spacing: 0.01em; line-height: 1.8; margin-top: 2.2rem; max-width: 58ch; opacity: 0.6; }
.piece-actions { opacity: 1; }
.piece-actions a {
  border-bottom: 1px solid currentColor; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.18em;
  padding-bottom: 0.3rem; text-transform: uppercase; transition: opacity 200ms ease;
}
.piece-actions a:hover { opacity: 0.6; }

/* The live window is a real one: one click lifts the shield below and the frame takes
   pointer events and focus, and "Visit PepTutor" under it is the way out to the full site.
   Same lift the reel needed — .band-inner's z-index was capping everything in the band
   below the grain layers, which printed film grain over a running website. Dropping it
   to auto stops the stacking context forming and lets the window climb past them.
   Measured with a flat plate inside the frame: sigma 0.000, identical to grain off. */
#systems .band-inner { z-index: auto; }
.window {
  aspect-ratio: 16 / 10; background: #000; border: 1px solid var(--hair);
  margin-top: clamp(2rem, 3.5vw, 3rem); overflow: hidden; position: relative; z-index: 210;
}
.window-plate { border: 0; display: block; height: 100%; position: relative; width: 100%; z-index: 1; }
/* Sits behind the frame and is covered by it the moment it paints. Only ever visible
   if a browser or extension refuses the third-party frame, which turns an empty box
   into a labelled plate. */
.window-fallback {
  color: var(--paper); font-family: var(--text); font-size: 0.76rem; font-weight: 600; left: 0;
  letter-spacing: 0.22em; opacity: 0.4; position: absolute; text-align: center;
  text-transform: uppercase; top: 50%; transform: translateY(-50%); width: 100%; z-index: 0;
}
.window:hover { border-color: currentColor; }
/* Clear cover over the frame so scrolling past it scrolls the page, not PepTutor.
   A click lifts it; the script in index.html puts it back. */
.window-shield {
  background: transparent; border: 0; cursor: pointer; inset: 0; margin: 0; padding: 0;
  position: absolute; z-index: 2;
}
.window-shield span {
  background: rgba(11, 11, 12, 0.8); bottom: 0.9rem; color: var(--paper); font-family: var(--text);
  font-size: 0.7rem; font-weight: 600; letter-spacing: 0.2em; opacity: 0; padding: 0.6rem 0.85rem;
  position: absolute; right: 0.9rem; text-transform: uppercase; transition: opacity 200ms ease;
}
.window-shield:hover span { opacity: 1; }
@media (hover: none) { .window-shield span { opacity: 0.92; } }
.window.is-live .window-shield { display: none; }

.screenplay {
  background: var(--wash); border: 1px solid var(--hair);
  font-family: var(--mono); font-size: 0.8rem; line-height: 1.62; margin-top: clamp(2rem, 3.5vw, 3rem);
  max-width: 760px; padding: clamp(1.6rem, 4vw, 3.4rem);
}
.screenplay p { margin-bottom: 1.05rem; opacity: 0.84; }
.screenplay .scene-heading { letter-spacing: 0.08em; opacity: 1; text-transform: uppercase; }
.screenplay .character { margin: 1.5rem 0 0.1rem 34%; opacity: 1; text-transform: uppercase; }
.screenplay .dialogue { margin-left: 18%; max-width: 64%; }
.screenplay .parenthetical { margin: 0 0 0.1rem 26%; opacity: 0.6; }
.screenplay .dialogue .parenthetical { display: inline; margin: 0; }

/* ------------------------------------------------------------- SYSTEMS */

/* Systems uses the same figure the articles do — plate on the left, one column of copy
   on the right — rather than the facing-column grid it had, which stranded the title
   alone on one side and every word on the other. It also holds the live window now,
   between the prose and the stack: read what it is, watch it run, then see the parts. */
#systems { padding-block: clamp(5rem, 9vw, 8.5rem); }
#systems .piece-inner { margin-top: clamp(2.2rem, 4vw, 3.4rem); }
#systems .piece-title { max-width: 30ch; }
#systems .piece-actions { margin-top: 2rem; }
.capabilities {
  border-top: 1px solid var(--hair); display: flex; flex-wrap: wrap; font-family: var(--text);
  font-size: 0.78rem; font-weight: 600; gap: 0.5rem 1.5rem; letter-spacing: 0.2em; list-style: none;
  margin: 2.2rem 0 0; opacity: 0.58; padding: 1.2rem 0 0; text-transform: uppercase;
}

/* ------------------------------------------------------------- CONTACT */

#contact { overflow: hidden; padding-block: clamp(5rem, 9vw, 8.5rem) clamp(2rem, 3vw, 3rem); }
.contact-line {
  font-family: var(--display); font-size: clamp(2.2rem, 4.8vw, 4.4rem); font-weight: 400;
  letter-spacing: 0.002em; line-height: 1.08; margin-top: clamp(2.5rem, 5vw, 4rem); max-width: 14ch;
  text-wrap: balance;
}
.contact-row { align-items: center; display: flex; flex-wrap: wrap; gap: 1.6rem; margin-top: clamp(2.2rem, 4vw, 3.2rem); }
.button {
  border: 1px solid currentColor; font-family: var(--text); font-size: 0.84rem; font-weight: 600;
  letter-spacing: 0.16em; padding: 1rem 1.6rem; text-transform: uppercase;
  transition: background 220ms ease, color 220ms ease;
}
.button:hover { background: var(--flip-bg); color: var(--flip-fg); }
.link {
  border-bottom: 1px solid currentColor; font-family: var(--text); font-size: 0.84rem; font-weight: 600;
  letter-spacing: 0.16em; padding-bottom: 0.3rem; text-transform: uppercase; transition: opacity 200ms ease;
}
.link:hover { opacity: 0.6; }
.colophon {
  border-top: 1px solid var(--hair); display: flex; flex-wrap: wrap; font-family: var(--text);
  font-size: 0.76rem; gap: 1rem; justify-content: space-between; letter-spacing: 0.16em;
  margin-top: clamp(4rem, 8vw, 7rem); opacity: 0.5; padding-top: 1.4rem; text-transform: uppercase;
}
/* A 44px hit area without moving the line. */
.colophon a { margin-block: -0.9rem; padding-block: 0.9rem; }

/* --------------------------------------------------------- RESPONSIVE */

@media (max-width: 1180px) {
  :root { --rail: clamp(300px, 30vw, 400px); }
  .piece-inner { grid-template-columns: 140px minmax(0, 1fr); }
}

@media (max-width: 900px) {
  :root { --rail: 0px; }
  /* Anchor jumps stop below the sticky bar. --bar is measured by index.html; the
     fallback is the two-row bar at phone width. */
  html { scroll-padding-top: var(--bar, 78px); }
  /* z-index 220: the reel and the PepTutor window sit at 210 to clear the grain, and
     as a top bar the rail has to clear them too or they scroll over it. The bar is
     solid black, so losing the grain over it costs nothing visible. */
  .rail {
    background: #000; bottom: auto; color: #fff; flex-direction: row; flex-wrap: wrap;
    gap: 0.4rem 1.4rem; align-items: baseline; mix-blend-mode: normal; padding: 1rem var(--gutter);
    position: sticky; width: 100%; z-index: 220;
  }
  /* The bar's links are 14px tall. These invisible extensions take each one to ~42px
     for a thumb without changing the bar's height. */
  .index-head, .wordmark a { position: relative; }
  .index-head::after { content: ""; inset: -14px -9px; position: absolute; }
  .wordmark a::after { content: ""; inset: -12px -6px; position: absolute; }
  .rail-foot, .index-children, .index .numeral, .lockup--b { display: none; }
  /* The sticky bar has no room to stack, so the mobile lockup goes back to one line. */
  .wordmark { margin-right: auto; width: 186px; }
  .lockup--a {
    aspect-ratio: 4154 / 414;
    -webkit-mask: url("/name-lockup.svg") no-repeat center / contain;
    mask: url("/name-lockup.svg") no-repeat center / contain;
  }
  .index { display: flex; gap: 1.2rem; margin: 0; padding: 0; }
  .index-group { display: block; margin: 0; }
  .index-head { font-size: 0.72rem; letter-spacing: 0.12em; }
  .band { padding-left: 0; }
  .masthead { min-height: 88vh; }
  .piece-inner { grid-template-columns: 1fr; }
  .piece-plate { aspect-ratio: 16 / 9; max-width: 340px; }
  .scroll-cue { left: var(--gutter); }
  /* A single narrow column has no clean side to push the caustics off to, so the
     wipe becomes an even hold-down instead of a left-to-right one. */
  .field::after {
    background: linear-gradient(180deg, rgba(0, 0, 0, 0.78) 0%, rgba(0, 0, 0, 0.62) 45%, rgba(0, 0, 0, 0.82) 100%);
  }
  .screenplay .character { margin-left: 26%; }
  .screenplay .dialogue { margin-left: 8%; max-width: 84%; }
  .screenplay .parenthetical { margin-left: 18%; }
}

/* Short windows: tighten the rail's vertical rhythm before anything has to scroll.
   The index and contact block are a fixed ~530px otherwise, which overflowed below
   roughly 700px of viewport height even at the old, smaller lockup size. */
@media (min-width: 901px) and (max-height: 800px) {
  .index { padding-block: 1.1rem; }
  .index-group { margin-bottom: 1.15rem; }
  .index-children { margin-top: 0.6rem; }
  .index-children li { margin-bottom: 0.4rem; }
  .rail-foot { font-size: 0.86rem; gap: 0.32rem; padding-top: 1rem; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; }
  /* The water keeps running here on purpose. This setting exists to suppress motion
     that can cause discomfort — parallax, spin, anything tracking the viewport. The
     plate is a near-black ambient wash (measured median 5/255) drifting slowly behind
     a gradient, decorative and aria-hidden, with nothing tied to scroll. Hiding it
     outright meant anyone with Reduce Motion enabled system-wide — which plenty of
     people leave on for unrelated reasons — silently got a frozen hero. It is damped
     instead. */
  .field::before { opacity: 0.5; }
  .field.is-live .field-plate, .field-still { opacity: 0.5; }
  .avail-dot::after { animation: none; opacity: 0.35; }
}
