/* fubl.org — the one file this site owns.
 *
 * PREPRINT is vendored under assets/preprint/ and is never edited. The site
 * loads the system in three layers and then this file:
 *
 *   <link rel="stylesheet" href="/assets/preprint/styles.css">    tokens
 *   <link rel="stylesheet" href="/assets/preprint/core.css">      every surface
 *   <link rel="stylesheet" href="/assets/preprint/reading.css">   what the renderer emits
 *   <link rel="stylesheet" href="/assets/site.css">               this file
 *
 * What is left here is fubl.org and not PREPRINT: the masthead and its scrawl,
 * the four doors, the index, the reading page's shell (top bar, contents rail,
 * progress), the CV, the print sheet, and the pointer wiring. The reading
 * CONTENT — the row grid, the article, sidenotes, figures, tables, code, the
 * marker layer — moved into the system, where a second site can have it.
 *
 * Nothing below reads a colour, a face or a control height that is not a
 * token, with two declared exceptions.
 *
 * --p-text, the pink the header scrawl is drawn in. It is named in the handoff
 * and used in the reference design, and it is not a plate, not a marker and
 * not a fourth ink — it is a hand-drawn annotation on the masthead and it
 * appears exactly once.
 *
 * --citron-ink, the fixed dark the workshop button is drawn in. That button is
 * the one object on the site that does NOT follow the mode, so the ink on it
 * cannot come from --pp-ink, which inverts. See .btn--citron.
 */

:root {
  --p-text: #c9146e;

  /* THE DOORS' RUNG. The system's prominent rung is 52px and this site's doors
     have always been 46. Overriding a non-invariant token is what invariants.md
     says a surface may do, so long as it says why: 52 is sized for a console's
     primary action, and a door is not that. It is a destination the reader may
     take or ignore, sitting under a name that is already the loudest thing on
     the page, and at 52 it competes with the name rather than hanging off it.

     Both endpoints are set, not the rung itself. Setting the rung would win
     over the system's narrow step — an unconditional value in the last sheet
     beats a media block that carries no extra specificity — and the doors
     would be 46 on a phone, which is where they are least affordable. */
  --pp-target-prominent-wide: 46px;
  --pp-target-prominent-narrow: 44px;

  /* 88ch on the index, and one column wider on a reading page, where the
     contents rail sits outside the text. */
  --site-page: 88ch;

  /* The rail was a flat 17ch and read as an afterthought on a wide screen: a
     column of 12px type beside 18px text, with four hundred pixels of empty
     glass to the right of the page it was apologising to. It grows now, but
     only out of space the window actually has.

     The floor is the old 17ch and the vw term does not clear it until 1280px,
     so every width at or below a laptop keeps exactly the layout it had. Above
     that the rail takes a share of the widening and the measure beside it is
     untouched, because the shell is rail plus gap plus measure and the shell is
     what grows. The cap is 25ch, which is about as wide as a section title
     wants to be set before it stops being a label and starts being a line. */
  --site-rail: clamp(17ch, 13vw, 25ch);
  --site-gap: clamp(1rem, 3vw, 2.25rem);
  --site-gutter: clamp(1.15rem, 5vw, 3rem);

  /* The band every page is set on: rail, gap, measure. Declared once here so
     the index and a piece cannot drift apart — the shared measure is the whole
     reason the two read as one document, and it used to be restated in four
     places that were free to disagree. */
  --site-shell: calc(var(--site-rail) + var(--site-gap) + var(--site-page));

  /* THE LEAN. Both gutters are free space, and the right one is given more of
     it, so the page sits left of centre and drifts further left the wider the
     window gets. Weights, not lengths, because that is what makes it
     progressive: at 1200px there is almost no free space to divide and the
     lean is invisible, at 3440px there is 2000px of it and the page is
     decisively left. It is also self-limiting on the narrow end — below the
     shell width both columns hit their `--site-gutter` floor and the layout is
     symmetric again, with no breakpoint needed to say so.

     1.85 is the largest lean that still reads as a margin. Past about 2.5 the
     page stops looking placed and starts looking dropped. */
  --site-lead: 1fr;
  --site-trail: 1.85fr;
}

[data-mode='dark'] { --p-text: #ff8fc4; }
html { scroll-behavior: smooth; scroll-padding-top: 1.5rem; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }
/* The system underlines every link. On this site most links are structural —
   a row, a button, an icon — so the underline is opted into, not out of. */
a { color: inherit; }
a:hover { color: var(--pp-plate-3-text); }
.sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

/* Keyboard only, so the floor is not the argument — but it is a control and it
   sits on the ladder with the rest rather than on a number of its own. */
.skip {
  min-height: var(--pp-target-compact);
  position: absolute; left: -9999px; top: 0; z-index: 60;
  padding: .6rem 1rem; background: var(--pp-ink); color: var(--pp-paper);
  font-family: var(--pp-font-mono); font-size: var(--pp-size-label);
  letter-spacing: .1em; text-transform: uppercase;
}
.skip:focus { left: 0; }
/* Every band on the page is placed by this one grid, and nothing below centres
   itself. That is the point: five containers used to each carry their own
   `max-width` and `margin: 0 auto`, so the lean would have had to be written
   five times and any one of them left behind would have sat centred while the
   rest leaned. Now the shell is declared here and the bands are placed in it.

   The gutters are grid columns rather than padding, because free space that is
   a column can be weighted and free space that is padding cannot. */
.page {
  min-height: 100vh; padding-bottom: clamp(3rem, 8vw, 5rem);
  display: grid; align-content: start;
  grid-template-columns:
    [full-start] minmax(var(--site-gutter), var(--site-lead))
    [shell-start] min(100% - var(--site-gutter) * 2, var(--site-shell)) [shell-end]
    minmax(var(--site-gutter), var(--site-trail)) [full-end];
}
.page > * { grid-column: shell; }
/* ══ THE HEADER ════════════════════════════════════════════════════════════
   Two compact lines, and no nav of any kind. The page is short enough to
   scroll and the scroll is the navigation. */

/* The masthead is the one band that is NOT set on the 88ch measure. Everything
   below it is, because a shared measure is what makes the index and the pieces
   read as one document, but the masthead is not reading matter: it is the name
   and the doors out of the site, and holding it to a column of body text left
   the buttons bunched in the middle with empty page either side.

   That reasoning was about a CENTRED page, and it does not survive the lean.
   Bunching was what happened when an 88ch column sat in the middle of a wide
   window with dead space on both sides; the masthead escaped it by running
   gutter to gutter. Nothing is centred now, and the shell it would be held to
   is rail plus gap plus measure, which is half again the 88ch the shelf was
   drawn for. There is nothing left to escape.

   So it takes the shell like every other band, and the page gets one left edge
   and one right edge from the wordmark down to the colophon. Letting it keep
   running to `full-end` was tried first and is worse at width: the buttons end
   up flush against the glass, which is the exact failure the paragraph above
   was written to prevent, and a masthead a thousand pixels wider than the text
   under it stops reading as the page's head and starts reading as a bar. */
.head {
  padding: clamp(1.75rem, 6vh, 3rem) 0 clamp(1rem, 2.5vw, 1.4rem);
}
/* The name at one end and the doors at the other, which only means anything
   now that there is a width to push them apart across. */
.head__shelf { justify-content: space-between; }

.head__shelf {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: .75rem clamp(1rem, 4vw, 2.5rem);
}

/* The mark is the F. There is no logo, and this system does not draw one. */
.wordmark {
  font-size: clamp(2.1rem, 6.5vw, 3.4rem); font-weight: 900;
  letter-spacing: -0.05em; line-height: 1; text-wrap: balance;
}
.wordmark__f {
  color: var(--pp-plate-2);
  -webkit-text-stroke: 2px var(--pp-plate-3);
  paint-order: stroke fill;
  text-shadow: .05em .05em 0 var(--pp-plate-1);
}

.head__buttons { position: relative; display: grid; justify-items: start; }
.head__row { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .85rem; }

/* The scrawl's two arrangements are the system's (core.css) and so is the
   switch between them. What is left here is the drawing: the ink, the tilt,
   and where the arrow starts.

   It used to be dropped outright below 958px, on the grounds that an arrow
   pointing at a button one line below it is not an aside. That was true of the
   arrangement it had. Stacked it points at a button that is now a single
   lettermark, and the words are the only thing on the page saying what that
   letter is, so the annotation stopped being decoration and started being the
   label. */
.scrawl { color: var(--p-text); font-size: .94rem; letter-spacing: -0.01em; }
/* THE ARROW STOPS SHORT, in both arrangements, by about ten pixels of clear
   ground between the head and the door's edge. A hand annotation that runs into
   the thing it points at reads as a line connecting two objects, which is a
   diagram; held off, it reads as a gesture towards one object, which is what a
   scrawl is. Ten is the smallest gap that still reads as deliberate against a
   2px door border and a 3px cast.

   The two arrangements need different numbers for the same visible gap, and
   that is geometry rather than drift: hung, the svg is a block in a
   `line-height: 0` box so its own bottom edge is the scrawl's bottom edge;
   stacked, it is inline on a 1.25 line with a -.9em shift, and the line box
   closes below it. The head also sits about 4px up from the svg's own bottom,
   inside the box, in both. Measured rather than derived — see the note on each. */

/* Law 06's one rotation, and the page's only one: the sticker on a `soon` door
   would be the second, and those doors ship off. */
.scrawl--beside { left: 1.9rem; margin-bottom: .4rem; transform: rotate(-2deg); }

/* Stacked, the note sits directly over the door and the arrow drops out of the
   end of the sentence into it. Three things make that work.

   It is anchored to the door's right edge and grown leftwards (`right`, plus
   `width: max-content` so it may be wider than the 44px box it hangs off).
   The arrow is the last thing in the line box, so anchoring the right edge
   anchors the ARROW, and where the sentence happens to break stops mattering.
   That was the flaw in the first attempt: left-anchored, the arrow landed
   wherever the last word ended, which was 220px wide of the door.

   `text-align: right` for the same reason — the short last line is pulled
   under the arrow rather than trailing away from it.

   No mirror. The drawn arrow curves down-RIGHT, and from a note sitting above
   and slightly left of the door, down-right is where the door is. The flip
   this rule used to carry was compensating for the anchor being wrong.

   The margin holds the head about ten pixels clear of the door's top edge, the
   same stop the hung arrangement takes. It used to tuck six pixels INSIDE that
   edge, which made the arrow and the door read as one welded object rather
   than as a note about it. Held off the door it may still crowd the wordmark on
   its left: the shelf is cramped at this width by design, and an annotation
   that keeps a polite distance from everything is not a scrawl.

   The gate is a width and not `:not(.scrawl--beside)`. The modifier is in the
   markup at every width — it says this scrawl MAY hang — and the system is
   what decides whether it does, above 60rem. A `:not()` on it never matches. */
@media (max-width: 59.99rem) {
  .scrawl {
    right: .8rem; left: auto;
    width: max-content; max-width: min(24ch, 62vw);
    margin-bottom: .15rem; text-align: right; line-height: 1.25;
  }
  .scrawl svg { vertical-align: -.9em; margin-left: .1rem; }

  /* The note is pulled up out of the shelf and the shelf's top padding is the
     only thing above it. That padding is a `6vh` clamp, so on a short screen
     (a phone held sideways) it collapses to 28px and the note's first line
     leaves the top of the page. The floor here is measured against the note at
     its tallest, which is two lines plus the arrow's descent. */
  .head:has(.scrawl) { padding-top: max(clamp(1.75rem, 6vh, 3rem), 4.75rem); }
}

.btn {
  gap: .55rem; min-height: var(--pp-target-prominent);
  padding: .5rem .9rem; font-family: var(--pp-font-mono); font-size: .76rem;
  letter-spacing: .06em; font-weight: 700;
}

/* The lettermark a door collapses to. The letter is the destination's own
   initial set in the display face at the display weight — the workshop's
   favicon is a Hepta Slab 900 W and this is the same letter from the same
   file, so the button a reader taps here and the tab it opens carry one mark.
   Size is em-based off the button, so a door that changes rung takes its
   letter with it. */
.btn__mark {
  font-family: var(--pp-font-display); font-weight: 900;
  font-size: 2.05em; letter-spacing: 0;
}
/* The one loud object, and the only citron on the public site. It presses
   onto its own shadow: a real displacement, not a colour fade. */
/* The GROUND does not follow the mode, and neither does the type on it. Citron
   is a MARKER in this system, and a marker is defined as a field that only ever
   sits behind ink: citron as type on eggshell measures 1.6:1, as a ground under
   ink 14:1 (see tokens/colors.css). Leaving the type on --pp-ink would flip it
   to #efeee8 at night and put pale letters on a near-white ground.

   The FRAME does follow the mode, like every other framed thing on the site.
   Pinning it dark made the border and the shadow disappear against the #141413
   ground, so at night the button lost the outline and the offset shadow that
   are the whole press affordance. A fixed ground does not oblige a fixed
   frame. */
.btn--citron {
  --citron-ink: #171716;
  background: #deee2e; color: var(--citron-ink);
  border: var(--pp-rule-structure) solid var(--pp-edge);
  box-shadow: 3px 3px 0 var(--pp-edge);
}
.btn--citron:hover, .btn--citron:active {
  color: var(--citron-ink);
  box-shadow: 1px 1px 0 var(--pp-edge);
  transform: translate(2px, 2px);
}

/* ── arcade.fubl.org ───────────────────────────────────────────────────────
   The one place on this site allowed to be loud in a way PREPRINT is not. It
   is a cabinet front: dark purple ground, hot pink tube, and the glow sits on
   the BORDER rather than on the type, because a neon text-shadow at 12px
   monospace turns the letters to mush. Pinned in both modes like the citron
   button, for the same reason — it is an identity, not a surface. */
.btn--arcade {
  --arcade-tube: #ff2d95;
  --arcade-deep: #2b0a3d;
  background: var(--arcade-deep); color: #ffe4f6;
  border: var(--pp-rule-structure) solid var(--arcade-tube);
  box-shadow: 3px 3px 0 var(--arcade-tube), 0 0 12px rgba(255, 45, 149, .45);
}
.btn--arcade .sq { background: var(--arcade-tube); }
.btn--arcade:hover:not([aria-disabled='true']),
.btn--arcade:active:not([aria-disabled='true']) {
  color: #fff;
  box-shadow: 1px 1px 0 var(--arcade-tube), 0 0 18px rgba(255, 45, 149, .7);
  transform: translate(2px, 2px);
}

/* ── decisions.fubl.org ────────────────────────────────────────────────────
   The same object as the other two, in plate 3 blue. It had its own type for a
   moment, smaller and letterspaced and uppercase, and that was the mistake: a
   door is a door. Four buttons in a row that disagree about their typeface do
   not read as a set of destinations, they read as four unrelated widgets. Only
   the colour changes. This one follows the mode, because unlike citron and the
   arcade tube it is not a fixed identity, it is the system's own blue. */
.btn--lab {
  background: var(--pp-surface); color: var(--pp-plate-3-text);
  border: var(--pp-rule-structure) solid var(--pp-plate-3);
  box-shadow: 3px 3px 0 var(--pp-plate-3);
}
.btn--lab .sq { background: var(--pp-plate-3); }
.btn--lab:hover:not([aria-disabled='true']),
.btn--lab:active:not([aria-disabled='true']) {
  box-shadow: 1px 1px 0 var(--pp-plate-3);
  transform: translate(2px, 2px);
}

/* The serious sibling. It does not exist yet, so it ships disabled with a
   sticker rather than as a link that lies. */
.btn-wrap { position: relative; display: inline-block; }
.btn--soon {
  border: var(--pp-rule-line) solid var(--pp-line); color: var(--pp-faint);
  cursor: not-allowed;
}
.btn--soon .sq { background: var(--pp-line); }
.sticker {
  position: absolute; top: -.5rem; right: -.7rem; transform: rotate(6deg);
  padding: .15rem .4rem; background: var(--pp-plate-2); color: #fff;
  font-family: var(--pp-font-mono); font-size: .58rem; letter-spacing: .1em;
  text-transform: uppercase; font-weight: 700; white-space: nowrap;
}

.head__bar {
  display: flex; flex-wrap: wrap; align-items: center; gap: .15rem 1.4rem;
  margin-top: clamp(.6rem, 2vw, 1rem); padding-top: .35rem;
  border-top: var(--pp-rule-line) solid var(--pp-line);
}

.socials { display: flex; align-items: center; gap: .1rem; margin-left: -.6rem; }
.soc {
  width: var(--pp-target); height: var(--pp-target);
  display: grid; place-items: center; color: var(--pp-muted);
}
.soc:hover { color: var(--pp-plate-3-text); }

/* ── the jump links ────────────────────────────────────────────────────────
   These are the only two links on the front page and they used to be set as
   small grey caps beside a number, which reads as a label for a count. A border,
   an arrow pointing the way they actually travel (down the page), and the count
   held in a filled tab: the same furniture as a button, because that is what
   they are. */
.jump {
  display: inline-flex; align-items: center; gap: .45rem; height: 34px;
  padding: 0 .3rem 0 .55rem; flex: none;
  border: var(--pp-rule-line) solid var(--pp-line);
  font-family: var(--pp-font-mono); font-size: .72rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--pp-muted);
}
.jump:hover { border-color: var(--pp-edge); color: var(--pp-ink); }
.jump svg { color: var(--pp-faint); }
.jump:hover svg { color: var(--pp-ink); }
.jump code {
  align-self: stretch; display: grid; place-items: center; min-width: 1.55rem;
  margin-left: .1rem; background: var(--pp-tint); color: var(--pp-muted);
  font-size: .68rem;
}
.jump:hover code { background: var(--pp-edge); color: var(--pp-paper); }
/* core.css fills it with edge and puts paper on top. The size, the tracking
   and the case are this site's. */
.btn--ink {
  min-height: var(--pp-target-compact); padding: 0 .75rem; gap: .45rem;
  font-size: .72rem; letter-spacing: .1em; text-transform: uppercase;
}
.btn--ink:hover {
  background: var(--pp-plate-3); border-color: var(--pp-plate-3); color: #fff;
}
.btn--ink .sq { width: .4rem; height: .4rem; }
/* An outlined button, for an action that is available rather than intended. */
.btn--ghost {
  min-height: var(--pp-target-compact); padding: 0 .7rem; gap: .45rem;
  border: var(--pp-rule-line) solid var(--pp-line); color: var(--pp-muted);
  font-size: .68rem; letter-spacing: .1em; text-transform: uppercase;
}
.btn--ghost:hover { border-color: var(--pp-edge); color: var(--pp-ink); }
.btn--ghost .sq { width: .35rem; height: .35rem; }

.head__end { display: flex; align-items: center; gap: 1rem; margin-left: auto; }

/* ── the mode switch ───────────────────────────────────────────────────────
   A rectangle three quarters full of ink. Both colours come from the mode, so
   in the light it is three quarters black on paper and in the dark it is three
   quarters white on the dark ground, with the ink block sitting low instead of
   high. It shows the mode you are IN. A lone sun or moon never resolves whether
   it is the state or the destination, and drawing both took two icons to say
   what one filled rectangle says by itself.

   The ink slides rather than jumps, and the whole switch takes a short squash
   on the way, which is the flick. It is 160ms and never repeats, because this
   is a control in a header and not something to watch. */
/* The fill is an ordinary flex child moved by `transform`, NOT an absolutely
   positioned one. A <button> wraps its children in an anonymous box that does
   not act as a containing block the way `position: relative` on the button
   implies, so an absolute child silently lands on its static position and
   `top` does nothing. Measured, not guessed: it sat at 8px in both modes.
   Translating the block by a third of its own height moves it exactly the
   quarter of the switch that is left over, and animates on the compositor. */
/* The button is the hit box and the box inside it is the drawing. They used to
   be the same element, which made the smallest target on the site the one
   control a reader is most likely to want on a phone: 26x34, against a floor
   of 44. Growing the rectangle was not an option — it is a swatch of the mode,
   and a swatch reads as a swatch at swatch size — so the target grew around it
   instead, and nothing visible moved. The overflow that clips the sliding
   block has to travel with the drawing, which is why this is a wrapper and not
   padding: padding on the button would have been clipped away with it. */
.modeswitch {
  display: grid; place-items: center;
  width: var(--pp-target); height: var(--pp-target); flex: none; padding: 0;
}
.modeswitch__box {
  display: flex; flex-direction: column;
  width: 26px; height: 34px;
  border: var(--pp-rule-line) solid var(--pp-line);
  background: var(--pp-paper); overflow: hidden;
}
.modeswitch:hover .modeswitch__box { border-color: var(--pp-edge); }

/* The block slides and does nothing else. It had a squash on the way, which
   read as bounce and made a two-position control look springy, and before that
   the squash sat on the button and took the border with it. One property
   changes: the block's vertical offset. `translate` rather than `transform`
   because it is the narrower statement and nothing else needs to compose. */
.modeswitch__fill {
  height: 75%; flex: none; background: var(--pp-ink);
  transition: translate var(--pp-dur-state) var(--pp-ease-out);
}
/* The pre-paint script always writes one or the other, so the block is on the
   correct side from the first frame and there is no flash of a switch set to
   neither. */
[data-mode='dark'] .modeswitch__fill { translate: 0 33.3333%; }

/* It arrives instantly instead of sliding. Nothing else changes. */
@media (prefers-reduced-motion: reduce) {
  .modeswitch__fill { transition: none; }
}
/* ══ THE INDEX ═════════════════════════════════════════════════════════════ */

/* The index is set on the same two columns as a piece: a rail on the left, the
   content beside it. It had no rail before and a piece did, which is the one
   place the two surfaces visibly disagreed.

   The sections live in a wrapper rather than as direct children. A rail that is
   a grid item can only be as tall as its own row, so making it span the whole
   column means `grid-row: 1 / -1`, and `-1` is the last EXPLICIT line — with
   auto-placed sections that is row 1, and the rail collapses beside the first
   section. One wrapper is cheaper than the row arithmetic and does not break
   when a section is added. */
.main {
  display: grid; align-items: start;
  grid-template-columns: var(--site-rail) minmax(0, 1fr);
  gap: var(--site-gap);
}
.main__body { display: grid; gap: clamp(2rem, 5vw, 3.25rem); min-width: 0; }

/* The index's contents. The reading page's rail lists the sections of a piece
   and how far through each one you are; this lists the sections of the site and
   what is in them. Same column, same job, so it takes the same width. */
.index-rail {
  position: sticky; top: 1.5rem; align-self: start;
  display: grid; gap: 1.5rem;
  max-height: calc(100vh - 3rem); overflow-y: auto;
}
.index-rail__nav { display: grid; gap: .1rem; }
.index-rail__l {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .5rem;
  align-items: center; min-height: var(--pp-target-compact);
  padding: .45rem 0 .45rem .55rem;
  border-left: var(--pp-rule-structure) solid var(--pp-hair);
  font-family: var(--pp-font-mono); font-size: .72rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--pp-muted);
}
.index-rail__l:hover { background: var(--pp-tint); border-left-color: var(--pp-line); color: var(--pp-ink); }
.index-rail__l code { font-size: .68rem; color: var(--pp-faint); }
.index-rail__l:hover code { color: var(--pp-ink); }

/* The years. They used to be set large and light in the margin column to the
   RIGHT of the list, which was the only decorative move on the page. Under a
   left lean that column is the wrong place for them: it puts a third band of
   content on the left and leaves every pixel of the drift empty on the right,
   which reads as a page that slid rather than a page that was placed. So the
   gesture moves to the rail and keeps its face and its weight. */
.index-rail__h {
  font-size: .64rem; letter-spacing: var(--pp-track-label);
  text-transform: uppercase; color: var(--pp-faint); margin-bottom: .45rem;
}
.index-rail__years { display: grid; gap: .15rem; }
.index-rail__y {
  justify-self: start;
  font-family: var(--pp-font-display); font-weight: 200;
  font-size: clamp(1.35rem, 2.2vw, 1.9rem); line-height: 1.15;
  letter-spacing: -0.04em; color: var(--pp-faint);
}
.index-rail__y:hover, .index-rail__y.is-on { color: var(--pp-ink); }

.bio { display: grid; gap: 1rem; }
.bio p { font-size: 1.0625rem; line-height: var(--pp-leading-body); text-wrap: pretty; }

.sect { display: grid; gap: clamp(1.25rem, 3vw, 1.8rem); }
.sect__col { display: grid; gap: clamp(1rem, 3vw, 1.4rem); }

/* The spine: 6px, the break that resets the reader. */
.sect__head {
  display: flex; flex-wrap: wrap; align-items: baseline;
  justify-content: space-between; gap: 1rem; padding-bottom: .6rem;
  border-bottom: var(--pp-rule-spine) solid var(--pp-edge);
}
.sect__head h2 {
  font-size: clamp(1.5rem, 4vw, 2.1rem); font-weight: 900; letter-spacing: -0.04em;
}
.sect__note {
  font-size: .7rem; letter-spacing: .1em; text-transform: uppercase;
  color: var(--pp-faint);
}

/* The filter is the legend. There is no second one.

   The two gaps are not the same number, and that is the whole rule. Each
   button is a 44px hit box around 21px of type, so a single 1.1rem gap on both
   axes puts about 40px of nothing between two wrapped rows and the strip reads
   as a layout fault rather than as a wrap. The columns keep their air, the rows
   close up, and the hit boxes do not move. The contents rail already worked
   this way at its own breakpoint. */
.filters { display: flex; flex-wrap: wrap; gap: .15rem 1.1rem; align-items: baseline; }
.filt {
  display: inline-flex; align-items: center;
  min-height: var(--pp-target); padding: .35rem 0;
  font-family: var(--pp-font-mono); font-size: .7rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--pp-faint);
}
.filt:hover { color: var(--pp-ink); }
/* The hit box is 44px tall and the rule stays tight under the label, which is
   why the underline lives on an inner span. */
.filt > span {
  display: inline-flex; align-items: baseline; gap: .4rem;
  border-bottom: var(--pp-rule-line) solid transparent; padding-bottom: .1rem;
}
.filt .sq { width: .4rem; height: .4rem; }
.filt.is-on { color: var(--pp-ink); }
.filt.is-on > span { border-bottom-color: var(--pp-edge); }

.writing { display: grid; gap: clamp(1.25rem, 3vw, 1.8rem); }

/* The group is a plain column now. It used to be a `.row`, so that the year
   could sit sticky in the margin column beside it — the year lives in the left
   rail instead, and with nothing left to put there the index no longer uses the
   margin column at all. That is deliberate and not a leftover: the margin
   column is a place for asides on a piece, and on a leaning index it would be
   the one thing standing between the text and the empty right side. */
.yeargroup__items { display: grid; }

/* One row per piece. The 2px left rule is the only register marker in the
   list: no letter badge, no coloured title, no thumbnail, no read time. */
.entry {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .75rem;
  align-items: baseline; padding: .8rem 0 .8rem .85rem;
  border-bottom: var(--pp-rule-hairline) solid var(--pp-hair);
  border-left: var(--pp-rule-structure) solid var(--pp-line);
}
.entry:hover { background: var(--pp-tint); color: inherit; }
.entry__t { display: grid; gap: .25rem; min-width: 0; }
.entry__title {
  font-family: var(--pp-font-display); font-weight: 700; font-size: 1.075rem;
  letter-spacing: -0.02em; line-height: 1.25; text-wrap: pretty;
}
.entry__dek { font-size: .9375rem; line-height: 1.5; color: var(--pp-muted); text-wrap: pretty; }
.entry__date { font-size: .7rem; color: var(--pp-faint); white-space: nowrap; }

/* Only the current year is expanded. A decade of writing then adds one line
   per year instead of a hundred rows. */
.showyear {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: .75rem;
  align-items: baseline; min-height: 44px; padding: .7rem 0 .7rem .85rem;
  border-bottom: var(--pp-rule-hairline) solid var(--pp-hair);
  border-left: var(--pp-rule-structure) solid var(--pp-hair);
  text-align: left;
}
.showyear:hover { background: var(--pp-tint); border-left-color: var(--pp-line); }
.showyear span { font-size: .95rem; line-height: 1.4; color: var(--pp-muted); }
.showyear code { font-size: .7rem; color: var(--pp-faint); white-space: nowrap; }

.hideyear {
  justify-self: start; display: inline-flex; align-items: center;
  min-height: 44px; font-family: var(--pp-font-mono); font-size: .68rem;
  letter-spacing: .1em; text-transform: uppercase; color: var(--pp-faint);
}
.hideyear:hover { color: var(--pp-ink); }

/* The state is per-year and toggles both ways. */
.yeargroup .entry, .yeargroup .hideyear { display: none; }
.yeargroup.is-open .entry { display: grid; }
.yeargroup.is-open .hideyear { display: inline-flex; }
.yeargroup.is-open .showyear { display: none; }
/* A filtered list is already short, and hiding matches inside it would be a
   lie, so a register filter expands everything. */
.writing.is-filtered .yeargroup .entry { display: grid; }
.writing.is-filtered .showyear, .writing.is-filtered .hideyear { display: none; }
.writing .entry[hidden] { display: none; }
.yeargroup[hidden] { display: none; }

/* Research is a section of the index, not a destination. */
.sect__col > p { font-size: 1.0625rem; line-height: var(--pp-leading-body); text-wrap: pretty; }
.papers { display: grid; }
.paper {
  display: grid; gap: .4rem; padding: 1rem 0;
  border-top: var(--pp-rule-line) solid var(--pp-line);
}
.paper__meta { display: flex; flex-wrap: wrap; gap: .75rem; align-items: baseline; }
.paper__meta code:first-child {
  font-size: .68rem; letter-spacing: .12em; text-transform: uppercase;
}
.paper__year { font-size: .68rem; color: var(--pp-faint); }
.paper h3 { font-size: 1.2rem; line-height: 1.2; text-wrap: pretty; }
.paper__abstract {
  font-size: .9375rem; line-height: 1.6; color: var(--pp-muted);
  max-width: 58ch; text-wrap: pretty;
}
.paper__links { display: flex; flex-wrap: wrap; gap: 1.1rem; margin-top: .15rem; }
.paper__link {
  font-family: var(--pp-font-mono); font-size: .72rem;
  color: var(--pp-plate-3-text); border-bottom: 1px solid currentColor;
}

.links { display: grid; }
.link-row {
  display: grid; grid-template-columns: 8ch minmax(0, 1fr); gap: .75rem;
  align-items: baseline; padding: .5rem 0;
  border-bottom: var(--pp-rule-hairline) solid var(--pp-hair);
}
.link-row code { font-size: .72rem; letter-spacing: .06em; color: var(--pp-faint); }

/* The footer states what the site does not do, sets the faces on the record,
   and stops. */
.foot {
  margin-top: clamp(3rem, 8vw, 5rem);
  padding-top: .8rem; border-top: var(--pp-rule-line) solid var(--pp-line);
  display: flex; flex-wrap: wrap; gap: 1.25rem; justify-content: space-between;
}
.foot code { font-size: .68rem; line-height: 1.6; color: var(--pp-faint); max-width: 54ch; }
/* ══ THE READING PAGE ══════════════════════════════════════════════════════ */

/* A piece is measure plus margin column, and the two are PREPRINT's to set. The
   width used to be written out as `calc(66ch + 20ch)`, which restated both
   tokens as literals and quietly dropped the gap between them, so the shell was
   a column-gap narrower than the thing it had to hold. Read the tokens instead:
   if the system ever changes the measure, this follows. */
.page--read {
  --site-page: calc(var(--pp-measure) + var(--pp-column-gap) + var(--pp-margin-column));
}

.topbar {
  padding: 1.1rem 0 .6rem;
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
}
/* The way back. An ink square and the site name in grey caps named a
   destination without saying it was one, so it now carries the arrow and the
   word. The arrow shifts left on hover, which is the only motion on the page and
   the cheapest way to say `backwards`. */
.topbar__home {
  display: inline-flex; align-items: center; gap: .5rem; min-height: 34px;
  font-family: var(--pp-font-mono); font-size: .72rem; letter-spacing: .08em;
  text-transform: uppercase; color: var(--pp-muted);
}
.topbar__home b { font-weight: var(--pp-weight-strong); color: var(--pp-muted); }
.topbar__home:hover, .topbar__home:hover b { color: var(--pp-ink); }
.topbar__home svg { transition: transform .12s ease; }
.topbar__home:hover svg { transform: translateX(-2px); }
@media (prefers-reduced-motion: reduce) {
  .topbar__home svg { transition: none; }
  .topbar__home:hover svg { transform: none; }
}
.topbar__end { display: flex; align-items: center; gap: .6rem; }

/* The whole-piece rule across the top. It and the rail's per-section bars
   answer different questions: where am I in the piece, where am I in this
   section. */
.progress { position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 40; pointer-events: none; }
.progress > i { display: block; height: 100%; width: 0; background: var(--pp-edge); }

.read {
  display: grid; grid-template-columns: var(--site-rail) minmax(0, 1fr);
  gap: var(--site-gap);
  padding-top: clamp(1rem, 3vw, 2rem);
}
/* No rail, so no rail column. It keeps the shell's left edge rather than
   re-centring inside it, which is what keeps a CV aligned with the masthead
   above it. */
.read--plain { grid-template-columns: minmax(0, 1fr); max-width: var(--site-page); }

/* The contents are a rail on the left, outside both the text column and the
   sidenote column. Not a block at the top, not a right-hand panel. */
.rail {
  position: sticky; top: 4.5rem; align-self: start;
  display: grid; gap: .75rem; max-height: calc(100vh - 6rem); overflow-y: auto;
}
/* Everything on the rail used to be set below the system's smallest step: a
   .64rem head over .68rem numbers and .8rem titles, against 1.0625rem body.
   That is furniture type, and the rail is not furniture, it is the piece's
   contents. So the head and the tail take `--pp-size-label` (.7rem) and the
   numbers take `--pp-size-mono` (.8125rem), which are the sizes the system
   already has for exactly those two jobs, and a title is set one step under
   body rather than five. One step is enough to keep it subordinate to the text
   it points at, and it was the SIZE that made the rail look small more than
   the width was. */
.rail__h {
  font-size: var(--pp-size-label); letter-spacing: var(--pp-track-label);
  text-transform: uppercase; color: var(--pp-faint);
}
.rail__list { display: grid; gap: .2rem; }
/* The rule is always 2px: the colour changes, not the weight, so an entry
   never moves sideways when it becomes active. */
.rail__e {
  display: grid; grid-template-columns: 1.35rem minmax(0, 1fr); gap: .4rem;
  align-items: start; text-align: left;
  padding: .5rem 0 .5rem .7rem;
  border-left: var(--pp-rule-structure) solid var(--pp-hair);
}
.rail__e:hover { background: var(--pp-tint); }
.rail__n { font-size: var(--pp-size-mono); line-height: 1.5; color: var(--pp-faint); }
.rail__t { display: grid; gap: .3rem; }
.rail__l { font-size: 1rem; line-height: 1.3; color: var(--pp-muted); font-weight: 400; }
.rail__bar { display: block; height: 2px; background: var(--pp-hair); }
.rail__bar > i { display: block; height: 2px; width: 0; background: var(--pp-edge); }
.rail__e[data-done] .rail__bar > i { background: var(--pp-plate-1); }
.rail__e.is-on { background: var(--pp-tint); border-left-color: var(--pp-edge); }
.rail__e.is-on .rail__n { color: var(--pp-ink); }
.rail__e.is-on .rail__l { color: var(--pp-ink); font-weight: var(--pp-weight-strong); }
.rail__pct { font-size: var(--pp-size-label); letter-spacing: .1em; color: var(--pp-faint); }
/* ══ THE POINTERS ══════════════════════════════════════════════════════════
   Thirteen drawn pointers ship in the vendored `tokens/cursors.css`, repainted
   per mode, at absolute paths the system now owns. This file used to restate
   all twenty-six declarations to work around Chrome resolving a relative url()
   inside a custom property against the document. The workshop had written the
   same block, at a different path, and a breach used twice is an undeclared
   token — so the system declares it and both surfaces deleted their copy.

   What is still the surface's job is the WIRING: arrow on the page, point on
   anything clickable, pressed while the mouse is down. Hotspots are contract,
   so a pointer is only ever swapped whole and never nudged. */

/* ── the keyword floor, declared once ──────────────────────────────────────
   It has to live at element level and nowhere else. A component rule that
   writes its own `cursor: pointer` on a class outranks the element selectors
   below — class beats element, and no amount of source order fixes that — so
   the drawn pointer is silently dropped on exactly that control and on
   nothing else. That is how the filter buttons, the mode switch and the year
   toggles all kept the stock hand while every link around them was drawn.
   Anything clickable is covered here. Do not re-declare it on a class. */
a[href], button, [role='button'], summary, [data-pp-mark] { cursor: pointer; }

/* ── and then the wiring ───────────────────────────────────────────────────
   All of it sits behind a hover/fine query, because the system's own rule is
   to drop drawn pointers on touch: there they are a lie about the input. The
   floor above carries equal specificity and comes first, so this block wins
   on source order, and each token carries its keyword fallback inside the
   var(), so a browser that will not load an SVG cursor still gets the right
   shape. */
@media (hover: hover) and (pointer: fine) {
  /* On the root rather than on body, so the strip below a short page and the
     scrollbar gutter are not the one place the OS arrow shows through. */
  html { cursor: var(--pp-cursor-arrow); }

  a[href], button, [role='button'], summary, [data-pp-mark] {
    cursor: var(--pp-cursor-point);
  }
  /* The lift is the only state change with no shape change: point casts
     1.1px, pressed casts 0.5px, so a press reads as the surface giving way.
     :active is the whole mechanism — no JS holds a pressing flag. */
  a[href]:active, button:active, [role='button']:active, [data-pp-mark]:active {
    cursor: var(--pp-cursor-pressed);
  }

  /* Blocked outranks point: the sticker says soon, and the pointer agrees. */
  .btn--soon, .btn--soon:active, button:disabled, [aria-disabled='true'] {
    cursor: var(--pp-cursor-blocked);
  }

  /* Prose on this site is a working surface — selecting a passage is how a
     marker is made — so the text pointer over a piece is the honest one. The
     links, marks and terms inside it match on their own elements, and a rule
     that matches an element always beats a cursor inherited from its parent,
     so each of them keeps its own pointer without needing to outrank this. */
  .article, .bio, .note, .refs__list, .paper__abstract, .figure figcaption {
    cursor: var(--pp-cursor-text);
  }

  .term { cursor: var(--pp-cursor-help); }
}
/* ══ THE CV ════════════════════════════════════════════════════════════════
   A CV is a document, not a piece, and it is read differently: scanned down a
   date rail first and only then read. So it drops the article's furniture (no
   dropcap, no 66ch measure) and takes a wider column, quieter section heads,
   and a left rail of dates that lines up all the way down the page.

   The whole page is built to survive being printed, because a CV that cannot be
   handed over on paper is only half a CV. Everything below has a matching rule
   in the print block at the foot of this file. */
.page--cv .read--plain { max-width: 46rem; }
.article--cv { gap: 1.6rem; }

/* A CV has no margin column and no sidenotes, so every row spans the full
   width. Without this the section rules stop at 66ch while the tables beside
   them run to 46rem, and the document reads as two documents. */
.article--cv .row { grid-template-columns: minmax(0, 1fr); }
.article--cv .row > * { grid-column: 1 / -1; }

/* Section heads: mono caps over a spine, not 900-weight display. A CV has seven
   of them and display heads at that count read as shouting. The selector has to
   be `.article--cv .h2` and not `.article--cv h2`: the renderer puts `class=h2`
   on every section head and `.article .h2` already claims it, so an element
   selector here loses on specificity and only the properties `.article .h2`
   happens not to set would land. */
.article--cv .h2 {
  margin-top: 1.5rem; padding: 0 0 .35rem; border-top: 0;
  border-bottom: var(--pp-rule-structure) solid var(--pp-edge);
  font-family: var(--pp-font-mono); font-size: .74rem; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase; color: var(--pp-ink);
}

/* The masthead. `## Name` is the first thing in the markdown, so it is the
   document's title rather than a section: display face, large, no rule. Every
   head is wrapped in its own `.row`, so this reaches through the wrapper —
   `> h2:first-child` matches nothing here. */
.article--cv .row:first-child .h2 {
  font-family: var(--pp-font-display);
  font-size: clamp(1.9rem, 4.5vw, 2.6rem); font-weight: 900;
  letter-spacing: -0.035em; text-transform: none;
  border: 0; padding: 0; margin: 0;
}
.article--cv .row:first-child + .row p { color: var(--pp-muted); }

/* The rail. A bare two-column table is a date and a thing, so the date column
   is set in mono at a fixed width and every row in the document agrees on where
   the thing starts. `nowrap` comes off, because a CV cell is prose and a CV is
   the one table on this site that must not scroll sideways. */
.article--cv .table--bare { border: 0; background: none; overflow: visible; }
.article--cv .table--bare table { width: 100%; }
.article--cv .table--bare td {
  padding: .5rem 0; vertical-align: baseline; white-space: normal;
  border-bottom: var(--pp-rule-hairline) solid var(--pp-hair);
  font-size: .92rem; color: var(--pp-ink);
}
.article--cv .table--bare tr:last-child td { border-bottom: 0; }
.article--cv .table--bare td:first-child {
  width: 9.5rem; padding-right: 1.2rem;
  font-family: var(--pp-font-mono); font-size: .7rem; letter-spacing: .04em;
  color: var(--pp-faint); white-space: nowrap;
}
.article--cv .table--bare td:last-child em {
  display: block; color: var(--pp-muted); font-size: .86rem; font-style: normal;
}

.article--cv ul { display: grid; gap: .3rem; }
.article--cv li { font-size: .92rem; }
.article--cv p { font-size: .92rem; }
/* A one-line label above a group, e.g. `Bachelor` over the courses. */
.article--cv h3 {
  font-family: var(--pp-font-mono); font-size: .66rem; letter-spacing: .12em;
  text-transform: uppercase; color: var(--pp-faint); margin-top: .4rem;
}
/* ══ PRINT ═════════════════════════════════════════════════════════════════
   The export. `save as pdf` runs the browser's own print, so this block *is*
   the PDF: there is no second renderer to keep in step and no committed file to
   go stale. Ink on paper, so the dark mode is dropped whatever the screen was
   set to, and the site's furniture goes with it. */
/* The margin is 14mm and not 0, and that is a decision rather than a default.
   The browser draws its own header and footer — page title, URL, date, page
   number — inside this margin, and the only way a stylesheet can suppress them
   is to leave no margin to draw in. Tested: `margin: 0` does remove them, and
   it also puts the second sheet's first line hard against the paper's edge,
   because padding on the content applies once to the box and not once per
   sheet. There is no third option in Chrome. So the sheet keeps its margin, and
   the hint beside the button tells the reader about the one checkbox that turns
   the browser's furniture off. The setting is remembered, so it is asked once. */
@page { size: A4; margin: 14mm 15mm; }

@media print {
  /* A reader who printed from dark mode should not be handed a black page.
     These win over the mode blocks because `html` here carries `data-mode` and
     an id-free element selector inside @media print still needs the extra
     weight — hence the attribute selector on both. */
  html[data-mode], html[data-mode] body {
    background: #fff !important; color: #000 !important;
  }
  html[data-mode] {
    --pp-paper: #fff; --pp-surface: #fff; --pp-sunk: #fff;
    --pp-ink: #000; --pp-muted: #333; --pp-faint: #555;
    --pp-edge: #000; --pp-line: #999; --pp-hair: #ccc; --pp-tint: #fff;
  }

  /* Screen furniture. None of it means anything on paper: a mode switch cannot
     be pressed, a `save as pdf` button cannot be pressed, and `back to` points
     at a page the sheet does not have. */
  .topbar, .foot, .progress, .skip, .rail, .index-rail, .modeswitch, [data-print] {
    display: none !important;
  }

  /* Paper has its own margins and the sheet is not a widescreen, so the shell
     grid and its lean are dropped entirely rather than printed. */
  .page, .page--cv { display: block; padding: 0; min-height: 0; }
  .page--cv .read--plain, .read, .read--plain { max-width: none; }
  .main { display: block; }

  /* Every block on this page is a grid container, and Chrome does not fragment
     grid across sheets: each row becomes monolithic, so a table that does not
     fit in what is left of a sheet moves whole and abandons its heading. As
     plain blocks they fragment normally, which is what makes `break-after`
     achievable at all. The CV's rows are single-column, so this costs nothing
     visually. */
  .article--cv, .article--cv .row, .article--cv .row > *,
  .article--cv .table--bare { display: block; }
  .article--cv > * + * { margin-top: 1.1rem; }
  .article--cv .table--bare table { width: 100%; }

  /* 10.5pt is the smallest this face stays comfortable at, and it puts the CV
     on two sheets rather than three. */
  html { font-size: 10.5pt; }
  a { text-decoration: none; color: #000; }
  /* The one URL worth having on paper is the site itself, which the masthead
     already prints. A CV full of bracketed hrefs is noise. */

  /* Nothing that reads as a unit may be split across a sheet boundary. */
  h2, h3 { break-after: avoid; page-break-after: avoid; }
  /* A row is the unit, not the table. Holding a whole table together sounds
     tidier and is worse: a table that cannot fit in what is left of the sheet
     jumps to the next one and leaves its own heading behind, which is the one
     thing a break rule exists to prevent. Rows split, headings do not. */
  tr, li { break-inside: avoid; page-break-inside: avoid; }
  /* `break-after` on the heading is not enough: each head is wrapped in its own
     `.row`, so the page break falls between two wrappers and never immediately
     after the h2. The rule has to sit on the wrapper. `TEACHING` printed alone
     at the foot of sheet one until it did. */
  .article--cv .row:has(.h2) { break-after: avoid; page-break-after: avoid; }
}
/* ══ RESPONSIVE ════════════════════════════════════════════════════════════ */

/* The rail would eat the measure on a narrow screen, and a piece is read
   linearly on a phone. */
@media (max-width: 1100px) {
  .rail { display: none; }
  .read { grid-template-columns: minmax(0, 1fr); }

  /* The index's rail does NOT go the same way. A piece's rail is a progress
     indicator and losing it costs a convenience; the index's rail is the only
     navigation on the site now that the jump links left the masthead, and
     hiding it would strand a phone at the top of the page. So it unpins and
     lays itself along the top instead. The years go, because a horizontal bar
     of them is a list of nothing — the groups they point at are one screen
     apart on a narrow layout. */
  .main { grid-template-columns: minmax(0, 1fr); }
  .index-rail {
    position: static; max-height: none; overflow: visible;
    padding-bottom: .3rem; border-bottom: var(--pp-rule-line) solid var(--pp-line);
  }
  .index-rail__nav { display: flex; flex-wrap: wrap; gap: .15rem 1.25rem; }
  .index-rail__l { border-left: 0; padding-left: 0; }
  .index-rail__years, .index-rail__h { display: none; }
}

/* The scrawl no longer dies here. `.scrawl--beside` is the system's hung
   arrangement and the system switches it off below 60rem, which is 960px and
   within 2px of the 958 this file used to compute for itself (88ch plus two
   3rem gutters, the last width at which the shelf holds full measure). Below
   that it stacks, which is a real narrow arrangement rather than the absence
   of one, so nothing needs to be hidden. The old rule is gone rather than
   changed. */

/* Below 820px the column becomes the screen. The row grid folds in
   reading.css; what folds here is the index's own furniture. */
@media (max-width: 820px) {
  .entry { grid-template-columns: minmax(0, 1fr); }
  .entry__date { grid-column: 1; }
}
@media (max-width: 560px) {
  .head__end { margin-left: 0; }
  .art__by { gap: .5rem 1rem; }

  /* The doors are squares here and the system has already done the collapsing.
     What is left is the drawing. A 3px cast under a 185px plank is a hard
     offset print; the same 3px under a 44px square is a third of the square,
     and the button starts reading as two stacked objects. It comes down with
     the button, and the press keeps its full travel because the travel is the
     affordance and it is unchanged at any size. */
  .btn--citron { box-shadow: 2px 2px 0 var(--pp-edge); }
  .btn--citron:hover, .btn--citron:active { box-shadow: 0 0 0 var(--pp-edge); }
  .btn--lab { box-shadow: 2px 2px 0 var(--pp-plate-3); }
  .btn--lab:hover:not([aria-disabled='true']),
  .btn--lab:active:not([aria-disabled='true']) { box-shadow: 0 0 0 var(--pp-plate-3); }
  .btn--arcade {
    box-shadow: 2px 2px 0 var(--arcade-tube), 0 0 8px rgba(255, 45, 149, .45);
  }
  .btn--arcade:hover:not([aria-disabled='true']),
  .btn--arcade:active:not([aria-disabled='true']) {
    box-shadow: 0 0 0 var(--arcade-tube), 0 0 12px rgba(255, 45, 149, .7);
  }
}
