@charset "utf-8";
/* ═══════════════════════════════════════════════════════════════════════════
   THE TRANSCRIPT — Weichen Fan

   Invariants: upright mono is the record, cursive-italic serif is the thought;
   colour identifies the speaker and nothing else; de-emphasis removes COLOUR,
   never contrast.

   (1) reset  (2) tokens  (3) base type  (4) layout  (5) components
   (6) texture  (7) @supports  (8) breakpoints  (9) PLAIN VIEW
   (10) print  (11) reduced-motion / contrast / forced-colors
   ═══════════════════════════════════════════════════════════════════════════ */


/* ═══ 1 · RESET ══════════════════════════════════════════════════════════ */

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%;text-size-adjust:100%}
body{margin:0}
h1,h2,h3,p,figure,dl,dd,fieldset,legend,ol,ul{margin:0;padding:0}
h1,h2,h3{font-size:inherit;font-weight:inherit}
ol,ul{list-style:none}
/* Safari drops the AX list role on list-style:none; an empty marker keeps it. */
@supports (list-style-type:""){ol,ul{list-style-type:""}}
fieldset{border:0;min-inline-size:0}
img,svg{display:block;max-width:100%}
button{font:inherit;color:inherit;background:none;border:0;padding:0;margin:0;text-align:inherit;cursor:pointer}
a{color:inherit}
b,strong{font-weight:inherit}
details summary::-webkit-details-marker{display:none}
[hidden]{display:none!important}
::selection{background:var(--rule-hair);color:var(--ink)}


/* ═══ 2 · TOKENS ═════════════════════════════════════════════════════════ */

:root{
  /* ── SURFACES ────────────────────────────────────────── */
  --paper:#F2EFE6;          /* page ground, warm parchment            */
  --paper-inset:#E9E5D9;    /* refile strip + colophon band only      */
  --bone-card:#FBFAF5;      /* hard-card fill on hover/focus          */

  /* ── INK ─────────────────────────────────────────────── */
  --ink:#12120E;            /* 16.33:1 on paper · all primary text    */
  --ink-muted:#5F5C52;      /*  5.82:1 on paper · authors, dates      */
  --rule-hair:#C9C4B4;      /*  1.52:1 · NON-TEXT hairlines only      */
  --rule-strong:#A9A292;    /*  2.21:1 · NON-TEXT read-progress only  */
  --shadow:#38423F;         /* desaturated teal-green offset shadow   */
  --trail-rgb:18,18,14;     /* the nib's ink, read once by the canvas  */

  /* ── VOICES · colour = speaker, nothing else ─────────── */
  --voice-enc:#4E4C44;      /*  7.48:1 ENCYCLOPEDIA   (registrar)     */
  --voice-vis:#2E5A55;      /*  6.74:1 VISUAL CALCULUS (pixels)       */
  --voice-int:#7A5C18;      /*  5.42:1 INTERFACING (systems, robots)  */
  --voice-rhe:#7A2B25;      /*  8.30:1 RHETORIC    (the objector)     */
  /* Lamplight-only faculties, defined here because a --speaker resolving to an
     undefined custom property is an invalid value; GAMES and MUSIC are
     display:none in this theme. Palette mutual-dE floor stays at 0.0548,
     still set by the enc–vis pair. */
  --voice-hlf:#264057;      /*  9.35:1 HALF LIGHT    (the shooters)   */
  --voice-inl:#4C2F6B;      /*  9.48:1 INLAND EMPIRE (the dreaming)   */
  --speaker:var(--voice-enc);

  /* ── TYPE ────────────────────────────────────────────── */
  --mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  --serif:'Newsreader','Iowan Old Style','Palatino Linotype',Georgia,serif;
  --t-cap-xs:10px; --lh-cap-xs:16px; --tr-cap-xs:.18em;
  --t-cap:11px;    --lh-cap:16px;    --tr-cap:.16em;
  --t-link:12px;   --lh-link:18px;
  --t-meta:13px;   --lh-meta:20px;
  --t-body:14px;   --lh-body:20px;   /* fixed — do not retune */
  --t-voice:16px;  --lh-voice:26px;
  --t-org:18px;    --lh-org:26px;
  --t-title:20px;  --lh-title:28px;
  --t-word:30px;   --lh-word:30px;

  /* ── SPACE · 8px baseline, no value off this scale ───── */
  --s1:4px; --s2:8px; --s3:12px; --s4:16px; --s5:24px;
  --s6:32px; --s7:40px; --s8:56px; --s9:96px; --s10:120px;
  --rail:176px; --gap:32px; --measure:720px; --band:928px;

  /* ── EDGES · brutalism has no radius ─────────────────── */
  --radius:0;
  --hair:1px solid var(--rule-hair);
  --card-border:2px solid var(--ink);
  --card-shadow:5px 5px 0 var(--shadow);
  --plate-shadow:6px 6px 0 var(--shadow);

  /* ── MOTION ──────────────────────────────────────────── */
  --ease-out:cubic-bezier(.2,0,0,1);
  --ease-in:cubic-bezier(.4,0,1,1);
  --d-snap:90ms;   /* hard-card lift — linear, never eased */
  --d-state:180ms; /* voice raise / lower                  */
  --d-open:220ms;  /* objection unroll                     */
  --d-plate:260ms; /* examine plate                        */
  --d-flip:320ms;  /* refile FLIP                          */

  /* ── DEPTH · four values, nothing else ───────────────── */
  --z-content:0; --z-texture:9990; --z-skip:9999;

  /* ── TEXTURE MASTER DIAL ─────────────────────────────── */
  --texture:1;
}

/* ── DERIVED · all resolve back to a token above, so the hairline and the
      bullets track the column boundary when --rail / --gap change. */
:root{
  --wrap-max:1280px;
  --wrap-pad:clamp(20px,5vw,96px);

  /* rail + gap + measure = 176 + 32 + 720 = 928, the literal token above.
     Re-derived so the masthead rule, the section rules and the colophon band
     stay flush with the grid when --rail and --gap shrink at a breakpoint. */
  --band:calc(var(--rail) + var(--gap) + var(--measure));

  /* .record = 1px border + 31px padding, so 1+31 = the 32px gap and
     the rule lands exactly on the grid column boundary. */
  --record-pad:calc(var(--gap) - 1px);

  --entry-pad-x:16px;   /* reserved hover-card padding */
  --entry-pad-y:12px;
  --entry-bleed:16px;   /* margin-inline, equal and opposite to pad-x */
  --entry-ring:2px;     /* permanently reserved transparent border */
  --bullet:6px;
  --stamp-col:96px;     /* logo column inside a .position row */

  /* .record border-box left edge (the hairline) to the .entry PADDING box left
     edge — the origin for position:absolute:
        record-pad + border(1) − bleed + ring   =  31 + 1 − 16 + 2 = 18px  */
  --rail-inset:calc(var(--record-pad) + 1px - var(--entry-bleed) + var(--entry-ring));
  /* Pull the bullet back so its 6px centre sits on the 1px rule's centre. */
  --bullet-pull:calc(var(--rail-inset) * -1 - (var(--bullet) - 1px) * .5);

  /* One 220px fibre tile, authored once, reused by .grain and every image. */
  --fibre:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='f'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.92' numOctaves='4' stitchTiles='stitch' seed='7'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='0.55'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23f)'/%3E%3C/svg%3E");
  --mottle-tile:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1400' height='1400'%3E%3Cfilter id='m'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.009' numOctaves='2' stitchTiles='stitch' seed='19'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.42 0 0 0 0 0.35 0 0 0 0 0.26 0.9 0 0 0 0'/%3E%3C/filter%3E%3Crect width='1400' height='1400' filter='url(%23m)'/%3E%3C/svg%3E");
}

@property --p{syntax:'<percentage>';inherits:true;initial-value:0%;}

/* ── SPEAKER BINDING · one rule per faculty ────────────── */
[data-speaker="encyclopedia"]{--speaker:var(--voice-enc)}
[data-speaker="visual-calculus"]{--speaker:var(--voice-vis)}
[data-speaker="interfacing"]{--speaker:var(--voice-int)}
[data-speaker="rhetoric"]{--speaker:var(--voice-rhe)}

/* The LAMPLIGHT bench. Encyclopedia and Rhetoric keep their hue in both themes.
   Electrochemistry reuses Interfacing's brass and Shivers Visual Calculus's
   verdigris: those pairs can never co-occur (WORK is display:none under
   Lamplight, GAMES/MUSIC display:none outside it). */
[data-speaker="half-light"]{--speaker:var(--voice-hlf)}
[data-speaker="inland-empire"]{--speaker:var(--voice-inl)}
[data-speaker="electrochemistry"]{--speaker:var(--voice-int)}
[data-speaker="shivers"]{--speaker:var(--voice-vis)}


/* ═══ 3 · BASE TYPE ══════════════════════════════════════════════════════ */

body{
  position:relative;           /* containing block for the fold strips */
  overflow-x:clip;             /* clip, not hidden: no scroll container, so
                                  scroll() timelines and fixed layers survive */
  background:var(--paper);
  color:var(--ink);
  font:400 var(--t-body)/var(--lh-body) var(--mono);
  font-feature-settings:'zero' 1;
  font-optical-sizing:auto;
  hyphens:none;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

/* LINKS · never coloured, because colour means speech. */
a{
  color:var(--ink);
  text-decoration:underline;
  text-decoration-thickness:1px;
  text-underline-offset:3px;
  text-decoration-skip-ink:auto;
}
a:hover,a:focus-visible{text-decoration-thickness:2px}

/* FOCUS · ink at 16.33:1, far above the 3:1 non-text floor. Never a colour. */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible,
.refile input:focus-visible+label{outline:2px solid var(--ink);outline-offset:3px}

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

.skip{
  position:absolute;z-index:var(--z-skip);
  top:var(--s3);left:var(--s3);
  transform:translateY(-250%);
  padding:var(--s2) var(--s3);
  background:var(--bone-card);
  border:var(--card-border);
  box-shadow:var(--card-shadow);
  color:var(--ink);text-decoration:none;
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
}
.skip:focus{transform:none}

.sep{color:var(--ink-muted);margin-inline:.5em;user-select:none}


/* ═══ 4 · LAYOUT ═════════════════════════════════════════════════════════ */

.wrap{max-width:var(--wrap-max);margin-inline:auto;padding-inline:var(--wrap-pad)}

/* Rail 176 + gap 32 + measure 720 = the 928px band. Everything right of it
   stays empty by design. */
.transcript{
  display:grid;
  grid-template-columns:var(--rail) minmax(0,var(--measure));
  column-gap:var(--gap);
  max-width:var(--band);   /* the section rule must stop at the band edge */
}
/* The rail's own strut must carry the speaker's metric, or the 14/20 body
   strut drops the name ~2.7px below the section label across the rule. */
.rail{text-align:right;font:500 var(--t-cap)/var(--lh-cap) var(--mono)}

.record{
  position:relative;
  border-left:var(--hair);
  padding-left:var(--record-pad);
  min-width:0;
}

.section{margin-top:var(--s9);scroll-margin-top:var(--s9)}
.section--overture{margin-top:var(--s10)}

/* One hairline across the band opens each section; the vertical rule meets
   it at a T. */
.section:not(.section--overture) .transcript{border-top:var(--hair)}
.section:not(.section--overture) .rail,
.section:not(.section--overture) .record{padding-top:var(--s3)}


/* ═══ 5 · COMPONENTS ═════════════════════════════════════════════════════ */

/* ── MASTHEAD ─────────────────────────────────────────── */
.masthead__band{
  position:relative;
  display:flex;flex-wrap:wrap;align-items:baseline;
  column-gap:var(--s7);row-gap:var(--s3);
  padding-block:var(--s8) var(--s3);
}
/* The closing hairline stops at the 928 band, not at the container edge. */
.masthead__band::after{
  content:"";position:absolute;bottom:0;left:var(--wrap-pad);
  width:min(100% - 2 * var(--wrap-pad),var(--band));
  border-bottom:var(--hair);
}

.wordmark{
  font:400 var(--t-word)/var(--lh-word) var(--serif);
  font-variation-settings:'opsz' 32;
  letter-spacing:-.01em;text-wrap:balance;
  color:var(--ink);
}

.masthead__nav{display:flex;flex-wrap:wrap;gap:var(--s5)}
.masthead__nav a{font:400 var(--t-link)/var(--lh-link) var(--mono)}

/* CROSS-EXAMINE raises one voice, PLAIN VIEW removes them all. Only the
   rightmost control carries the band-edge margin. */
.crossexam{
  margin-left:auto;
  padding:6px 0;
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--voice-rhe);white-space:nowrap;
}
.crossexam:hover{text-decoration:underline;text-decoration-style:dotted;text-underline-offset:4px}
.crossexam[aria-pressed="true"]{text-decoration:underline;text-decoration-style:solid;text-underline-offset:4px}
.crossexam+.plainview,.lamplight+.plainview{margin-left:0}

/* Only exists once the maze has been solved; JS unsets [hidden]. */
.lamplight{
  padding:6px 0;
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);white-space:nowrap;
}
.lamplight:hover{text-decoration:underline;text-decoration-style:dotted;text-underline-offset:4px}
.lamplight[aria-pressed="true"]{color:var(--ink);text-decoration:underline;text-underline-offset:4px}
.lamplight[hidden]{display:none}

.plainview{
  margin-left:auto;
  margin-right:max(0px,calc(100% - var(--band)));  /* land on the band edge */
  padding:6px 0 6px var(--s6);
  border-left:var(--hair);
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink);white-space:nowrap;
}
.plainview__box{
  display:inline-block;width:9px;height:9px;
  margin-right:var(--s2);
  border:1px solid var(--ink);     /* border, not inset shadow: survives forced-colors */
  background:transparent;
  transition:background-color var(--d-state) var(--ease-out);
}
.plainview[aria-pressed="true"] .plainview__box,
html[data-voice="off"] .plainview__box{background:var(--ink)}

/* ── SECTION LABEL ────────────────────────────────────── */
.section__label{
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  text-wrap:balance;color:var(--ink);
  margin-bottom:var(--s3);
}

/* Fragment aliases for the old #about / #Experience / #publications URLs:
   zero-size, out of the a11y tree, still a valid scroll target. */
.anchor-alias{display:block;height:0;scroll-margin-top:96px}

/* ── VOICE LINE ───────────────────────────────────────── */
.voice{
  margin-top:10px;
  color:var(--speaker);           /* the name and the dash, by inheritance */
  text-wrap:pretty;
}
.voice__who{
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  vertical-align:baseline;
}
b.voice__who--static{cursor:default}
/* Dotted on hover, solid when pressed. button: only — RHETORIC's <b> speaker
   is not a control. */
button.voice__who:hover,.rail__speaker:hover{
  text-decoration:underline;text-decoration-style:dotted;
  text-decoration-thickness:1px;text-underline-offset:4px;
}
/* Same specificity as the :hover rule and declared after it, so hovering an
   already-raised name keeps the solid mark instead of reverting to dotted. */
button.voice__who[aria-pressed="true"],
button.voice__who[aria-pressed="true"]:hover,
.rail__speaker[aria-pressed="true"],
.rail__speaker[aria-pressed="true"]:hover{
  text-decoration:underline;text-decoration-style:solid;
  text-decoration-thickness:1px;text-underline-offset:4px;
}
.voice__dash{margin-inline:.42em;user-select:none}
/* Without color-mix the declaration drops and the prose inherits
   var(--speaker) from .voice. */
.voice__prose{
  font:italic 400 var(--t-voice)/var(--lh-voice) var(--serif);
  color:color-mix(in oklab,var(--speaker) 82%,var(--ink));
}

/* Section leads are SPLIT: name in the rail, em-dash and prose in the record.
   Flex hangs the dash. */
.voice--lead{
  display:flex;align-items:baseline;gap:.42em;
  margin-top:0;margin-bottom:var(--s5);
}
.voice--lead .voice__who{flex:none}
.voice--lead .voice__dash{flex:none;margin-inline:0}
.voice--lead .voice__prose{flex:1 1 auto;min-width:0}
/* The overture lead runs at the wordmark's 30px, not the 16px the other leads
   use. One-off: WORK, RECORD and CONTACT stay at 16/26. Its speaker name is
   inline at every width — the rail here belongs to the portrait. */
.section--overture .voice--lead{
  display:block;margin-bottom:var(--s5);
  text-indent:0;
}
.section--overture .voice--lead>.voice__who{display:inline}
.section--overture .voice--lead .voice__dash{margin-inline:.3em}
.section--overture .voice--lead .voice__prose{
  font:italic 400 var(--t-word)/38px var(--serif);
  font-variation-settings:'opsz' 32;
  letter-spacing:-.005em;
}

/* WORK / RECORD / CONTACT leads carry BOTH speaker names in the DOM and show
   exactly one — no second tab stop, no bare em dash. ONE max-width:999px
   block flips both halves; two queries leave a fractional band (999.2px at
   125% zoom) where neither matches and the name prints twice. */
.voice--lead>.voice__who{display:none}

.rail__speaker{
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  color:var(--speaker);
  transition:color var(--d-state) var(--ease-out);
}
.rail__speaker[aria-pressed="true"]{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:4px;
}

/* FACULTY FOCUS · one voice raised, the rest dimmed. De-emphasis removes
   COLOUR (--ink-muted, 5.82:1), never contrast. */
main[data-focus] .voice,
main[data-focus] .rail__speaker{color:var(--ink-muted);transition:color var(--d-state) var(--ease-out)}
main[data-focus] .voice .voice__prose{color:var(--ink-muted);transition:color var(--d-state) var(--ease-out)}

/* THE RESTORE LIST · one line per faculty; every faculty must appear. CSS
   cannot compare two attribute values, so a faculty missing here is INERT,
   not merely unstyled: the dim rule greys the page and the "raised" voice
   stays grey with it. Adding one means a line here, in .voice__prose below,
   and in the forced-colors copy in §11.
     encyclopedia · visual-calculus · interfacing · rhetoric   — the CV bench
     half-light · inland-empire · electrochemistry · shivers   — the Lamplight bench
   :is() keeps both arms at 0,1,0, so one line per faculty, not two. */
main[data-focus="encyclopedia"]    :is(.voice,.rail__speaker)[data-speaker="encyclopedia"],
main[data-focus="visual-calculus"] :is(.voice,.rail__speaker)[data-speaker="visual-calculus"],
main[data-focus="interfacing"]     :is(.voice,.rail__speaker)[data-speaker="interfacing"],
main[data-focus="rhetoric"]        :is(.voice,.rail__speaker)[data-speaker="rhetoric"],
main[data-focus="half-light"]      :is(.voice,.rail__speaker)[data-speaker="half-light"],
main[data-focus="inland-empire"]   :is(.voice,.rail__speaker)[data-speaker="inland-empire"],
main[data-focus="electrochemistry"] :is(.voice,.rail__speaker)[data-speaker="electrochemistry"],
main[data-focus="shivers"]         :is(.voice,.rail__speaker)[data-speaker="shivers"]{color:var(--speaker)}

main[data-focus="encyclopedia"]    .voice[data-speaker="encyclopedia"]    .voice__prose,
main[data-focus="visual-calculus"] .voice[data-speaker="visual-calculus"] .voice__prose,
main[data-focus="interfacing"]     .voice[data-speaker="interfacing"]     .voice__prose,
main[data-focus="rhetoric"]        .voice[data-speaker="rhetoric"]        .voice__prose,
main[data-focus="half-light"]      .voice[data-speaker="half-light"]      .voice__prose,
main[data-focus="inland-empire"]   .voice[data-speaker="inland-empire"]   .voice__prose,
main[data-focus="electrochemistry"] .voice[data-speaker="electrochemistry"] .voice__prose,
main[data-focus="shivers"]         .voice[data-speaker="shivers"]         .voice__prose{
  color:color-mix(in oklab,var(--speaker) 82%,var(--ink));
}

/* ── ENTRY · the one card on the page ─────────────────── */
/* Rest state RESERVES the hover geometry: 12/16 padding, equal-and-opposite
   -16 inline margin, 2px transparent border. Hover only swaps colour and adds
   transform + shadow, so no layout moves. */
.entry{
  position:relative;
  padding:var(--entry-pad-y) var(--entry-pad-x);
  margin-inline:calc(var(--entry-bleed) * -1);
  border:var(--entry-ring) solid transparent;
  background:transparent;
}
.entry:hover,.entry:focus-within,.entry:focus-visible{
  background:var(--bone-card);
  border-color:var(--ink);
  box-shadow:var(--card-shadow);
  transform:translate(-2px,-2px);
  transition:transform var(--d-snap) linear,
             box-shadow var(--d-snap) linear,
             background-color var(--d-snap) linear,
             border-color var(--d-snap) linear;
}

.entries>li+li{margin-top:var(--s7)}
.positions>li+li{margin-top:var(--s6)}

/* Square bullet, straddling the rule so the rule threads through it. */
.entry__bullet{
  position:absolute;
  left:var(--bullet-pull);
  top:calc(var(--entry-pad-y) + 11px);
  width:var(--bullet);height:var(--bullet);
  background:var(--ink);      /* default is TICKED; §7 animates it in */
}
.position .entry__bullet{top:calc(var(--entry-pad-y) + 9px)}

.entry__head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s5)}

.entry__title{
  font:500 var(--t-title)/var(--lh-title) var(--serif);
  font-variation-settings:'opsz' 24;
  letter-spacing:-.005em;text-wrap:pretty;color:var(--ink);
}

.entry__authors{
  font:400 var(--t-meta)/var(--lh-meta) var(--mono);
  color:var(--ink-muted);
  margin-top:var(--s2);
}
.entry__authors b{font-weight:600;color:var(--ink)}
.entry__authors--long{overflow-wrap:anywhere}

/* Lowercase is never tracked: a sentence, not a label. */
.footnote{
  font:400 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:0;word-spacing:normal;
  color:var(--ink-muted);
  margin-top:var(--s5);padding-top:var(--s3);
  margin-left:calc(var(--record-pad) * -1);padding-left:var(--record-pad);
  border-top:var(--hair);
}

/* ── VENUE STAMP · solid = refereed, dashed = preprint. The words carry it
      independently of the border; hue is never the channel. ──────────── */
.stamp-venue{
  flex:none;
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  font-variant-numeric:tabular-nums slashed-zero;
  padding:2px 6px;white-space:nowrap;
}
.stamp-venue--refereed{border:1px solid var(--ink);color:var(--ink)}
.stamp-venue--preprint{border:1px dashed var(--rule-strong);color:var(--ink-muted)}

.stamp-legend{
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);
  margin:var(--s3) 0 var(--s7);
}
.stamp-legend .stamp-venue{
  font-size:var(--t-cap-xs);letter-spacing:var(--tr-cap-xs);
  padding:1px 5px;margin-right:var(--s1);
}

/* ── REFILE · the one --paper-inset block above the colophon ──────────── */
.refile{
  position:relative;
  display:flex;flex-wrap:wrap;align-items:center;
  column-gap:var(--s4);row-gap:var(--s2);
  background:var(--paper-inset);
  padding:20px;
  margin-block:var(--s5) 0;
}
.refile__label{
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);
}
.refile label{
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);cursor:pointer;
  padding-block:6px;white-space:nowrap;
}
.refile label::before{content:"\25AF";margin-right:.5em;letter-spacing:0}
.refile input:checked+label{color:var(--ink)}
.refile input:checked+label::before{content:"\25AE"}
/* Repeat with the alt-text form to keep the glyph OUT of the accessible name.
   Declared second so engines without `content/alt` keep the plain glyph. */
.refile label::before{content:"\25AF" / ""}
.refile input:checked+label::before{content:"\25AE" / ""}
/* Zeroed IN FLOW, not hidden with .sr-only's absolute positioning:
   absolutely-positioned radios all sit at the strip's left edge, so
   arrow-keying to the last one scrolls the strip to the wrong place. */
/* 1px, not 0: AT filters out zero-sized controls much as it does
   display:none. 1px keeps a layout box, so focus scrolls to the right chip. */
.refile input[type="radio"]{
  position:static;width:1px;height:1px;padding:0;border:0;
  opacity:0;appearance:none;-webkit-appearance:none;
  clip:auto;clip-path:none;overflow:visible;
  /* In flow it is a flex item and buys a second column-gap, pushing
     [ examine all ] onto a row of its own. Cancel the gap to its label. */
  margin:0 calc(var(--s4) * -1 - 1px) 0 0;
}
.refile__all{margin-left:auto;flex:none;white-space:nowrap}

/* JS writes these as <li class="divider"> when an ordering needs them. */
.divider{
  display:flex;align-items:center;gap:var(--s3);
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);
  margin-top:var(--s7);
}
.divider::before,.divider::after{content:"";height:1px;background:var(--rule-hair)}
.divider::before{flex:0 0 var(--s4)}
.divider::after{flex:1 1 auto}

/* ── UTILITY BUTTON · the brackets are literal characters, never borders ── */
.btn{
  font:400 var(--t-link)/var(--lh-link) var(--mono);
  padding:4px 0;color:var(--ink);
  vertical-align:baseline;
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:3px;
}
.btn:hover,.btn:focus-visible{text-decoration-thickness:2px}

/* Inline flow, not flex: as flex items the middots are independent boxes and
   orphan onto the next line when a row wraps. In inline flow the markup's own
   whitespace controls breaking — no space before a middot, one after it. */
.linkrow{
  font:400 var(--t-link)/calc(var(--lh-link) + 6px) var(--mono);
  margin-top:var(--s3);
}
/* .6em is IBM Plex Mono's advance width, matching the collapsed word space
   after the middot — symmetric without a break opportunity on its left. */
.linkrow .sep{margin-inline:.6em 0;color:var(--ink-muted)}
.linkrow--primary{margin-top:var(--s4)}

/* ── OVERTURE ─────────────────────────────────────────── */
.bio{font:400 var(--t-body)/var(--lh-body) var(--mono);color:var(--ink)}
.edu{
  font:400 var(--t-meta)/var(--lh-meta) var(--mono);
  color:var(--ink-muted);margin-top:var(--s3);
}

/* THE PORTRAIT · studio headshot on white. multiply dissolves the white ground
   into the parchment; the wrapper is isolated AND carries --paper so the blend
   has the right backdrop and cannot escape to the body. */
.portrait{
  width:168px;height:208px;
  margin:0 0 var(--s3) auto;
  background:var(--paper);
  border:var(--card-border);
  box-shadow:var(--plate-shadow);
  isolation:isolate;
  overflow:hidden;
  transform:rotate(var(--tilt,-.4deg));   /* JS writes --tilt; static, so
                                             reduced-motion does not apply */
}
/* Always full colour. multiply still dissolves the white studio ground and
   costs the skin tones under 6% mean luminance on a ground this light. */
.portrait img{
  width:100%;height:100%;
  object-fit:cover;object-position:50% 30%;
  mix-blend-mode:multiply;
}

/* ── AFFIXED STAMPS · the four logos, in the record column ────────────── */
/* Wide lockups on white, not square marks — hence a 96px column to the left
   of the organisation name. */
.position{
  display:grid;
  grid-template-columns:var(--stamp-col) minmax(0,1fr);
  column-gap:var(--s4);
  align-items:start;
  background:var(--paper);   /* the multiply backdrop for the logo */
  isolation:isolate;
}
.position>.stamp{
  grid-column:1;grid-row:1/span 3;
  width:100%;height:auto;max-height:48px;
  object-fit:contain;object-position:left top;
  margin-top:var(--s1);
  filter:grayscale(1) contrast(1.05);
  mix-blend-mode:multiply;
  opacity:.82;
  transform:rotate(var(--tilt,0deg));
  transition:opacity 240ms var(--ease-out),
             filter 240ms var(--ease-out),
             transform 240ms var(--ease-out);
}
.entry:hover .stamp,.entry:focus-within .stamp{opacity:1;filter:none;transform:none}
.position>.entry__head{grid-column:2;grid-row:1}
.position__role{grid-column:2;grid-row:2}
.position__place{grid-column:2;grid-row:3}

.position__org{
  font:500 var(--t-org)/var(--lh-org) var(--serif);
  letter-spacing:-.005em;color:var(--ink);
}
.position__dates{
  flex:none;
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  font-variant-numeric:tabular-nums slashed-zero;
  color:var(--ink-muted);white-space:nowrap;
}
.position__role{font:400 var(--t-body)/var(--lh-body) var(--mono);color:var(--ink);margin-top:var(--s1)}
.position__place{font:400 var(--t-meta)/var(--lh-meta) var(--mono);color:var(--ink-muted)}

/* ── PHOTOSTAT STRIP AND EXAMINED PLATE ───────────────── */
.plate{position:relative;isolation:isolate;margin-top:var(--s3)}

/* OPEN is the default everywhere: no-JS readers keep the figure, and every
   plate ships with .is-examined in the markup so the full set is visible on
   arrival. Setting that in HTML rather than from JS avoids a flash of closed
   slivers before the deferred script runs. CONTAIN on a mat: the teasers run
   1.68:1 to 3.41:1 and cover would slice the widest. */
.plate__img,
html.js .plate.is-examined .plate__img{
  width:100%;height:auto;max-height:340px;
  object-fit:contain;object-position:50% 50%;
  background:var(--paper-inset);
  padding:10px;
  border:var(--card-border);
  box-shadow:var(--plate-shadow);
  filter:grayscale(0) saturate(.86) contrast(1.04);
  transition:filter var(--d-plate) var(--ease-out);
}
/* CLOSED is a 56px sliver — a sample, not the information, so cover is right
   here. Applied only when JS can reopen it, and now only after a reader
   closes one: plates start with .is-examined. */
html.js .plate__img{
  height:56px;max-height:none;
  object-fit:cover;
  padding:0;background:none;
  border:0;border-top:var(--hair);
  box-shadow:none;
  filter:grayscale(1) contrast(1.06) sepia(.22);
}

/* ── RHETORIC OBJECTION ───────────────────────────────── */
.objection{margin-top:var(--s2)}
.objection__toggle{
  display:inline-flex;align-items:baseline;gap:.5em;
  list-style:none;
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--voice-rhe);cursor:pointer;
  padding-block:var(--s1);
}
.objection__toggle::marker{content:""}
.objection__toggle::before{
  content:"\25B8";letter-spacing:0;
  transition:transform var(--d-open) var(--ease-out);
}
.objection__toggle::before{content:"\25B8" / ""}   /* keep the glyph out of the accessible name */
.objection[open] .objection__toggle::before{transform:rotate(90deg)}
/* 0fr → 1fr on an overflow:hidden wrapper: genuinely animatable,
   no max-height guessing, no layout jump. */
.objection__wrap{
  display:grid;grid-template-rows:0fr;
  transition:grid-template-rows var(--d-open) var(--ease-out);
}
.objection__wrap>*{min-height:0;overflow:hidden}
.objection[open] .objection__wrap{grid-template-rows:1fr}
.objection .voice{margin-top:var(--s1)}

/* TALKED OVER · the primary voice loses its colour, never its contrast.
   .is-talked-over is the JS fallback for engines without :has(). */
.entry.is-talked-over>.voice{color:var(--ink-muted);transition:color var(--d-state) var(--ease-out)}
.entry.is-talked-over>.voice .voice__prose{color:var(--ink-muted)}

/* ── THE INK TRAIL ────────────────────────────────────────────────────────
      Fixed canvas under the grain, so the fibre sits on the ink as it does on
      every other mark. JS creates it: never without a fine pointer, never
      under reduced motion. */
.trail{
  position:fixed;inset:0;
  width:100%;height:100%;    /* a <canvas> is REPLACED: without an explicit
                                size, `inset:0` does not stretch it and size
                                containment would resolve it to 0x0 */
  z-index:calc(var(--z-texture) - 5);
  pointer-events:none;
  contain:layout style paint;   /* deliberately NOT `strict` — no size containment */
}

/* ── THE WAY THROUGH · same hairline and ink square as the transcript ──── */
.lock{display:none}                      /* JS-only: never an empty box */
html.js .lock{display:block}
html.no-js .lock,
html[data-voice="off"] .lock{display:none}

.lock__band{padding-block:var(--s9) var(--s10)}
.lock__label{
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);margin-bottom:var(--s4);
}
.maze{
  width:100%;max-width:462px;
  padding:var(--s4);
  background:var(--paper-inset);
  border:var(--card-border);
  box-shadow:var(--card-shadow);
  cursor:crosshair;touch-action:none;   /* drag is the pointer control */
}
.maze:focus-visible{outline:2px solid var(--ink);outline-offset:3px}
.maze svg{display:block;width:100%;height:auto}
/* vector-effect keeps every wall a true 1px hairline at any rendered scale */
.maze .wall{stroke:var(--ink);stroke-width:1;vector-effect:non-scaling-stroke;stroke-linecap:square}
.maze .edge{stroke:var(--rule-strong);stroke-width:1;vector-effect:non-scaling-stroke}
.maze .goal{fill:none;stroke:var(--ink);stroke-width:1;vector-effect:non-scaling-stroke;stroke-dasharray:2 2}
.maze .token{
  fill:var(--ink);
  transition:transform 110ms var(--ease-out);
}
.maze .breadcrumb{fill:var(--rule-strong)}
.maze.is-solved{border-color:var(--voice-vis)}
.maze.is-solved .token{fill:var(--voice-vis)}

.lock__help{
  font:400 var(--t-link)/var(--lh-link) var(--mono);
  color:var(--ink-muted);margin-top:var(--s3);
}
.lock__won{margin-top:var(--s4)}
.lock__won[hidden]{display:none}

/* A real tab stop, so it must become visible on focus or keyboard users tab
   into nothing. Same reveal-on-focus pattern as the skip link. */
#lock-bypass:focus-visible{
  position:static;width:auto;height:auto;margin:var(--s3) 0 0;
  clip:auto;clip-path:none;overflow:visible;white-space:normal;
  display:inline-block;
  font:400 var(--t-link)/var(--lh-link) var(--mono);
  color:var(--ink);background:var(--bone-card);
  padding:8px 12px;border:var(--card-border);box-shadow:var(--card-shadow);
  text-decoration:underline;text-underline-offset:3px;
  outline:2px solid var(--ink);outline-offset:3px;
}

@media (prefers-reduced-motion:reduce){
  .maze .token{transition:none}
}

/* Clipboard-blocked fallback: the citation, on the page, selectable. */
.cite-out{
  font:400 var(--t-meta)/var(--lh-meta) var(--mono);
  color:var(--ink);background:var(--paper-inset);
  margin-top:var(--s2);padding:var(--s3);
  white-space:pre-wrap;overflow-x:auto;
  user-select:all;-webkit-user-select:all;
}
.cite-out:focus-visible{outline:2px solid var(--ink);outline-offset:3px}

/* ── CONTACT ──────────────────────────────────────────── */
.contact{
  display:grid;grid-template-columns:96px minmax(0,1fr);
  column-gap:var(--s4);row-gap:var(--s2);
  align-items:baseline;
}
.contact dt{
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);
}
.contact dd{font:400 var(--t-body)/var(--lh-body) var(--mono);color:var(--ink)}
.contact dd .btn{margin-left:var(--s2)}

/* ── COLOPHON ─────────────────────────────────────────── */
.colophon{margin-top:var(--s9)}
.colophon__band{
  position:relative;
  display:flex;flex-wrap:wrap;align-items:flex-start;
  justify-content:space-between;
  column-gap:var(--s6);row-gap:var(--s5);
  padding-block:var(--s5);
}
/* The inset band stops at the 928 boundary. */
.colophon__band::before{
  content:"";position:absolute;top:0;bottom:0;left:var(--wrap-pad);
  width:min(100% - 2 * var(--wrap-pad),var(--band));
  background:var(--paper-inset);
  border-top:var(--hair);
  pointer-events:none;
}
.colophon__text,.colophon__widget{position:relative}
.colophon__text{flex:1 1 320px;min-width:0}
.colophon__text p+p{margin-top:var(--s1)}
.colophon__rights{font:400 var(--t-meta)/var(--lh-meta) var(--mono);color:var(--ink)}

/* The printer's device shows only under Lamplight. */
.seal{display:none}
html[data-theme="disco"] .seal{
  display:block;width:44px;height:auto;
  margin:0 0 var(--s3);
  opacity:.92;
}
/* PLAIN VIEW strips it and it never prints. Both need theme specificity to
   beat the rule above. */
html[data-theme="disco"][data-voice="off"] .seal,
html[data-theme="disco"].no-js .seal{display:none}
@media print{
  html[data-theme="disco"] .seal{display:none!important}
}

/* Quarantined third-party widget: fixed box, height reserved so the async
   script cannot shift a pixel. */
.colophon__widget{
  flex:none;max-width:180px;
  margin-right:max(0px,calc(100% - var(--band)));
}
.colophon__plate{
  display:block;margin-bottom:var(--s2);
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);
}
.widget{
  width:180px;height:112px;overflow:hidden;contain:strict;
  filter:grayscale(1) contrast(.95);opacity:.55;
}
.widget img,.widget iframe,.widget canvas{max-width:100%;height:auto}


/* ═══ 6 · TEXTURE ════════════════════════════════════════════════════════ */
/* Five layers, all pointer-events:none, none repainting on scroll, every
   opacity scaled by the --texture dial. No image downloaded. */

.grain{
  position:fixed;inset:0;z-index:var(--z-texture);
  pointer-events:none;contain:strict;
  opacity:calc(.045 * var(--texture));
  mix-blend-mode:multiply;
  background-image:var(--fibre);
  background-size:220px 220px;
}

.mottle{
  position:fixed;inset:0;z-index:calc(var(--z-texture) - 1);
  pointer-events:none;contain:strict;
}
.mottle::before{
  content:"";position:absolute;inset:0;
  background-image:var(--mottle-tile);
  background-size:1400px 1400px;
  mix-blend-mode:multiply;
  opacity:calc(.035 * var(--texture));
}
/* Teal-green vignette, not black. */
.mottle::after{
  content:"";position:absolute;inset:0;
  background:radial-gradient(120% 92% at 50% 38%,transparent 52%,rgba(56,66,63,.11) 100%);
  mix-blend-mode:multiply;
  opacity:var(--texture);
}

/* Two creases, scrolling with the sheet: a dark stop immediately followed by
   a light stop. */
.fold{
  position:absolute;left:0;right:0;height:3px;
  z-index:calc(var(--z-texture) - 2);
  pointer-events:none;
  opacity:var(--texture);
  background:linear-gradient(180deg,transparent,rgba(0,0,0,.055) 49%,rgba(255,255,255,.55) 51%,transparent);
}
.fold--a{top:34%}
.fold--b{top:68%}

/* The same fibre tile runs across the page/picture boundary. */
.plate::after,.portrait::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--fibre);
  background-size:220px 220px;
  mix-blend-mode:multiply;
  opacity:calc(.06 * var(--texture));
}


/* ═══ 7 · @SUPPORTS ══════════════════════════════════════════════════════ */

/* TALKED-OVER · :has() drives it; .is-talked-over above is the fallback.
   The objection's own voice is nested deeper, so it keeps its colour. */
@supports selector(:has(*)){
  .entry:has(>.objection[open])>.voice{color:var(--ink-muted);transition:color var(--d-state) var(--ease-out)}
  .entry:has(>.objection[open])>.voice .voice__prose{color:var(--ink-muted)}
}

/* THE RULE INKS IN / THE BULLETS TICK · scroll timelines, no JS. Without
   animation-timeline the rule is a uniform hairline and bullets stay solid. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    /* Longhands only: the `animation` shorthand would reset duration to 0s
       and wipe animation-timeline, and both are load-bearing here. */
    .record::before{
      content:"";position:absolute;left:-1px;top:0;bottom:0;width:1px;
      background:linear-gradient(180deg,var(--rule-strong) 0 var(--p),var(--rule-hair) var(--p) 100%);
      animation-name:ink;
      animation-timing-function:linear;
      animation-fill-mode:both;
      animation-timeline:scroll(root);
    }
    .entry__bullet{
      background:var(--paper);
      outline:1px solid var(--ink);outline-offset:-1px;
      animation-name:tick;
      animation-timing-function:linear;
      animation-fill-mode:both;
      animation-timeline:view(block);
      animation-range:entry 60% entry 100%;
    }
  }
}
@keyframes ink{from{--p:0%}to{--p:100%}}
@keyframes tick{from{background-color:var(--paper)}to{background-color:var(--ink)}}

/* SCUMBLED CARD EDGE · border-only ::before, four seeds cycled. Text and
   images are never inside the filter. */
@supports (filter:url(#wobble0)){
  @media (min-width:900px) and (hover:hover){
    .entry::before{
      content:"";position:absolute;inset:-1px;
      border:var(--card-border);
      pointer-events:none;opacity:0;
      transition:opacity var(--d-snap) linear;
    }
    .entry:hover,.entry:focus-within{border-color:transparent}
    .entry:hover::before,.entry:focus-within::before{opacity:1;filter:url(#wobble0)}
    li:nth-child(4n+2)>.entry:hover::before,li:nth-child(4n+2)>.entry:focus-within::before{filter:url(#wobble1)}
    li:nth-child(4n+3)>.entry:hover::before,li:nth-child(4n+3)>.entry:focus-within::before{filter:url(#wobble2)}
    li:nth-child(4n+4)>.entry:hover::before,li:nth-child(4n+4)>.entry:focus-within::before{filter:url(#wobble3)}
  }
}

/* Without multiply, five bright rectangles punch holes in the parchment.
   Outline, not border, so the fallback shifts nothing. */
@supports not (mix-blend-mode:multiply){
  .portrait img,.position>.stamp{
    mix-blend-mode:normal;
    background:var(--bone-card);
    outline:1px solid var(--rule-hair);outline-offset:-1px;
  }
  .grain,.mottle::before,.mottle::after,.plate::after,.portrait::after{display:none}
}


/* ═══ 8 · BREAKPOINTS ════════════════════════════════════════════════════ */

/* 1000–1199px · the portrait must shrink with the rail: at 168px in a 140px
   column it overhangs the gutter and its 6px offset shadow lands on the
   hairline. */
@media (max-width:1199px){
  :root{--rail:140px;--gap:28px}
  .portrait{width:132px;height:164px}
}

/* 760–999px · the grid collapses and the speaker name becomes an inline
   lead-in on the voice line itself. No DOM is moved. */
@media (max-width:999px){
  :root{--record-pad:17px}
  .transcript{grid-template-columns:minmax(0,1fr);max-width:var(--measure)}
  .rail{text-align:left}
  /* Both halves of the speaker swap flip together, in this one query. */
  .rail__speaker{display:none}
  .voice--lead>.voice__who{display:inline}
  .section--overture .voice--lead .voice__prose{font-size:24px;line-height:32px}
  .section:not(.section--overture) .rail{padding-top:0}
  .portrait{width:148px;height:184px;margin:0 0 var(--s5) 0}
  .voice--lead{margin-bottom:var(--s4)}
  .masthead__band::after,.colophon__band::before{width:calc(100% - 2 * var(--wrap-pad))}
  .plainview,.colophon__widget{margin-right:0}
  .position{--stamp-col:80px}
}

/* ≤759px · one column. */
@media (max-width:759px){
  :root{--t-voice:15px;--lh-voice:24px;--t-title:18px;--lh-title:26px;--t-word:26px;--lh-word:26px}
  /* Wordmark takes the first line, four short words share the second. No
     `order` anywhere — tab order must equal visual order. */
  .masthead__band{padding-top:var(--s7);column-gap:var(--s5);row-gap:var(--s4)}
  .wordmark{flex:1 1 100%}
  .plainview{padding-left:var(--s5)}
  .portrait{width:120px;height:148px}
  .entry__head{flex-wrap:wrap;gap:var(--s2)}
  .contact{grid-template-columns:minmax(0,1fr);row-gap:0}
  .contact dd{margin-bottom:var(--s3)}
  /* REFILE becomes a scrollable chip row — same radios, same handler. */
  .refile{
    flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-x:contain;
    scroll-snap-type:x proximity;padding:var(--s3) var(--s4);
  }
  .refile label{scroll-snap-align:start}
  .refile__all{margin-left:var(--s3)}
  /* Logos move above the organisation name. */
  .position{grid-template-columns:minmax(0,1fr)}
  .position>.stamp{grid-column:1;grid-row:1;width:88px;margin:0 0 var(--s2)}
  .position>.entry__head{grid-column:1;grid-row:2}
  .position__role{grid-column:1;grid-row:3}
  .position__place{grid-column:1;grid-row:4}
  .mottle{display:none}
  .grain{opacity:calc(.032 * var(--texture))}
}

/* No tap-hunting for a figure on a phone: every plate renders open. */
@media (hover:none){
  html.js .plate__img{
    height:auto;max-height:340px;object-fit:contain;
    padding:10px;background:var(--paper-inset);
    border:var(--card-border);box-shadow:var(--plate-shadow);
    filter:grayscale(0) saturate(.86) contrast(1.04);
  }
  /* The examine controls are now inert, so remove them — and the middot that
     would be left hanging in front of each one. */
  [data-examine],#examine-all{display:none}
  .linkrow .sep:has(+ [data-examine]){display:none}
}

/* ≥44px tap targets via padding, which does not change the visual size. */
@media (pointer:coarse){
  .btn{padding-block:13px}
  .linkrow a,.masthead__nav a,.contact dd a{padding-block:13px}
  .objection__toggle{padding-block:14px}
  .refile label{padding-block:14px}
  .plainview,.crossexam{padding-block:14px}
  .voice__who,.rail__speaker{padding-block:8px}
  /* .linkrow is inline flow, and vertical padding on an INLINE box does not
     grow the line box — two wrapped rows overlap by ~18px and steal each
     other's hits. Use leading instead. */
  .linkrow{line-height:44px}
}


/* ═══ 9 · PLAIN VIEW ═════════════════════════════════════════════════════ */
/* One state serving three paths: the toggle, the no-JS rendering and print.
   No information is lost; the facts were never inside the voices. */

html[data-voice="off"] .voice,
html.no-js .voice,
html[data-voice="off"] .rail__speaker,
html.no-js .rail__speaker,
html[data-voice="off"] .objection,
html.no-js .objection,
html[data-voice="off"] .refile,
html.no-js .refile,
html[data-voice="off"] .crossexam,
html[data-voice="off"] .entry__bullet,
html.no-js .entry__bullet{display:none}

html[data-voice="off"],
html.no-js{--record-pad:0px}

html[data-voice="off"] .transcript,
html.no-js .transcript{grid-template-columns:minmax(0,var(--measure));max-width:var(--measure)}

html[data-voice="off"] .record,
html.no-js .record{border-left:0;padding-left:0}

html[data-voice="off"] .rail,
html.no-js .rail{text-align:left}

html[data-voice="off"] .portrait,
html.no-js .portrait{margin:0 0 var(--s5) 0}

html[data-voice="off"] .masthead__band::after,
html.no-js .masthead__band::after,
html[data-voice="off"] .colophon__band::before,
html.no-js .colophon__band::before{width:min(100% - 2 * var(--wrap-pad),var(--measure))}

html[data-voice="off"] .plainview,
html.no-js .plainview,
html[data-voice="off"] .colophon__widget,
html.no-js .colophon__widget{margin-right:max(0px,calc(100% - var(--measure)))}

html[data-voice="off"] .stamp-legend{margin-bottom:var(--s5)}

/* With the speaker names gone the rail is empty outside the overture, leaving
   an empty 12px block above each section. :has() removes it; the padding reset
   is the fallback. */
html[data-voice="off"] .section:not(.section--overture) .rail,
html.no-js .section:not(.section--overture) .rail{padding-top:0}
@supports selector(:has(*)){
  html[data-voice="off"] .rail:not(:has(.portrait)),
  html.no-js .rail:not(:has(.portrait)){display:none}
}

/* Without JS the controls are inert, so they are not offered — and the
   middots that introduced them go too, or every link row ends "Code· ·". */
html.no-js .btn,
html.no-js .crossexam,
html.no-js .plainview{display:none}
html.no-js .linkrow .sep:has(+ .btn){display:none}


/* ═══ 10 · PRINT ═════════════════════════════════════════════════════════ */

@media print{
  @page{margin:18mm}

  :root{
    --paper:#fff;--paper-inset:#fff;--bone-card:#fff;
    --ink:#000;--ink-muted:#333;
    --rule-hair:#999;--rule-strong:#666;
    --shadow:transparent;
    --texture:0;
    --record-pad:0px;
  }

  body{background:#fff;color:#000}
  /* .btn too: [ examine ] / [ cite ] / [ copy ] do nothing on paper. */
  .grain,.mottle,.fold,.skip,.refile,.colophon__widget,.trail,.lock,
  .voice,.rail__speaker,.objection,.entry__bullet,.plainview,.crossexam,.lamplight,
  .masthead__nav,.plate::after,.portrait::after,.btn,.cite-out,
  .record::before,.entry::before{display:none!important}
  /* The middot left in front of a removed [ examine ] / [ cite ] goes with it. */
  .linkrow .sep:has(+ .btn){display:none!important}

  .transcript{grid-template-columns:minmax(0,1fr)}
  .record{border-left:0;padding-left:0}
  .rail{text-align:left}
  .section:not(.section--overture) .rail{padding-top:0}
  .section{margin-top:var(--s7)}
  .section--overture{margin-top:var(--s5)}
  .masthead__band{padding-block:0 var(--s3)}
  .masthead__band::after,.colophon__band::before{width:100%}

  .portrait{width:112px;height:138px;margin:0 0 var(--s3) 0;box-shadow:none;border:1px solid #000}
  .portrait img{filter:grayscale(1)}
  .position>.stamp{filter:grayscale(1);opacity:1;transform:none}

  .entry,.position{break-inside:avoid;box-shadow:none;border-color:transparent;transform:none}
  .entries>li+li{margin-top:var(--s5)}

  /* Every plate open, but modest. !important because the screen rule for an
     examined plate is more specific and would otherwise win at 340px. */
  html.js .plate__img,html.js .plate.is-examined .plate__img,.plate__img{
    height:auto!important;max-height:280px!important;object-fit:contain!important;
    filter:grayscale(1)!important;border:1px solid #000!important;
    box-shadow:none!important;padding:6px!important;background:#fff!important;
  }

  /* The bio and primary link row carry the advisor and lab URLs, which appear
     nowhere else — print them too. */
  .entry a[href^="http"]::after,
  .contact a[href^="http"]::after,
  .bio a[href^="http"]::after,
  .linkrow--primary a[href^="http"]::after{content:" " attr(href);font-size:9px;word-break:break-all}
  a{text-decoration:none}
}


/* ═══ 11 · PREFERENCES ═══════════════════════════════════════════════════ */

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:1ms!important;
    animation-iteration-count:1!important;
    transition-duration:1ms!important;
    scroll-behavior:auto!important;
  }
  /* Only the 2px travel goes; every state change still happens, and the rule
     renders fully inked. */
  .entry:hover,.entry:focus-within,.entry:focus-visible{transform:none}
  .record{border-left-color:var(--rule-strong)}
}

@media (prefers-contrast:more){
  :root{
    --ink-muted:#3F3D36;
    --rule-hair:#8B8778;
    --rule-strong:#6E6A5C;
    --texture:0;
  }
  .grain,.mottle,.fold,.plate::after,.portrait::after{display:none}
  .plate__img,.portrait img,.position>.stamp{filter:none}
  .position>.stamp{opacity:1}
  .stamp-venue--preprint{border-style:dashed;border-color:var(--ink)}
}

@media (forced-colors:active){
  :root{
    --voice-enc:CanvasText;--voice-vis:CanvasText;
    --voice-int:CanvasText;--voice-rhe:CanvasText;
    --shadow:CanvasText;
    /* --ink-muted carries author lists, dates and cities — real content.
       GrayText is the system's "unavailable" colour and would render the
       whole record as greyed out. */
    --ink:CanvasText;--ink-muted:CanvasText;
    --paper:Canvas;--paper-inset:Canvas;--bone-card:Canvas;
    --rule-hair:CanvasText;--rule-strong:CanvasText;
  }
  .grain,.mottle,.fold,.plate::after,.portrait::after{display:none}
  /* No colour, so the card is drawn by its border; the border keeps its
     reserved 2px width, so hover still shifts zero pixels. */
  .entry:hover,.entry:focus-within,.entry:focus-visible{border-color:CanvasText;box-shadow:none}
  .entry::before{display:none}
  .entry__bullet{background:CanvasText;outline-color:CanvasText}
  /* FACULTY FOCUS degrades to an underline; hue no longer exists. All eight
     faculties, for the reason given at the restore list in §5 — a faculty
     left out here is not merely unstyled, its control does nothing. */
  main[data-focus="encyclopedia"]    .voice[data-speaker="encyclopedia"]    .voice__prose,
  main[data-focus="visual-calculus"] .voice[data-speaker="visual-calculus"] .voice__prose,
  main[data-focus="interfacing"]     .voice[data-speaker="interfacing"]     .voice__prose,
  main[data-focus="rhetoric"]        .voice[data-speaker="rhetoric"]        .voice__prose,
  main[data-focus="half-light"]      .voice[data-speaker="half-light"]      .voice__prose,
  main[data-focus="inland-empire"]   .voice[data-speaker="inland-empire"]   .voice__prose,
  main[data-focus="electrochemistry"] .voice[data-speaker="electrochemistry"] .voice__prose,
  main[data-focus="shivers"]         .voice[data-speaker="shivers"]         .voice__prose{
    text-decoration:underline;text-underline-offset:3px;
  }
  .portrait img,.position>.stamp{filter:none;mix-blend-mode:normal;opacity:1}
}


/* ═══ 12 · LAMPLIGHT — html[data-theme="disco"] ══════════════════════════════
   Same transcript and grid on a dark ground. Five findings drive the block.

   (a) THE OFFSET INVERTS. On #141B19 pure black reaches only 1.20:1 (light
       theme 9.05:1), so --shadow becomes a lit brass edge at 3.53:1. Same
       5px/6px offsets, zero blur.

   (b) OVER-CONTENT DARKENING IS UNAFFORDABLE — the WCAG +0.05 flare term
       dominates once both luminances are near zero. Mottle, vignette and
       folds move below the content (z-index:-1).

   (c) BELOW-CONTENT IS NOT FREE: it repaints the ground, which is half of
       every ratio, so it is free only while it DARKENS. All darken-only except
       the fold ridge, which is bounded instead (§12.6).

   (d) A BLEND ON .mottle's PSEUDO-ELEMENTS NEVER REACHED THE PAGE. .mottle
       carries contain:strict, and contain:paint forms an isolated group (CSS
       Compositing 1 §5.2), so a child's blend resolves against .mottle's own
       transparent backdrop. Both layers are mix-blend-mode:normal here.

   (e) TILE CONSTANTS ARE NOT sRGB BY DEFAULT. SVG filters interpolate in
       linearRGB: 0.02/0.055/0.055 resolves to #274242, L 0.0472 against the
       ground's L 0.0100 — LIGHTENING the page by up to 22% of every ratio.
       Fixed with color-interpolation-filters='sRGB'.
   ═══════════════════════════════════════════════════════════════════════════ */

html[data-theme="disco"]{
  color-scheme:dark;        /* scrollbars, form UI, the mobile .refile overflow */

  /* ── SURFACES · deep desaturated teal-green ───────────
     --paper-inset RECESSES here where in the light theme it rose. --bone-card
     still LIFTS, 1.077:1 vs the light theme's 1.100:1 — in both themes the
     card is drawn by its 2px border and offset, not its fill. */
  --paper:#141B19;          /* L 0.010028                                   */
  --paper-inset:#0D1413;    /* refile strip, colophon band, maze, cite-out   */
  --bone-card:#1A2220;      /* hard-card fill on hover/focus                 */

  /* ── ONE NEW TOKEN ────────────────────────────────────
     --mat is the board behind every full-colour raster. (1) A white-ground
     figure meets this room at 17.49:1; the mat splits that into 2.08:1 then
     6.08:1. (2) Two teasers carry real alpha and would otherwise composite
     onto near-black and vanish. #514D44 is restrained: an even split would
     need L≈0.164. */
  --mat:#514D44;

  /* ── INK · nicotine-stained bone, not paper-white ───── */
  --ink:#E2DCC8;            /* 12.75:1 · held under a glaring 15:1           */
  --ink-muted:#949C92;      /*  6.19:1 · cooled green to hold dE 0.080 from
                                the (warm) Encyclopedia                      */
  --rule-hair:#3B4542;      /*  1.76:1 · NON-TEXT · 1.64:1 on the card,
                                matching the light theme's own 1.67:1 there  */
  --rule-strong:#4E5A57;    /*  2.44:1 · NON-TEXT · light theme ships 2.21   */
  --shadow:#7C6F4E;         /*  3.53:1 · THE LIT EDGE — finding (a)          */
  --trail-rgb:242,190,110;  /* warmer than the type, so the nib reads as a
                                lit mark                                   */  /* NOT decorative: js/site.js:435 reads
                                this string into a <canvas> and setTheme()
                                re-reads it per toggle. At the light theme's
                                18,18,14 the trail draws at 1.00:1 here.     */

  /* ── VOICES · colour still identifies WHO IS SPEAKING ─
     enc neutral registrar, vis teal, int brass, rhe oxblood. oklab hues
     96.9° / 176.7° / 79.5° / 28.0°; mutual dE floor 0.0745 against the light
     theme's 0.0548. */
  --voice-enc:#B5B2A4;      /*  8.22:1 · okC 0.020, near-achromatic. Separates
                                by LIGHTNESS, never hue — pushing it green
                                collides with Visual Calculus. Step over
                                --ink-muted 1.328× (light theme 1.285×), and
                                faculty-focus dims it to --ink-muted, so that
                                step is the only signal left.                */
  --voice-vis:#78B8A8;      /*  7.68:1 · verdigris                           */
  --voice-int:#C9A05A;      /*  7.21:1 · tarnished brass                     */
  --voice-rhe:#D97367;      /*  5.50:1 · THE BINDING VALUE. True oxblood
                                #7A2B25 measures 1.83:1 here; okL 0.669 is the
                                floor at which chroma 0.130 still clears 5:1 on
                                the card. Rhetoric sets the page's two smallest
                                types (10px tracked caps), so it is held at
                                5.09:1 worst-surface.                        */

  /* The two Lamplight-only faculties, solved against all three dark surfaces
     (#141B19 ground, #1A2220 card, #0D1413 inset) and the four voices above;
     six-way mutual-dE floor 0.0745. */
  --voice-hlf:#8CB2D2;      /*  7.84:1 ground · 7.28:1 card · HALF LIGHT.
                                okh 244.2°, the one gap left between verdigris
                                and the near-hueless registrar. Must not be a
                                second red — Rhetoric objects under it.      */
  --voice-inl:#C0A8DF;      /*  8.25:1 ground · 7.66:1 card · INLAND EMPIRE.
                                okh 304.5°, furthest of the six from every
                                other (dE floor 0.0777).                     */
}

/* The selection band must read AS a band; --rule-hair would be a 1.76:1
   smear. Tarnished brass at 2.30:1 against the ground (the light theme's
   band reads at 1.52:1), carrying --ink at 5.53:1. */
html[data-theme="disco"] ::selection{background:#5C5340;color:var(--ink)}


/* ── 12.1 · THE PORTRAIT · full colour, in both themes ───────────────────
   multiply has to GO, not just change value: the light theme uses it to
   dissolve the white studio ground into parchment, and on #141B19 it drives
   the face to black. The photo sits on a --mat board instead. brightness(.86)
   puts studio white at 12.63:1, one notch UNDER --ink at 12.75:1, so it never
   out-glares body text; saturate(1.08) restores the chroma dimming costs. */
html[data-theme="disco"] .portrait{background:var(--mat)}
html[data-theme="disco"] .portrait img{
  mix-blend-mode:normal;
  filter:brightness(.86) saturate(1.08);
}
/* The fibre falls on a photograph the same as on everything else. */
html[data-theme="disco"] .portrait::after{opacity:calc(.05 * var(--texture))}


/* ── 12.2 · THE FOUR LOCKUPS · two different problems ────────────────────
   THREE (NTU, SenseTime ×2) are dark marks on pure white, no alpha. multiply
   dissolves the white but annihilates the marks; screen alone turns the white
   ground into a slab. Fix: grayscale then invert gives light marks on BLACK,
   which screen maps onto the backdrop exactly. grayscale must run BEFORE
   invert or NTU's red negates to teal. Measured at the light theme's own .82:

     source luma  →  light theme (multiply)  →  disco (invert+screen)
        255              1.00:1                     1.00:1   ← exact dissolve
        16              10.98:1                     9.87:1
        48               7.35:1                     7.64:1
        88               4.49:1                     5.36:1
        128              2.88:1                     3.63:1
        168              1.93:1                     2.34:1
        255→hover       16.68:1                    16.01:1

   .position drops its isolated --paper backdrop so screen resolves against the
   real lit ground. On hover .entry becomes a stacking context, so the blend
   needs the card's fill back: .entry and .position share an <article>, and
   that (0,2,1) override beats the base .entry:hover fill at (0,2,0). Without
   it a hovered row hands the blend a TRANSPARENT backdrop and every lockup
   renders as a solid black plate. Do not delete the hover rule. */
html[data-theme="disco"] .position{background:transparent;isolation:auto}
html[data-theme="disco"] .position:hover,
html[data-theme="disco"] .position:focus-within{background:var(--bone-card)}
html[data-theme="disco"] .position>.stamp{
  filter:grayscale(1) invert(1) contrast(1.04) brightness(.94);
  mix-blend-mode:screen;
  opacity:.82;              /* the light theme's own resting opacity */
}
/* The base hover rule is .entry:hover .stamp at (0,3,0); the disco resting
   rule above is (0,3,1) and would out-specify it, freezing every logo. */
html[data-theme="disco"] .entry:hover .stamp,
html[data-theme="disco"] .entry:focus-within .stamp{
  opacity:1;
  filter:grayscale(1) invert(1) contrast(1.04);
  transform:none;
}

/* THE FOURTH IS THE TRAP. HyperGAI is already REVERSED — white type on a
   #1C3298 field, 85.7% of the raster — so inverting it gives an orange slab.
   It needs DEEPENING: screen maps only true black, and grayscale leaves this
   field at luma 53. contrast(1.8) drives 53 to 0 and pins 255, so the same
   screen at .82 dissolves the field to 1.00:1 and floats the wordmark at
   12.01:1. A plain brightness dim leaves it at 1.25:1 — a visible 96×33px
   rectangle. */
html[data-theme="disco"] .position>.stamp[src*="hypergai"]{
  filter:grayscale(1) contrast(1.8) brightness(.94);
  mix-blend-mode:screen;
  opacity:.82;
}
html[data-theme="disco"] .entry:hover .stamp[src*="hypergai"],
html[data-theme="disco"] .entry:focus-within .stamp[src*="hypergai"]{
  filter:grayscale(1) contrast(1.8);opacity:1;
}


/* ── 12.3 · THE ELEVEN PLATES · legible and TRUE ─────────────────────────
   No invert (it flips every colormap), grayscale, hue rotation or saturate —
   ONE uniform scalar, so every colormap keeps its ratios. brightness(.86) is
   the portrait's anchor: white ground 12.63:1, one notch under --ink at
   12.75:1; in-figure black-on-white 21.00:1 → 15.17:1. The --mat board makes
   the step room→mat→figure 2.08:1 then 6.08:1 instead of one 17.49:1 cliff.

   Second selector required: the base examined-plate rule is (0,4,1) and would
   beat a plain (0,2,1) theme rule. */
html[data-theme="disco"] .plate__img,
html[data-theme="disco"].js .plate.is-examined .plate__img{
  background:var(--mat);
  filter:brightness(.86);
}
/* The closed 56px sliver is a sample, never the information, so it may be
   degraded: 6.04:1, well under both the open plate and --ink. */
html[data-theme="disco"].js .plate__img{
  background:none;
  filter:grayscale(1) brightness(.55) sepia(.34) contrast(1.04);
}
/* …but on touch every plate renders open, and THAT base rule is (0,2,1),
   so the disco sliver rule above at (0,3,1) would out-specify it and leave
   every figure collapsed to a strip on a phone. Re-open them, after. */
@media (hover:none){
  html[data-theme="disco"].js .plate__img{
    background:var(--mat);
    filter:brightness(.86);
  }
}


/* ── 12.4 · THE GRAIN · the one layer that stays ABOVE the content ───────
   Bounded: tile alpha caps at 0.55, so at opacity .07 the effective alpha is
   0.0385. Adversarial case (pure-black glyph pixel beside a pure-white one)
   costs the worst text pair 5.09:1 → 4.87:1. */
html[data-theme="disco"] .grain{
  mix-blend-mode:soft-light;
  opacity:calc(.07 * var(--texture));
  /* Real canvas weave instead of feTurbulence, contrast-clamped to 64–189.
     Brightest backdrop pixel: --ink 10.69:1, --voice-rhe 4.61:1. Seam check on
     the 512px tile: wrap discontinuity is 0.75x/0.69x of the interior
     adjacent-pixel step, so no join shows. */
  background-image:url("../assets/img/canvas.webp");
  background-size:512px 512px;
}


/* ── 12.5 · MOTTLE, VIGNETTE AND FOLDS GO UNDER THE SHEET ────────────────
   Findings (b) and (c). They never touch --bone-card, --paper-inset or the
   maze — opaque backgrounds on in-flow elements paint above a negative
   z-index — so each token has ONE worst-case surface. */
html[data-theme="disco"] .mottle,
html[data-theme="disco"] .fold{z-index:-1}

/* Finding (e): a colder pigment, declared in sRGB. #070E10 at L 0.00397
   against the ground's L 0.010028. Max pooling takes the ground to #0E1515,
   where --ink rises 12.75→13.46 and Rhetoric 5.50→5.80. */
html[data-theme="disco"]{
  --mottle-tile:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='1400' height='1400'%3E%3Cfilter id='m' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.009' numOctaves='2' stitchTiles='stitch' seed='19'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.027 0 0 0 0 0.055 0 0 0 0 0.063 0.9 0 0 0 0'/%3E%3C/filter%3E%3Crect width='1400' height='1400' filter='url(%23m)'/%3E%3C/svg%3E");
}
/* THE ROOM. A painting of a dim interior replaces the procedural turbulence.
   Below-content (z-index:-1), so it repaints the ground every ratio is
   measured against — and unlike the mottle it does not only darken, hence the
   pre-dimmed file (constants below). Worst case, brightest pool pixel over
   --paper: --ink 10.72:1 · --ink-muted 5.20:1 · --voice-rhe 4.62:1 ·
   --voice-vis 6.46:1. Do not raise the opacity. */
html[data-theme="disco"] .mottle::before{
  mix-blend-mode:normal;                 /* finding (d) — it never blended */
  background-image:url("../assets/img/room-backdrop.webp");
  background-size:cover;
  background-position:50% 30%;
  /* The LAMP POOL sets this layer's ceiling, so the file compresses it (γ1.6
     then ×0.30: peak 173 -> 56) rather than dimming uniformly — that buys
     opacity .47 against .29. Solved for the PAIR with the soft-light grain on
     top: brightest pool pixel, --ink 10.47:1 · --voice-rhe 4.51:1. */
  opacity:calc(.47 * var(--texture));
}
/* No image on a phone: 100KB for a texture behind one narrow column, and the
   grain already carries the surface. */
@media (max-width:759px){
  html[data-theme="disco"] .mottle::before{background-image:none}
}

/* THE BULB. Darkening only — the lit centre is the ABSENCE of falloff, which
   keeps --paper a single worst-case surface. At the far corner the ground
   reaches #080E0F, where --ink reads 14.17:1 and Rhetoric 6.11:1. */
html[data-theme="disco"] .mottle::after{
  background:radial-gradient(118% 88% at 50% 30%,
              transparent 34%,
              rgba(6,13,14,.42) 66%,
              rgba(4,9,11,.72) 100%);
  mix-blend-mode:normal;
  opacity:var(--texture);
}

/* THE ONE LAYER THAT IS NOT FREE. The valley darkens (free); the ridge
   catches the lamp. Bounded rather than removed: at alpha .035 the lit ground
   reaches #1C221E, holding Rhetoric at 5.09:1 and --ink at 11.80:1 — the
   scheme's floor. */
html[data-theme="disco"] .fold{
  background:linear-gradient(180deg,
              transparent,
              rgba(0,0,0,.45) 49%,
              rgba(255,226,174,.035) 51%,
              transparent);
}

/* multiply does nothing over --mat or a dimmed plate, so the fibre tile would
   stop at the figure's edge; soft-light carries it across figure AND board.
   Both sit inside isolation:isolate wrappers, so the blend resolves against
   the picture. */
html[data-theme="disco"] .plate::after,
html[data-theme="disco"] .portrait::after{
  mix-blend-mode:soft-light;
  opacity:calc(.09 * var(--texture));
}

/* The visitor widget draws in white (cl=ffffff), so it already reads here. */
html[data-theme="disco"] .widget{filter:grayscale(1) brightness(.9);opacity:.4}

/* ── 12.5b · THE ORNAMENT ────────────────────────────────────────────────
   Under the lamp each section rule gets one brass lozenge at its head: a 7px
   square on the diagonal. Three places on the page; keep it to that. */
html[data-theme="disco"] .section:not(.section--overture) .transcript{position:relative}
html[data-theme="disco"] .section:not(.section--overture) .transcript::after{
  content:"";position:absolute;top:-4px;left:0;
  width:7px;height:7px;
  background:var(--shadow);
  transform:rotate(45deg);
  pointer-events:none;
}
/* The masthead band gets the same head-mark. */
html[data-theme="disco"] .masthead__band::before{
  content:"";position:absolute;bottom:-4px;left:var(--wrap-pad);
  width:7px;height:7px;background:var(--shadow);transform:rotate(45deg);
}

/* The phone rule that would otherwise be lost: the base sheet drops the grain
   to .032 at (0,1,0) and the disco rule above is (0,2,0). Same proportion. */
@media (max-width:759px){
  html[data-theme="disco"] .grain{opacity:calc(.05 * var(--texture))}
}


/* ── 12.6 · FALLBACK · no mix-blend-mode ─────────────────────────────────
   The base fallback paints --bone-card behind the images — near-black here,
   so use the mat. Drop the invert too: without screen to map black onto the
   backdrop an inverted logo is a black slab. */
@supports not (mix-blend-mode:multiply){
  html[data-theme="disco"] .portrait img,
  html[data-theme="disco"] .position>.stamp,
  html[data-theme="disco"] .position>.stamp[src*="hypergai"]{
    filter:none;opacity:1;
    background:var(--mat);
    outline:1px solid var(--rule-hair);outline-offset:-1px;
  }
  html[data-theme="disco"] .mottle::before,
  html[data-theme="disco"] .mottle::after{display:block}
}


/* ── 12.7 · PREFERS-CONTRAST: MORE ───────────────────────────────────────
   Required: the base more-contrast block targets :root at (0,1,0) and
   hardcodes LIGHT values (--ink-muted:#3F3D36 on a dark ground), while
   html[data-theme="disco"] is (0,1,1) and wins regardless of source order.
   Every voice rises to at least 7.06:1 worst-surface, --ink to 14.38:1. */
@media (prefers-contrast:more){
  html[data-theme="disco"]{
    --ink:#F6F1E2;          /* 15.49:1 paper · 14.38:1 card */
    --ink-muted:#AAB1A8;    /*  7.97:1 ·  7.39:1 */
    --rule-hair:#525E5B;    /*  2.59:1 ·  2.40:1 · NON-TEXT */
    --rule-strong:#6F7B77;  /*  3.98:1 ·  3.69:1 · NON-TEXT */
    --shadow:#A08F63;       /*  5.50:1 ·  5.11:1 · NON-TEXT */
    --voice-enc:#D2CFC0;    /* 11.18:1 · 10.38:1 */
    --voice-vis:#9CD5C7;    /* 10.63:1 ·  9.86:1 */
    --voice-int:#E3BD7C;    /*  9.86:1 ·  9.15:1 */
    --voice-rhe:#E89685;    /*  7.61:1 ·  7.06:1 */
    --texture:0;
  }
  html[data-theme="disco"] .grain,
  html[data-theme="disco"] .mottle,
  html[data-theme="disco"] .fold,
  html[data-theme="disco"] .plate::after,
  html[data-theme="disco"] .portrait::after{display:none}
  html[data-theme="disco"] .portrait img{filter:none}
  html[data-theme="disco"] .plate__img,
  html[data-theme="disco"].js .plate__img,
  html[data-theme="disco"].js .plate.is-examined .plate__img{filter:none}
  html[data-theme="disco"] .position>.stamp{
    opacity:1;filter:grayscale(1) invert(1) contrast(1.12);
  }
  html[data-theme="disco"] .position>.stamp[src*="hypergai"]{
    opacity:1;filter:grayscale(1) contrast(1.8);
  }
}


/* ── 12.8 · FORCED-COLORS ────────────────────────────────────────────────
   MANDATORY: html[data-theme="disco"] at (0,1,1) out-specifies the base
   sheet's :root at (0,1,0), so without this the disco hexes WIN and defeat
   the user's palette. Also stop the ink trail — a raw rgb triple no system
   keyword can reach. */
@media (forced-colors:active){
  html[data-theme="disco"]{
    --voice-enc:CanvasText;--voice-vis:CanvasText;
    --voice-int:CanvasText;--voice-rhe:CanvasText;
    --shadow:CanvasText;
    --ink:CanvasText;--ink-muted:CanvasText;
    --paper:Canvas;--paper-inset:Canvas;--bone-card:Canvas;--mat:Canvas;
    --rule-hair:CanvasText;--rule-strong:CanvasText;
  }
  html[data-theme="disco"] .grain,
  html[data-theme="disco"] .mottle,
  html[data-theme="disco"] .fold,
  html[data-theme="disco"] .trail,
  html[data-theme="disco"] .plate::after,
  html[data-theme="disco"] .portrait::after{display:none}
  html[data-theme="disco"] .portrait,
  html[data-theme="disco"] .position{background:Canvas}
  html[data-theme="disco"] .portrait img,
  html[data-theme="disco"] .position>.stamp,
  html[data-theme="disco"] .position>.stamp[src*="hypergai"]{
    filter:none;mix-blend-mode:normal;opacity:1;background:Canvas;outline:none;
  }
  html[data-theme="disco"] .plate__img,
  html[data-theme="disco"].js .plate__img,
  html[data-theme="disco"].js .plate.is-examined .plate__img{
    filter:none;background:Canvas;
  }
  html[data-theme="disco"] ::selection{background:Highlight;color:HighlightText}
}


/* ── 12.9 · PRINT ────────────────────────────────────────────────────────
   Same specificity defect as §12.8: the base print block is :root at (0,1,0),
   so printing with the lamp down comes out bone-on-black. Only the tokens
   disco declares need repeating. The plates are covered by the base rule's
   !important; the two image rules are not — .portrait img (0,1,1) and
   .position>.stamp (0,2,1) lose to their disco counterparts, and a live
   mix-blend-mode:screen prints blank rectangles. */
@media print{
  html[data-theme="disco"]{
    color-scheme:light;
    --paper:#fff;--paper-inset:#fff;--bone-card:#fff;--mat:#fff;
    --ink:#000;--ink-muted:#333;
    --rule-hair:#999;--rule-strong:#666;
    --shadow:transparent;
    --trail-rgb:18,18,14;
    --voice-enc:#000;--voice-vis:#000;--voice-int:#000;--voice-rhe:#000;
    --voice-hlf:#000;--voice-inl:#000;
  }
  html[data-theme="disco"] .portrait{background:#fff}
  html[data-theme="disco"] .portrait img{
    filter:grayscale(1);mix-blend-mode:multiply;
  }
  html[data-theme="disco"] .position{background:#fff}
  html[data-theme="disco"] .position>.stamp,
  html[data-theme="disco"] .position>.stamp[src*="hypergai"]{
    filter:grayscale(1);mix-blend-mode:multiply;opacity:1;
    transform:none;outline:none;
  }
}


/* ═══ 13 · THE OTHER LEDGER — GAMES AND MUSIC ════════════════════════════
   Lamplight swaps content, not just colour: WORK and RECORD leave, GAMES and
   MUSIC arrive, the masthead renames itself. The swap must be display, not
   visibility — display:none takes the hidden section's links out of the tab
   order and the a11y tree, so neither ledger is invisible tab stops inside
   the other.                                                               */

#arcade,#rotation,.nav-night{display:none}

html[data-theme="disco"] #arcade,
html[data-theme="disco"] #rotation{display:block}
html[data-theme="disco"] .nav-night{display:revert}
html[data-theme="disco"] #work,
html[data-theme="disco"] #record,
html[data-theme="disco"] .nav-day{display:none}

/* GAMES becomes the first section under the overture. The hairline already
   applies (the rule is written against .section:not(.section--overture));
   this only restates the margin so the two ledgers start at an identical
   height and the swap does not jump. */
html[data-theme="disco"] #arcade{margin-top:var(--s9)}

/* ── THE TALLY · the section's own header ──────────────────────────────── */
.tally{
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  font-variant-numeric:tabular-nums slashed-zero;
  color:var(--ink-muted);
  margin:var(--s5) 0 var(--s7);
  padding-top:var(--s3);
  margin-left:calc(var(--record-pad) * -1);padding-left:var(--record-pad);
  border-top:var(--hair);
}

/* ── THE ART ─────────────────────────────────────────────────────────────
   Every image is SELF-HOSTED WebP beside the paper teasers: eleven files,
   184KB. Hotlinking Steam's and Spotify's CDNs would fire eleven third-party
   requests at page load; as it is the page calls nobody until play.
   Two shapes because the sources have two: Steam header art is 460x215, a
   sleeve is square. Both lay out as image column + data column.
   alt="" is deliberate — the titles are in the <h3> and .facts line beside. */
.game__body,.track__body{
  display:flex;align-items:flex-start;
  gap:var(--s4);
  margin-top:var(--s3);
}
.game__data,.track__data{flex:1 1 auto;min-width:0}
.game__body .facts,.track__body .facts{margin-top:0}

.game__art,.track__art{
  flex:none;display:block;height:auto;
  border:1px solid var(--rule-hair);
  background:var(--paper-inset);
}
.game__art{width:180px}
.track__art{width:96px}

/* Under the lamp this art is the only saturated thing in the room — the glare
   cliff §12 opens with. Same treatment as the eleven teasers: a --mat board so
   the step is room→mat→picture, plus the brass edge. No filter. */
html[data-theme="disco"] .game__art,
html[data-theme="disco"] .track__art{
  border-color:var(--mat);
  background:var(--mat);
  box-shadow:3px 3px 0 var(--shadow);
}

/* ── THE FACT LINE · row metadata, same register as .entry__authors ────── */
.facts{
  font:400 var(--t-meta)/var(--lh-meta) var(--mono);
  font-variant-numeric:tabular-nums slashed-zero;
  color:var(--ink-muted);
  margin-top:var(--s2);
}
.facts b{font-weight:600;color:var(--ink)}

/* ── THE METERS · DECORATION, and declared so ────────────────────────────
   aria-hidden and empty of text; every quantity is words on the .facts line
   above, so a JS failure costs nothing but a bar. Widths arrive as --w from
   js/site.js, computed from data-hours against the largest in the list.
   Thick track = TIME, scaled to the longest row; thin = COMPLETION, scaled
   to its own total.                                                        */
.meter{margin-top:var(--s3);display:flex;flex-direction:column;gap:var(--s1)}
.meter__track{
  display:block;height:7px;
  background:var(--paper-inset);
  border:1px solid var(--rule-hair);
}
.meter__track--thin{height:3px;border:0;background:transparent;box-shadow:inset 0 0 0 1px var(--rule-hair)}
.meter__fill{
  display:block;height:100%;
  width:var(--w,0%);
  background:var(--ink);
  transform-origin:left center;
}
.meter__fill--got{background:var(--rule-strong)}

/* Under the lamp the hours bar reuses --shadow's brass, not a new colour. */
html[data-theme="disco"] .meter__fill{background:var(--shadow)}
html[data-theme="disco"] .meter__fill--got{background:var(--rule-strong)}
html[data-theme="disco"] .meter__track{background:var(--paper-inset)}

/* Scroll timeline, so no observer, rAF or listener; degrades to an already
   drawn bar where animation-timeline is unsupported. scaleX and not width:
   the box is already --w wide, so this runs on the compositor and never
   reflows a row. */
@supports (animation-timeline:view()){
  @media (prefers-reduced-motion:no-preference){
    @keyframes meter-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
    .meter__fill{
      animation:meter-draw linear both;
      animation-timeline:view();
      animation-range:entry 15% cover 30%;
    }
  }
}

/* ── THE SEAL · 48 of 48. The only achievement stamp, so the only one with
      a double rule; no venue stamp uses one. ──────────────────────────── */
.stamp-perfect{
  border:1px double var(--ink);
  outline:1px solid var(--ink);outline-offset:2px;
  color:var(--ink);
}
html[data-theme="disco"] .stamp-perfect{
  border-color:var(--shadow);outline-color:var(--shadow);color:var(--voice-int);
}

/* Inland Empire's violet — the voice that speaks directly underneath it, so
   stamp and annotation share a faculty. */
.stamp-source{border:1px solid var(--voice-inl);color:var(--voice-inl)}

/* ── THE DECK · built by JS on the click, removed on close ───────────────
   Nothing exists until somebody presses play: no third-party request, no
   third-party cookie for a visitor who only reads. */
.deck{margin-top:var(--s3)}
.deck[hidden]{display:none}
/* --ink-muted, NOT --speaker: the inherited --speaker resolves to Encyclopedia
   grey and would read as the registrar having said it. */
.deck__head{
  display:flex;align-items:center;gap:var(--s2);
  font:500 var(--t-cap-xs)/var(--lh-cap-xs) var(--mono);
  letter-spacing:var(--tr-cap-xs);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink-muted);
  margin-bottom:var(--s2);
}
/* Means "the panel is open", not "audio is playing": playback happens in a
   cross-origin frame and is not observable from here. */
.deck__lamp{
  flex:none;width:7px;height:7px;
  background:var(--shadow);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--shadow) 22%,transparent);
}
@media (prefers-reduced-motion:no-preference){
  @keyframes filament{50%{opacity:.45}}
  .deck__lamp{animation:filament 2.6s var(--ease-out) infinite}
}
/* 16:9 or YouTube's own player letterboxes it. Capped at 480 so a music row
   stays a row instead of taking the full 720 measure. */
.deck iframe{
  display:block;width:100%;max-width:480px;
  aspect-ratio:16/9;height:auto;
  border:var(--card-border);
  background:var(--paper-inset);
  color-scheme:normal;   /* the frame carries its own dark UI in both themes */
}
html[data-theme="disco"] .deck iframe{box-shadow:var(--card-shadow)}

/* The row whose deck is open keeps its card lifted. */
.entry.is-playing{
  background:var(--bone-card);
  border-color:var(--ink);
  box-shadow:var(--card-shadow);
  transform:translate(-2px,-2px);
}

/* ── NARROW ──────────────────────────────────────────────────────────────
   The band collapses to one column below 900px (see §8). The deck and the
   meters are already fluid; only the fact line needs help, since three
   middot-separated facts wrap badly at 340px. */
@media (max-width:559px){
  .facts .sep{display:none}
  .facts{display:flex;flex-direction:column}
  /* A 180px banner beside a 150px text column serves neither, and the game
     art is 2.15:1 landscape, so stack it at the full measure. */
  .game__body{display:block}
  .game__art{width:100%;margin-bottom:var(--s3)}
  /* A square sleeve survives being small, so the track stays side-by-side. */
  .track__art{width:72px}
}

/* ── PLAIN VIEW · the quantities live on .facts, not in the meters ─────── */
html[data-voice="off"] .deck__head,
html.no-js .deck__head,
html[data-voice="off"] .meter,
html.no-js .meter{display:none}

/* ── PRINT · covers printing WHILE the lamp is down, which otherwise puts a
      dead player on the page. Neither ledger prints its meters. ───────── */
@media print{
  #arcade,#rotation,.nav-night,.meter,.deck{display:none!important}
  html[data-theme="disco"] #work,
  html[data-theme="disco"] #record,
  html[data-theme="disco"] .nav-day{display:revert!important}
  html[data-theme="disco"] #arcade,
  html[data-theme="disco"] #rotation{display:none!important}
}

@media (forced-colors:active){
  :root{--voice-hlf:CanvasText;--voice-inl:CanvasText}
  .meter__track{border-color:CanvasText}
  .meter__fill{background:CanvasText}
  .meter__fill--got{background:GrayText}   /* decoration: GrayText is fine here */
  .stamp-perfect{border-color:CanvasText;outline-color:CanvasText}
  .deck__lamp{background:CanvasText;box-shadow:none}
  .deck iframe{border-color:CanvasText}
}


/* ═══ 14 · THE DOORWAY ═══════════════════════════════════════════════════
   Links to phd.html, a game. On its own line below the link row in the
   utility-control register (bracketed monospace) so it reads as an aside.
   Brackets are literal characters, as on [ examine ] and [ cite ]. */
.doorway{
  margin-top:var(--s5);
  display:flex;flex-wrap:wrap;align-items:baseline;
  column-gap:var(--s3);row-gap:var(--s1);
}
.doorway__link{
  font:500 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:var(--tr-cap);word-spacing:.08em;text-transform:uppercase;
  color:var(--ink);
  text-decoration:none;white-space:nowrap;
}
.doorway__brackets{color:var(--ink-muted);margin-inline:.35em}
.doorway__link:hover,.doorway__link:focus-visible{
  text-decoration:underline;text-decoration-thickness:1px;text-underline-offset:5px;
}
.doorway__link:hover .doorway__brackets,
.doorway__link:focus-visible .doorway__brackets{color:var(--ink)}
.doorway__aside{
  font:400 var(--t-cap)/var(--lh-cap) var(--mono);
  letter-spacing:0;word-spacing:normal;text-transform:none;
  color:var(--ink-muted);
}

/* Inland Empire's violet under the lamp — the voice that speaks for Disco
   Elysium in the games ledger. */
html[data-theme="disco"] .doorway__link{color:var(--voice-inl)}
html[data-theme="disco"] .doorway__brackets{color:var(--shadow)}

@media (max-width:559px){ .doorway{flex-direction:column} }

/* A dead URL on paper. */
@media print{ .doorway{display:none!important} }
