/* ==========================================================================
   nixie.css — visual study: nixie tubes in the dark
   Yimin Liu

   Self-contained on purpose. This file imports nothing and imports into
   nothing, so the theme can be reasoned about as one file.

   House rule: there is no material. The page is black, and the only things
   drawn on it are type, hairlines and light. Earlier builds gave it a metal
   face — sheen, grain, screws, bevels, seams — and every one of those is a
   texture asking for attention while carrying no information. Nothing here
   has a surface: depth comes from what is lit and what is not.

   The glow rule: neon orange appears in three places and nowhere else —
   nixie digits, hover/focus, and the indicator lamp. One light source means
   whatever is lit is the thing worth reading.
   ========================================================================== */

:root {
  /* Pure black, and no material at all.

     The chassis went through four palettes — brown, graphite, copper, and
     copper-on-grey — and every one of them was the same mistake in a different
     colour. The problem was never the hue. It was that a lit tube on a
     textured metal face is a lit tube competing with a texture: sheen, grain,
     screws, seams and gradients all ask for attention, and none of them are
     information. The page ended up looking like a machine and reading like a
     junk drawer.

     Black has no material to compete. There is exactly one substance on this
     page now — light — and everything else is either type or the hairline that
     separates two blocks of it. The instrument is still there; it is just the
     glow and the silkscreen doing it, which is all that was ever carrying it. */
  --void:        #000000;   /* the page. Not near-black — black. */
  --rule:        #1C1C1E;   /* the only structural line there is */
  --anchor:      #2A2A2C;   /* underlines on small caps */
  --rust:        #C0714A;   /* the caret, and what a link is underlined in */

  /* The one light source */
  --glow:        #FF9A3D;   /* neon discharge — the real colour of Ne */
  --glow-core:   #FFC47E;   /* the wire itself, hotter than its halo */
  --glow-hot:    #FFE7C2;   /* the innermost millimetre */
  --cathode:     #5F3819;   /* the digits that are NOT lit. Do not delete
                               these; they are the whole tell of a real tube */

  /* Ink — a trace of warmth so it shares a lamp with the discharge instead of
     looking like a separate screen behind it. On black: 12.0, 7.0, 5.3 and
     4.5:1. Nothing on the page sits on anything but black, so those four
     numbers are the whole contrast story, and all four pass AA.

     --text used to be #E9E7E3, at 17:1. Two things were wrong with that. It is
     the halation figure — white-on-black that bright smears for anyone with
     astigmatism, and this site has pages of prose to read. And it made the body
     copy the brightest thing on the page, brighter than the lit filament at
     13.4:1, which quietly inverts the entire premise: the tubes are supposed to
     be the light. At 12.0:1 the text sits below the glow where it belongs and
     is still five stops clear of the 7:1 AAA floor.

     Dropping --text alone would have halved the step down to --text-dim, so the
     whole ramp is rescaled with it. The spacing below is in CIE L*, not in
     contrast ratio, because L* is what the eye actually reads as "one step
     quieter" — even gaps here look even. --label is the fixed end: it is
     pinned to the 4.5:1 floor and everything above is spaced off it. */
  --text:        #C6C3BE;   /* body                    L* 78.9 */
  --text-dim:    #98948E;   /* secondary copy          L* 61.5  ↓17.4 */
  --text-faint:  #817F7A;   /* colophon                L* 53.2  ↓ 8.3 */
  --label:       #78746D;   /* the silkscreened rail label. Set by contrast, not
                               by taste: at 9px it is small text, so it has to
                               clear 4.5:1 even though it reads as a whisper.
                                                        L* 49.0  ↓ 4.2 */

  --pix:  "Silkscreen", ui-monospace, monospace;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --rail: 3.25rem;          /* width of the silkscreened label rail */
  --measure: 64ch;

  --s1: .25rem; --s2: .5rem; --s3: .75rem; --s4: 1rem;
  --s5: 1.5rem; --s6: 2.25rem; --s7: 3.5rem;
}

/* --- Reset ---------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

/* 106.25% and not 17px, so this multiplies whatever the visitor set in their
   browser rather than overriding it. Everything on the page is sized in rem —
   type, the rail, the spacing scale, the tube geometry — so this moves all of
   it together and the composition stays self-similar. The breakpoints are the
   one exception: rem in a media query resolves against the browser default and
   not against this, so they do not drift with it. */
html {
  font-size: 106.25%;
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  scroll-padding-top: 4.5rem;
}
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  min-height: 100vh;
  font: 400 .9375rem/1.75 var(--mono);
  color: var(--text);
  background: var(--void);
  -webkit-font-smoothing: antialiased;
}

img { display: block; max-width: 100%; }
ul, ol { list-style: none; padding: 0; }

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

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: var(--s3) var(--s4);
  background: var(--glow); color: #130F0C;
  font: 400 .75rem/1 var(--pix); letter-spacing: .1em;
}
.skip-link:focus { left: 0; }

/* --- Links ---------------------------------------------------------------- */

a {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--rust);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color .12s linear, text-decoration-color .12s linear;
}
a:hover { color: var(--glow); text-decoration-color: var(--glow); }
a:focus-visible {
  outline: 2px solid var(--glow);
  outline-offset: 3px;
  color: var(--glow);
}

/* --- The column ----------------------------------------------------------- */
/* A measure, not an object. No outline, no shadow, no fill — the page does not
   sit on anything, because there is nothing for it to sit on. */

.rack {
  max-width: 62rem;
  margin: 0 auto;
}

/* --- Legend strip --------------------------------------------------------- */
/* The silkscreened band across the top. Everything in here is panel legend:
   small, letterspaced, bitmap. */

/* Opaque because it is sticky and content scrolls under it. That is the only
   reason it has a background at all. */
.legend {
  position: sticky; top: 0; z-index: 40;
  background: var(--void);
  border-bottom: 1px solid var(--rule);
}

.legend-in {
  display: flex; align-items: center; gap: var(--s5);
  padding: var(--s3) var(--s5);
  flex-wrap: wrap;
}

.mark {
  display: inline-flex; align-items: center; gap: var(--s3);
  font: 400 .8125rem/1 var(--pix);
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text);
  margin-right: auto;
}

/* The one LED: power. */
.lamp {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--glow);
  box-shadow: 0 0 4px rgba(255,190,120,1), 0 0 10px rgba(255,154,61,.85), 0 0 20px rgba(255,154,61,.45);
  animation: breathe 4s ease-in-out infinite;
}

/* --- The pointer ----------------------------------------------------------
   The arrow is hidden and a lamp is put in its place. It is built by nixie.js
   and does not exist in the markup, because it is decoration and the pages
   have to be complete without the script.

   This is the only lit thing on the site that is not content, so it is on the
   tightest leash of anything here:

   - It is *small.* 8px of core, and that — not dimness — is what keeps it off
     the glow rule. The first build tried to solve this with opacity and the
     result was invisible: a 22px radial at .62 reads as a smudge, not as a
     lamp. The real argument is that a point this size only ever appears where
     the pointer already is, which is where the reader is already looking. It
     marks attention; it does not pull it. What would break the rule is a large
     or trailing glow sweeping across the composition — and neither of those is
     a brightness problem either.
   - It is *locked to the pointer*, with no easing and no trail. That was a
     house-style argument when the glow only accompanied the arrow; now that it
     *is* the arrow it is not negotiable. A cursor that arrives late is a
     broken cursor.
   - It is *built exactly like .lamp*: a small solid core and a four-stop halo,
     no radial background, no border. Same construction as the indicator lamps
     the page already has, so it joins the existing vocabulary rather than
     introducing a second kind of light.
   - It does not breathe on its own. One supply, and idling is not on it — the
     only time it changes size is when it is struck.

   The justification under the glow rule is that this is the continuous form of
   hover, which is already one of the three permitted places — not a fourth
   light source. `.is-hot` is where that stops being a rationalisation: over
   anything clickable it opens up, which is the same ember the nav lamps and
   link colours already use.

   Two elements, not one, because the outer is driven by JS and the inner by
   CSS. `transform` on .pointer is the position, rewritten every frame; putting
   a scale in the same declaration would mean animating the press from
   JavaScript. Nesting keeps each owner to its own property. */
.pointer {
  position: fixed;
  top: 0; left: 0;
  z-index: 90;
  pointer-events: none;        /* never eat a click */
  opacity: 0;                  /* until the pointer has actually moved once */
  transition: opacity 180ms linear;
  will-change: transform;
}
.pointer.is-live { opacity: .85; }
.pointer.is-hot  { opacity: 1; }

.pointer i {
  display: block;
  width: 8px; height: 8px;
  margin: -4px 0 0 -4px;       /* centre the core on the hotspot */
  border-radius: 50%;
  background: var(--glow-hot);
  box-shadow:
    0 0  5px rgba(255,196,126,.95),
    0 0 14px rgba(255,154,61,.70),
    0 0 30px rgba(255,154,61,.35),
    0 0 54px rgba(255,130,30,.16);
  /* This timing is the *release* only. The press overrides it to none — see
     .is-down. A slight overshoot past 1 on the way back is what makes it read
     as a lamp springing back rather than a box resizing. */
  transition:
    transform  260ms cubic-bezier(.3, 1.4, .6, 1),
    box-shadow 260ms ease-out;
}

/* Over a link or a switch the halo opens; the core does not move. Nothing on
   this page changes size on hover — that is reserved for the press, which is
   the only thing here that is an action rather than a state. */
.pointer.is-hot i {
  box-shadow:
    0 0  6px rgba(255,214,160,1),
    0 0 18px rgba(255,154,61,.80),
    0 0 38px rgba(255,154,61,.42),
    0 0 66px rgba(255,130,30,.20);
}

/* The press. Instant on the way down and eased on the way back, which is the
   same shape as the 1px key travel on .switch: a detent is instant or it is
   not a detent, but nothing in the world snaps back without settling.

   Read it as striking the tube — the discharge flares and then falls to its
   sustaining level. Scale on the core is safe here where it would not be on
   the page itself, because the core is 8px of light with nothing laid out
   around it; there is nothing for it to push.

   1.85 and not the 2.05 this had when the core was 5px. The press is judged on
   how far it travels, not on the multiplier, and 8 → 14.8px is already further
   than 5 → 10.25px was. */
.pointer.is-down i {
  transform: scale(1.85);
  box-shadow:
    0 0  9px rgba(255,231,194,1),
    0 0 26px rgba(255,154,61,.85),
    0 0 56px rgba(255,154,61,.45),
    0 0 98px rgba(255,130,30,.22);
  transition: none;
}

/* Hiding the native cursor is applied from JS, never from here.

   The whole feature is gated off under prefers-reduced-motion, so an
   unconditional `cursor: none` would leave exactly those visitors with no
   pointer at all. The universal selector is needed because .switch, links and
   buttons set `cursor: pointer` themselves.

   When the class goes on is a real decision and the first answer was wrong:
   it waited for the first mousemove, which meant every page you navigated to
   showed the arrow again until you happened to twitch the mouse. Clicking a
   link is the one moment you are guaranteed *not* to be moving it. See
   nixie.js job 4 for what replaced it. */
html.is-dot,
html.is-dot * { cursor: none; }

/* Each destination keeps its indicator lamp and loses its housing. The lamp
   was always the part doing the work — a button drawn around it just added a
   rectangle, and five rectangles across the top of a black page is a toolbar. */

.legend-nav ul { display: flex; gap: var(--s5); flex-wrap: wrap; }
.legend-nav a {
  display: inline-flex; align-items: center; gap: var(--s3);
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-dim);
  transition: color 120ms linear;
}

/* The lamp, unlit. Same trick the tubes use: what sells a lamp as a lamp is
   that you can see it while it is off. */
.legend-nav a::before {
  content: "";
  flex: none;
  width: 5px; height: 5px; border-radius: 50%;
  background: #26241F;
  transition: background 120ms linear, box-shadow 120ms linear;
}

/* Hover brings the lamp up to an ember, not to a strike, and lifts the label
   to full --text rather than turning it orange. A switch you are about to throw
   should look warmed, not selected — anything brighter and five of these
   across the top of the page start competing with the readouts. */
.legend-nav a:hover { color: var(--text); }
.legend-nav a:hover::before {
  background: #7A4620;
  box-shadow: 0 0 5px rgba(255,154,61,.35);
}

/* The page you are on is the one lamp on the strip that is actually struck. */
.legend-nav a[aria-current] { color: var(--text); }
.legend-nav a[aria-current]::before {
  background: var(--glow);
  box-shadow: 0 0 4px rgba(255,190,120,1), 0 0 10px rgba(255,154,61,.7);
}

.legend-toggle {
  display: none;
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding: var(--s2) var(--s3);
  background: none;
  border: 1px solid var(--rule);
  cursor: pointer;
}
.legend-toggle:hover { color: var(--glow); }
.legend-toggle:active { transform: translateY(1px); }

/* --- A section ------------------------------------------------------------ */
/* No face, no fill, no bolts. One hairline between one section and the next,
   and generous space either side of it. Space separates better than material
   does, and it costs nothing to look at. */

.unit {
  position: relative;
  padding: var(--s6) var(--s5) var(--s6) calc(var(--rail) + var(--s5));
}
.unit + .unit { border-top: 1px solid var(--rule); }

/* The label down the left edge. It survives the strip because it is
   typography, not hardware — a stencilled word, set sideways. */
.unit-tag {
  position: absolute; left: 0; top: var(--s6); bottom: var(--s6);
  z-index: 1;
  width: var(--rail);
  display: grid; place-items: start center;
  writing-mode: vertical-rl;
  transform: rotate(180deg);
  font: 400 .625rem/1 var(--pix);
  letter-spacing: .34em;
  text-transform: uppercase;
  color: var(--label);
}

.unit-body { max-width: var(--measure); }

.unit-h {
  font: 500 .9375rem/1.4 var(--mono);
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--text);
  margin-bottom: var(--s5);
}

/* --- Nameplate ------------------------------------------------------------ */

.plate {
  display: flex; align-items: flex-start; gap: var(--s6);
  flex-wrap: wrap;
}

/* A hairline, and nothing else. The portrait is the only rectangle it needs
   to be, and on black it does not need a housing to be contained. */
.viewport {
  position: relative;
  flex: none;
  width: 148px; height: 148px;
  display: grid; place-items: center;
  overflow: hidden;
  background: #000;
  box-shadow: 0 0 0 1px var(--rule);
}
.viewport img {
  grid-area: 1/1;
  width: 100%; height: 100%;
  object-fit: cover;
  /* Turned down until it sits under the glow. Everything on this page is lit
     by one source, and the portrait is the largest single surface that is not
     type — if it out-shines the tubes the premise of the theme is gone.

     Desaturated with it, so it belongs to a page with one colour on it rather
     than being a full-colour hole punched in the black.

     The first version of this got it backwards. It was grayscale(.55) with
     brightness(.95) — nearly all of the colour taken out and almost none of
     the light — and the result was a corpse: the overcast sky came through at
     .72 relative luminance, brighter than the body copy at .548 and brighter
     than the lit filament at .622, so the single brightest surface on a page
     built around one light source was a photograph of the weather.

     That is the same mistake --text made before the pure-black pass, and it
     has the same fix: bring the highlight under the glow. The highlight now
     tops out around .44, below the body copy, which is below the filament.

     Grayscale comes *down* at the same time, .55 → .40. Draining the colour
     was never what kept the photograph in line — brightness was — and drained
     plus bright is exactly what reads as pallid. Darker and warmer is a
     photograph in a dark room; paler and greyer is a bad scan. What the
     desaturation still has to do is hold the red rock back from being a field
     of orange next to the tubes, and .40 with the light down does that on its
     own.

     A comic-halftone drawing was tried in this frame and taken back out. Worth
     knowing why the numbers were different: its peak was ordinary paper white,
     same as the sky here, but a *third* of its area sat above .548 against
     this photograph's small patch. Lit area, not peak, is what has to come
     under the glow, so it needed contrast(1.15) brightness(.66) — a harder cap
     than a photograph ever needs. */
  filter: grayscale(.4) contrast(1.18) brightness(.72);
}
.viewport-fallback {
  grid-area: 1/1;
  font: 400 2rem/1 var(--pix);
  letter-spacing: .1em;
  color: var(--anchor);
}
.plate-text { flex: 1 1 18rem; }


.plate-text h1 {
  font: 400 clamp(1.5rem, 5.4vw, 2.05rem)/1.15 var(--pix);
  letter-spacing: .03em;
  color: var(--text);
}

/* The two-part stencil under the name, the way a rating plate splits
   model from class. */
.stencil {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s3);
  margin-top: var(--s4);
}
/* Every small-caps chip on the site is the same object: a hairline box with
   nothing inside it. No fill, no bevel, no inner highlight — on black, one
   line is already enough to read as a discrete thing, and a second one is
   decoration. */
.stencil span {
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding: .45rem .55rem;
  border: 1px solid var(--rule);
}

.plate-where { margin-top: var(--s4); color: var(--text-dim); font-size: .875rem; }
.plate-mail  { color: var(--text-dim); font-size: .875rem; }

.ports {
  display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5);
  margin-top: var(--s4);
}
.ports a {
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-dim);
  border-bottom: 1px solid var(--anchor);
  padding-bottom: 3px;
}
.ports a:hover { color: var(--glow); border-bottom-color: var(--glow); }

/* --- Prose ---------------------------------------------------------------- */

.prose p + p { margin-top: var(--s4); }
.prose { color: var(--text-dim); }
.prose strong { color: var(--text); font-weight: 500; }

.tags { display: flex; flex-wrap: wrap; gap: var(--s2); margin-top: var(--s5); }
.tags li {
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-dim);
  padding: .5rem .6rem;
  border: 1px solid var(--rule);
}

/* --- Row lists ------------------------------------------------------------ */

.row {
  display: grid;
  grid-template-columns: 12rem 1fr;
  gap: var(--s2) var(--s5);
  padding: var(--s4) 0;
  border-top: 1px solid var(--rule);
}
.row dt {
  font: 400 .75rem/1.5 var(--pix);
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text);
}
.row dd { color: var(--text-dim); }

.more {
  display: inline-block;
  margin-top: var(--s5);
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--text-dim);
  padding: .55rem .7rem;
  border: 1px solid var(--rule);
  transition: color 120ms linear, border-color 120ms linear;
}
.more:hover { color: var(--glow); border-color: rgba(255,154,61,.4); }
.more:active { transform: translateY(1px); }

/* --- The lit section ------------------------------------------------------ */
/* Nothing. This used to wash the surrounding metal with the light the tubes
   throw, which was needed when there was metal to wash. Against pure black a
   gradient bounded by the section box reads as a faint rectangle no matter how
   soft it is — the panel coming back in through the side door. The tubes carry
   their own halo and do not need the room lit for them. */

.log li {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--s2) var(--s5);
  align-items: start;
  padding: var(--s5) 0;
  border-top: 1px solid var(--rule);
}
.log p { color: var(--text-dim); padding-top: .35rem; }
.log strong { color: var(--text); font-weight: 500; }

/* --- Nixie tube ----------------------------------------------------------- */
/* Four layers, back to front:
     1. glass envelope  — cylinder shading, painted on .tube itself
     2. dead cathodes   — <s>, plus its ::after: the digits NOT selected
     3. lit cathode     — <i>
     4. anode mesh      — .tube::before, a cross-hatch IN FRONT of the digit
   Layer 2 is the one that does the work. A lit digit on a dark ground is an
   orange number; a lit digit with two unlit ones hanging behind it at
   different depths is a tube. */

.tubes {
  /* Tube geometry lives here, not on .tube, so the separator pip between
     year and month can read the same height its neighbours got. */
  --tw: 1.6rem; --th: 2.55rem; --tf: 1.5rem;
  display: inline-flex;
  align-items: flex-end;
  gap: .34rem;
  /* No socket strip, no housing, no box. The tubes stand in the panel itself.
     Everything that used to draw the enclosure — the padding, the seated
     gradient, the three shadow layers — is deliberately absent: a rectangle
     around a light source is the one thing that stops it reading as light. */
}

.tube {
  position: relative;
  display: grid;
  width: var(--tw); height: var(--th);
  /* No radius, no ring, no drop shadow — every one of those draws an edge,
     and an edge is a frame. What is left is a soft column of glass that
     never finds a boundary, and the discharge landing on the metal behind
     it. Both reach `transparent` before they reach the element's box, so
     there is nothing rectangular anywhere in the stack to see.

     Still deliberately NOT overflow:hidden: the bloom has to spill past the
     envelope, which is most of why it reads as light rather than as ink. */
  background:
    /* the cylinder: lit down one flank, shaded down the other, fading out */
    radial-gradient(ellipse 62% 78% at 34% 40%,
      rgba(255,255,255,.055) 0%,
      rgba(255,255,255,.015) 40%,
      rgba(0,0,0,.34) 72%,
      transparent 100%),
    /* the halo the discharge leaves in the air around the envelope */
    radial-gradient(ellipse 108% 86% at 50% 52%,
      rgba(255,154,61,.14) 0%,
      rgba(255,122,28,.055) 42%,
      transparent 78%);
}
.tube > *,
.tube::before,
.tube::after {
  grid-area: 1 / 1;
  display: grid;
  place-items: center;
}

/* Layer 2 — the cathodes that are not selected. <s> is being used as a bare
   hook for this layer, so its default strike has to come off. */
.tube s {
  position: relative;
  z-index: 1;
  text-decoration: none;
  font: 300 var(--tf)/1 var(--mono);
  color: var(--cathode);
  transform: translate(-1px, -2px) scale(1.05);
}
.tube s::after {
  content: "0";
  position: absolute; inset: 0;
  display: grid; place-items: center;
  transform: translate(3px, 2px) scale(.96);
  opacity: .55;
}

/* Layer 3 — the wire that is actually glowing. */
.tube i {
  z-index: 2;
  font: 300 var(--tf)/1 var(--mono);
  font-style: normal;
  color: var(--glow-core);
  text-shadow:
    0 0 1px var(--glow-hot),
    0 0 5px rgba(255,190,120,.95),
    0 0 13px rgba(255,154,61,.8),
    0 0 28px rgba(255,154,61,.5),
    0 0 52px rgba(255,130,30,.3);
  animation: breathe 4s ease-in-out infinite;
}

/* Layer 4 — the anode mesh sits in front of everything, which is why a real
   tube's digits always look very slightly screened. */
.tube::before {
  content: "";
  z-index: 3;
  background-image:
    repeating-linear-gradient(46deg,  rgba(0,0,0,.30) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(-46deg, rgba(0,0,0,.24) 0 1px, transparent 1px 5px);
  /* The mesh has to stop the way the glass does. Clipped to a rounded box it
     ended on a visible corner; faded out radially it just runs out, which is
     what looking through the front of a cylinder actually does. */
  -webkit-mask-image: radial-gradient(ellipse 58% 74% at 44% 44%, #000 34%, transparent 84%);
          mask-image: radial-gradient(ellipse 58% 74% at 44% 44%, #000 34%, transparent 84%);
}
/* and the specular arc on the glass, off-centre so the cylinder reads round */
.tube::after {
  content: "";
  z-index: 4;
  background:
    radial-gradient(30% 20% at 27% 13%, rgba(255,255,255,.16), transparent 72%),
    radial-gradient(14% 32% at 84% 60%, rgba(255,255,255,.05), transparent 74%);
}

/* The neon dot between year and month — the same discharge, one pip of it. */
.sep {
  width: .45rem; height: var(--th, 2.55rem);
  display: grid; place-items: center;
}
.sep::after {
  content: "";
  width: 3px; height: 3px; border-radius: 50%;
  background: var(--glow);
  box-shadow: 0 0 5px rgba(255,190,120,1), 0 0 12px rgba(255,154,61,.85), 0 0 24px rgba(255,154,61,.45);
  transform: translateY(.5rem);
  animation: breathe 4s ease-in-out infinite;
}

.readout--sm .tubes {
  --tw: 1.05rem; --th: 1.7rem; --tf: 1rem;
  gap: .24rem;
}

/* Cathode wear. No two tubes in a display ever match: the cathodes erode at
   different rates and no driver divides current perfectly, so a row that is
   uniform to the pixel is the tell of a render. The variance is in amplitude
   only — the phase stays locked, because they are all on one supply and a row
   that breathed out of step would read as four machines instead of one. */
.tube:nth-child(2) i { opacity: .945; }
.tube:nth-child(3) i { opacity: .99;  }
.tube:nth-child(4) i { opacity: .915; }
.tube:nth-child(5) i { opacity: .97;  }

/* --- Footer --------------------------------------------------------------- */

.unit--foot { padding-left: var(--s5); }
.unit--foot .unit-body { max-width: none; }

.foot-grid {
  display: flex; flex-wrap: wrap; gap: var(--s5);
  align-items: flex-start; justify-content: space-between;
}
.foot-name {
  font: 400 .8125rem/1 var(--pix);
  letter-spacing: .14em; text-transform: uppercase;
}
.foot-where { color: var(--text-dim); font-size: .875rem; margin-top: var(--s2); }
.unit--foot .ports { margin-top: 0; }

/* --- The coda: lyric left, display module right --------------------------- */
/* The seam runs the full width of the panel and both things hang off it, so
   the module reads as mounted on the same face as the text rather than as an
   illustration that happens to be nearby. */

.foot-coda {
  display: flex; align-items: flex-start; gap: var(--s6);
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: 1px solid var(--rule);
}

.lyric {
  flex: 1 1 20rem;
  min-width: 0;
  color: var(--text-dim);
}
.lyric p { font-style: italic; }
.lyric cite {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
  margin-top: var(--s4);
  font-size: .875rem; font-style: normal;
  color: var(--text-dim);
}

.colophon {
  margin-top: var(--s5);
  font: 400 .625rem/1.6 var(--pix);
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-faint);
}
/* --- The owl display module ----------------------------------------------- */
/* Unframed, like everything else here — this component brings its own glass
   and its own pins, and a housing drawn around it would be a housing inside a
   housing.

   Screen blending is what makes that possible: the photograph's near-black
   surround blends away, so only the discharge and the glass rim survive, and
   the module reads as lit rather than as a picture of something lit. It also
   means the image needs no cut-out and no alpha. */

.owl-module {
  position: relative;
  flex: none;
  width: 14rem;
  margin-left: auto;
  /* The photograph carries a wide margin of dead black on every side. Pulled
     tight the component would sit marooned in the middle of its own frame, so
     the margin is cropped back with negative space on all four edges. */
  margin: -2.25rem -1rem -2.75rem 0;
}
.owl-module img {
  display: block;
  width: 100%; height: auto;
  mix-blend-mode: screen;
  /* Against a dark chassis the photograph's surround screened away to nothing
     on its own. Against pure black there is nothing left to screen *into*, so
     the surround is now the lightest thing in its own rectangle and the crop
     shows. The contrast lift crushes it back toward zero and the radial mask
     takes the corners, so what is left is the component and its glow. */
  filter: brightness(1.05) contrast(1.22);
  -webkit-mask-image: radial-gradient(ellipse 64% 68% at 50% 47%, #000 46%, transparent 88%);
          mask-image: radial-gradient(ellipse 64% 68% at 50% 47%, #000 46%, transparent 88%);
  /* Same 4s cycle as the tubes and the power lamp: everything lit on this
     page is on one supply, so it all breathes together or the illusion goes. */
  animation: breathe 4s ease-in-out infinite;
}

@media (max-width: 46rem) {
  .foot-coda { flex-wrap: wrap; gap: var(--s4); }
  .owl-module { width: 11rem; margin: -1.5rem auto -2rem; }
}

/* --- Motion --------------------------------------------------------------- */
/* One roll on entry, then a breath you are not meant to notice — just enough
   that the panel is powered rather than printed. */

@keyframes breathe {
  0%, 100% { opacity: 1; }
  50%      { opacity: .965; }
}

/* Gas does not strike cleanly. For the fraction of a second a cathode is
   being switched the discharge has not settled, so the digit runs dim and
   unstable — then hands straight back to `breathe` the moment it lands.
   steps() rather than a fade, for the same reason the roll is a hard cut:
   there is no dimmer in a nixie, only struck and not struck. */
@keyframes strike {
  0%   { opacity: .5;  }
  30%  { opacity: 1;   }
  55%  { opacity: .68; }
  80%  { opacity: .94; }
  100% { opacity: .78; }
}
.tube i.is-rolling { animation: strike 150ms steps(5, jump-none) infinite; }

@media (prefers-reduced-motion: reduce) {
  .tube i, .sep::after, .lamp, .owl-module img { animation: none; }
  * { transition-duration: .01ms !important; }
}

/* Navigation is not animated. The mechanics belong to the switches you throw,
   not to the page arriving — a panel does not wipe, it is simply on. */

/* --- Narrow --------------------------------------------------------------- */

@media (max-width: 46rem) {
  /* The vertical rail costs too much width on a phone, so the label lies
     down above the unit instead. */
  .unit { padding: var(--s6) var(--s4) var(--s5); }
  .unit-tag {
    position: static;
    width: auto;
    writing-mode: horizontal-tb;
    transform: none;
    justify-items: start;
    padding-bottom: var(--s4);
  }

  .row { grid-template-columns: 1fr; }
  .log li { grid-template-columns: 1fr; }

  .legend-toggle { display: inline-block; }
  .legend-nav { display: none; flex-basis: 100%; }
  .legend-nav.is-open { display: block; }
  /* Stacked, but still five separate switches — gap: 0 would weld the housings
     into one strip. */
  .legend-nav ul { flex-direction: column; gap: var(--s4); align-items: stretch; }

  .tube { --tw: 1.35rem; --th: 2.15rem; --tf: 1.25rem; }
}

/* ==========================================================================
   Interior panels
   Parts that only the pages below the homepage need. Same three materials —
   metal, silkscreen, one light — so nothing here introduces a new idea.
   ========================================================================== */

/* --- Page head ------------------------------------------------------------ */
/* The masthead of an interior page. It used to be a brighter plate; now it is
   simply the block with the most air around it, which reads as the top of the
   page for the same reason a title does in print. */

.unit--head {
  padding-top: var(--s7);
  padding-bottom: var(--s7);
}
.unit--head h1 {
  font: 400 clamp(1.35rem, 4.4vw, 1.95rem)/1.25 var(--pix);
  letter-spacing: .03em;
  color: var(--text);
}
.unit--head .lede {
  max-width: 54ch;
  margin-top: var(--s4);
  color: var(--text-dim);
}
.head-links { display: flex; flex-wrap: wrap; gap: var(--s2) var(--s5); margin-top: var(--s5); }
.head-links a {
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .14em; text-transform: uppercase; text-decoration: none;
  color: var(--text-dim);
  border-bottom: 1px solid var(--anchor);
  padding-bottom: 3px;
}
.head-links a:hover { color: var(--glow); border-bottom-color: var(--glow); }

/* An expected-completion readout, the one number a committee looks for. */
.head-readout { display: flex; align-items: center; gap: var(--s4); margin-top: var(--s5); }
.head-readout .cap {
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .16em; text-transform: uppercase; color: var(--text-dim);
}

/* --- Year band ------------------------------------------------------------ */
/* The year is stated once at the head of its group instead of repeating down
   every row. Nine lit readouts on one page is wallpaper; four is a scale. */

.band {
  display: flex; align-items: center; gap: var(--s4);
  padding: var(--s6) 0 var(--s4);
}
.band:first-child { padding-top: 0; }
.band-rule { flex: 1; height: 1px; background: var(--rule); }

/* --- Shared row seam ------------------------------------------------------ */

.seam {
  padding: var(--s5) 0;
  border-top: 1px solid var(--rule);
}

/* --- Bulleted detail ------------------------------------------------------ */
/* A pixel caret rather than a disc: a disc is a typographic bullet and this
   page has no typography that is not stencilled or machined. */

.detail li {
  position: relative;
  padding-left: 1.15rem;
  color: var(--text-dim);
}
.detail li + li { margin-top: var(--s3); }
.detail li::before {
  content: "\25B8";
  position: absolute; left: 0; top: 0;
  color: var(--rust);
}
.detail strong, .prose strong, .pubs strong, .entry strong { color: var(--text); font-weight: 500; }

/* --- Research projects ---------------------------------------------------- */

.proj-name {
  font: 400 .8125rem/1.5 var(--pix);
  letter-spacing: .08em; text-transform: uppercase; color: var(--text);
}
.proj-meta {
  font: 400 .625rem/1.6 var(--pix);
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-dim);
  margin-top: var(--s2);
}
.projects p.body { color: var(--text-dim); margin-top: var(--s3); }
.projects .detail { margin-top: var(--s3); }

/* --- Publications --------------------------------------------------------- */

.pub-title { font: 500 .9375rem/1.6 var(--mono); color: var(--text); }
.pub-title a { color: inherit; }
.pub-authors { color: var(--text-dim); font-size: .875rem; margin-top: var(--s2); }
.pub-venue {
  font: 400 .625rem/1.6 var(--pix);
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-dim);
  margin-top: var(--s2);
}

/* --- Filter switches ------------------------------------------------------ */
/* Panel switches, so each one carries its own lamp. Pressed state is the lamp
   plus a pushed-in shadow, never colour alone. */

.filters {
  display: flex; align-items: center; flex-wrap: wrap; gap: var(--s3);
  padding-bottom: var(--s5);
}
.filters-label {
  font: 400 .625rem/1 var(--pix);
  letter-spacing: .18em; text-transform: uppercase; color: var(--text-dim);
  margin-right: var(--s2);
}
.switch {
  display: inline-flex; align-items: center; gap: var(--s3);
  font: 400 .6875rem/1 var(--pix);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--text-dim);
  padding: .55rem .7rem;
  cursor: pointer;
  background: none;
  border: 1px solid var(--rule);
  transition: color 120ms linear, border-color 120ms linear;
}
.switch::before {
  content: "";
  width: 5px; height: 5px; border-radius: 50%;
  background: #26241F;
}
.switch:hover { color: var(--text); }
/* Key travel. A real panel switch moves under the finger before it latches, and
   1px of it is enough to feel — any more and it reads as a bouncing web button.
   No transition on the way down: a detent is instant or it is not a detent. */
.switch:active { transform: translateY(1px); }
.switch[aria-pressed="true"] {
  color: var(--text);
  border-color: rgba(255,154,61,.38);
}
.switch[aria-pressed="true"]::before {
  background: var(--glow);
  box-shadow: 0 0 4px rgba(255,190,120,1), 0 0 10px rgba(255,154,61,.7);
}

/* --- CV entries ----------------------------------------------------------- */

.entry-head {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--s4); flex-wrap: wrap;
}
.entry-role {
  font: 400 .8125rem/1.5 var(--pix);
  letter-spacing: .08em; text-transform: uppercase; color: var(--text);
}
.entry-when {
  font: 400 .625rem/1.6 var(--pix);
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-dim);
  flex: none;
}
.entry-org  { color: var(--text-dim); margin-top: var(--s2); }
.entry-note { color: var(--text-dim); font-size: .875rem; }
.entry .detail { margin-top: var(--s3); }

/* --- Plain list with a right-hand date ------------------------------------ */

.plain li {
  display: flex; justify-content: space-between; align-items: baseline;
  gap: var(--s5);
  padding: var(--s4) 0;
  border-top: 1px solid var(--rule);
  color: var(--text-dim);
}
.plain .when {
  flex: none;
  font: 400 .625rem/1.6 var(--pix);
  letter-spacing: .14em; text-transform: uppercase; color: var(--text-dim);
}

/* --- The CV itself -------------------------------------------------------- */
/* Same hairline as the portrait. The document is white and will hold its own
   edge against black without any help. */

.pdf-frame {
  position: relative;
  background: #000;
  box-shadow: 0 0 0 1px var(--rule);
  padding: 10px;
}
/* The opening is cut to the shape of the document, not to the viewport. With
   #view=FitH the viewer scales the page to the frame's width, so a US Letter
   ratio means exactly one full page fills the glass — no letterbox above and
   below it, and no second page peeking in at the bottom edge. A vh height
   could not do that: it is right at one window size and wrong at every other. */
.pdf-frame object {
  display: block; border: 0;
  width: 100%;
  aspect-ratio: 8.5 / 11;
  min-height: 32rem;
}
.pdf-frame p { padding: var(--s6); color: var(--text-dim); text-align: center; }
.pdf-note {
  margin-top: var(--s4);
  font: 400 .625rem/1.7 var(--pix);
  letter-spacing: .12em; text-transform: uppercase; color: var(--text-dim);
}

/* A phone will not usefully embed a PDF, and neither will a browser with its
   viewer switched off. Either way the transcription below is the page. */
@media (max-width: 46rem) {
  .pdf-frame object { display: none; }
  .pdf-frame p { display: block; }
}
html[data-no-pdf] .pdf-frame object { display: none; }
html[data-no-pdf] .pdf-frame p { display: block; }

/* --- Wide units ----------------------------------------------------------- */
/* Publications and the CV carry long lines that a 64ch measure would break
   into ribbons. */

.unit--wide .unit-body { max-width: 78ch; }

/* The CV sheet is not prose, so no measure applies to it — it takes the whole
   width the rack has left after the rail. */
.unit--sheet .unit-body { max-width: none; }

/* --- Skills spec table ----------------------------------------------------- */
/* Read as a spec plate: term stencilled on the left rail, value in mono beside
   it, so it scans like the label on the back of a chassis. */

.specs > div { display: grid; grid-template-columns: 13rem 1fr; gap: var(--s5); }
.specs dt {
  font: 400 .6875rem/1.7 var(--pix);
  letter-spacing: .14em; text-transform: uppercase; color: var(--text);
}
.specs dd { color: var(--text-dim); font-size: .875rem; }

@media (max-width: 46rem) {
  .specs > div { grid-template-columns: 1fr; gap: var(--s3); }
}

/* --- First row under a band ------------------------------------------------ */
/* The band already draws a rule. A seam border immediately below it would
   double the line, so the first row in each group goes without one. */

.band + ol > .seam:first-child,
.band + ul > .seam:first-child,
.unit-h + .entries > .seam:first-child,
.unit-h + .projects > .seam:first-child,
.prose + .projects > .seam:first-child {
  border-top: 0;
}
.unit-h + .plain > li:first-child,
.unit-h + .specs > div:first-child { border-top: 0; }

/* --- Filtered-out state ---------------------------------------------------- */
/* A whole year disappears when nothing in it survives the filter — an empty
   band would advertise a group that is no longer there. */

[hidden] { display: none !important; }

/* Filtering can hide the row the rule above was suppressed on, so the script
   marks whichever row is first *after* filtering. */
.seam.is-first { border-top: 0; }


/* ── Two-column summary rows (home) ─────────────────────────────────── */
.rows--cols { display: grid; grid-template-columns: 1fr 1fr; column-gap: var(--s6); }
.rows--cols .row { grid-template-columns: 1fr; gap: var(--s1) 0; align-content: start; }
.rows--cols .row dt { margin-bottom: 2px; }
@media (max-width: 46rem) { .rows--cols { grid-template-columns: 1fr; } }

/* ── At-a-glance rail ───────────────────────────────────────────────── */
/* A page-level right rail on wide screens: the main column narrows, the
   glance column stands beside it, and its inner block rides the scroll
   (sticky under the legend). Below the breakpoint it folds back into the
   flow after the about unit. */
@media (min-width: 64rem) {
  main#main { display: grid; grid-template-columns: minmax(0, 1fr) 15rem; column-gap: var(--s7); }
  main#main > .unit { grid-column: 1; }
  main#main > .glance { grid-column: 2; grid-row: 1 / span 20; }
  .glance-in {
    position: sticky; top: 5rem;
    padding-top: var(--s6);
    max-height: calc(100vh - 6.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--rule) transparent;
  }
  .glance-in::-webkit-scrollbar { width: 2px; }
  .glance-in::-webkit-scrollbar-thumb { background: var(--rule); }
  .glance-in::-webkit-scrollbar-track { background: transparent; }
}
@media (max-width: 63.98rem) {
  .glance-in { padding: var(--s5) 0 0; }
}
.aside-block { border-top: 1px solid var(--rule); padding-top: var(--s3); }
.aside-block + .aside-block { margin-top: var(--s5); }
.aside-label { font: 400 .5625rem/1 var(--pix); letter-spacing: .18em; text-transform: uppercase; color: var(--label); margin: 0 0 var(--s3); }
.aside-list { list-style: none; margin: 0; padding: 0; }
.aside-list li { font-size: .875rem; color: var(--text-dim); padding: var(--s1) 0; }
.aside-list li strong { color: var(--text); font-weight: 500; }
.aside-list li .cap { display: block; color: var(--text-faint); font-size: .8125rem; }
