/* ==========================================================================
   davidbordon.com — prototype stylesheet

   Type: one superfamily, two voices. IBM Plex Serif reads; IBM Plex Mono
   labels, navigates and counts.
   Theme: dark is the default and the identity; light is a real alternative.
   Layout: text left, picture right, running off the edge of the viewport.
   ========================================================================== */

:root {
  --bg:         #121110;
  --bg-sunk:    #0C0B0A;
  --surface:    #1A1815;
  --ink:        #EDE9E2;
  --ink-soft:   #A8A296;
  /* Lifted from #756E63, which fell to 3.74:1 on the page ground. Every
     label, rail tick and caption on the site is this colour, so it has to
     clear 4.5:1 — this reads 5.02 on --bg and 4.71 on --surface. */
  --ink-faint:  #8A8376;
  --rule:       #2C2924;
  --rule-firm:  #3D3931;

  /* Verdigris — oxidised copper. Muted on purpose: it marks, never shouts. */
  --accent:     #8FB5A5;
  --accent-dim: #5E7F72;

  --serif: "IBM Plex Serif", Iowan Old Style, Palatino, Georgia, serif;
  --mono:  "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  --wrap: 74rem;
  --gutter: clamp(1.25rem, 4vw, 3rem);
  --header-h: 4.25rem;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);

  /* Distance from the wrap's content edge out to the viewport edge. Anything
     that needs to bleed past the column grid uses this. --sbw is the scrollbar
     width, measured in JS: 100vw includes it, so without subtracting it every
     full-bleed element overhangs by ~15px and the page scrolls sideways. */
  --sbw: 0px;
  --page: calc(100vw - var(--sbw));
  --bleed: calc((var(--page) - min(var(--page), var(--wrap))) / 2 + var(--gutter));

  /* How a photograph dissolves toward the text column. A linear ramp reads
     as a cheap screen wipe — the eye catches both of its ends. This one is
     weighted like the portrait's: a long soft toe, a quick middle, a soft
     shoulder, so the picture is simply not there where the words are and
     nobody can say where it went. */
  --media-fade: linear-gradient(to left,
    #000 60%, rgba(0,0,0,0.95) 67%, rgba(0,0,0,0.82) 73%,
    rgba(0,0,0,0.62) 79%, rgba(0,0,0,0.40) 85%, rgba(0,0,0,0.21) 90%,
    rgba(0,0,0,0.08) 95%, rgba(0,0,0,0.02) 98%, transparent 100%);
}

:root[data-theme="light"] {
  --bg:         #F4F2EF;
  --bg-sunk:    #E7E3DD;
  --surface:    #FBFAF8;
  --ink:        #16150F;
  --ink-soft:   #565149;
  --ink-faint:  #6A645A;
  --rule:       #DCD7CE;
  --rule-firm:  #C4BDB1;
  --accent:     #3E6A59;
  --accent-dim: #6D9384;
}

*, *::before, *::after { box-sizing: border-box; }

/* `clip` rather than `hidden`: it contains the bleeding elements without
   creating a scroll container, which would break `position: sticky`. */
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--serif);
  font-size: clamp(1rem, 0.3vw + 0.95rem, 1.0625rem);
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }

.wrap { width: 100%; max-width: var(--wrap); margin-inline: auto; padding-inline: var(--gutter); }

:where(a, button, [role="button"]):focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.skip-link { position: absolute; left: -9999px; background: var(--ink); color: var(--bg); padding: 0.6rem 1rem; z-index: 30; }
.skip-link:focus { left: 1rem; top: 1rem; }

.label {
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.13em;
  text-transform: uppercase; color: var(--ink-faint);
}
.label--kind { color: var(--accent); }

/* One hover treatment for every link on the page. */
.link {
  color: inherit; text-decoration: none;
  background-image: linear-gradient(var(--accent), var(--accent));
  background-repeat: no-repeat; background-position: 0 100%; background-size: 0% 1px;
  transition: background-size 0.35s var(--ease), color 0.25s var(--ease);
  padding-bottom: 0.08em;
}
.link:hover, .link:focus-visible, .link[aria-current="page"] { color: var(--accent); background-size: 100% 1px; }

@media (prefers-reduced-motion: reduce) {
  /* Delays too: a squashed duration still waits out its delay first, which
     turns the arrival stagger into content flashing in one piece at a time. */
  *, *::before, *::after {
    transition-duration: 0.01ms !important; transition-delay: 0s !important;
    animation-duration: 0.01ms !important; animation-delay: 0s !important;
  }
}

/* ------------------------------------------------------- prototype bar */

.proto-note { background: var(--surface); border-bottom: 1px solid var(--rule); padding-block: 0.6rem; }
.proto-note__inner { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 1.25rem; }
.proto-note p { margin: 0; font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.06em; color: var(--ink-faint); }
.proto-note strong { color: var(--accent); font-weight: 500; }

/* ---------------------------------------------------------------- header */

.site-header {
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  backdrop-filter: blur(10px) saturate(1.2);
  border-bottom: 1px solid var(--rule);
}
.site-header__inner { display: flex; align-items: center; gap: 1.5rem; min-height: var(--header-h); }

/* THE COLLAPSE (Sept 2026). Where the nav sits inline the header arrives
   roomy and firms up once the page has moved 120px: the visible bar closes
   from 80px to 60px and the ground sinks a shade. --header-h follows the
   two states, so everything that clears the header (the rail's sticky
   top, the chapter jumps, the 404 stage) clears the header it actually
   has. A phone keeps the fixed 4.25rem bar: the burger menu hangs off
   that number and a bar that breathes under a thumb is a bar that jumps.

   HOW IT CLOSES. The box never changes height. Shrinking it looked right
   and was a trap: a sticky header is in flow, so a 20px shorter header
   moves the whole page up 20px, scroll anchoring pulls scrollY back by
   the same 20px to hold the reader's place, and a reader parked just past
   the threshold watched the bar open and close thirteen times in two
   seconds. Instead the 80px box slides 20px up under the edge of the
   screen (a negative sticky offset) and its padding shifts to keep the
   row centred in the 60px that still show. Nothing below moves, and
   nothing is left for the anchoring to correct. */
@media (min-width: 52.0625rem) {
  :root { --header-h: 5rem; }
  :root[data-scrolled="true"] { --header-h: 3.75rem; }
  .site-header__inner {
    min-height: 0; padding-block: 1.375rem;
    transition: padding 0.4s var(--ease);
  }
  [data-scrolled="true"] .site-header { top: -1.25rem; }
  [data-scrolled="true"] .site-header__inner { padding-block: 2rem 0.75rem; }

  /* Home only: past the hero the nav dissolves and one note takes its
     place at the same right edge, naming the instrument below. Faded link
     by link rather than as a block, because the note lives inside the nav
     to share its edge. */
  .nav { position: relative; }
  .nav > a { transition: color 0.25s var(--ease), background-size 0.35s var(--ease), opacity 0.35s var(--ease); }
  .site-header__note {
    position: absolute; right: 0; top: 50%; translate: 0 -50%;
    font-family: var(--mono); font-size: 0.625rem; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--ink-faint); white-space: nowrap;
    opacity: 0; pointer-events: none; transition: opacity 0.35s var(--ease);
  }
  [data-scrolled="true"] [data-page="home"] .nav > a { opacity: 0; pointer-events: none; }
  [data-scrolled="true"] [data-page="home"] .site-header__note { opacity: 1; }
}
@media (max-width: 52rem) { .site-header__note { display: none; } }

.wordmark {
  font-family: var(--mono); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0.24em; text-transform: uppercase;
  text-decoration: none; color: inherit; margin-right: auto; white-space: nowrap;
}

.nav { display: flex; align-items: center; gap: clamp(1.1rem, 2.5vw, 2rem); }
.nav a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--ink-soft); }
.nav a:hover, .nav a:focus-visible, .nav a[aria-current="page"] { color: var(--accent); }

.header-tools { display: flex; align-items: center; gap: 0.35rem; }
.icon-btn {
  display: grid; place-items: center; width: 2.25rem; height: 2.25rem;
  background: none; border: 1px solid transparent; border-radius: 2px;
  color: var(--ink-soft); cursor: pointer;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease);
}
.icon-btn:hover { color: var(--accent); border-color: var(--rule-firm); }
.icon-btn svg { width: 1.05rem; height: 1.05rem; fill: none; stroke: currentColor; stroke-width: 1.5; }
.theme-toggle .sun { display: none; }
:root[data-theme="light"] .theme-toggle .sun { display: block; }
:root[data-theme="light"] .theme-toggle .moon { display: none; }
.nav-toggle { display: none; }

/* Six items since Reverberations joined: the full row first fits at
   ~815px, so the burger holds until 52rem - this query only; the rest
   of the sheet keeps its 48rem phone boundary. */
@media (max-width: 52rem) {
  .nav-toggle { display: grid; }
  .nav {
    position: fixed; inset: var(--header-h) 0 auto 0;
    flex-direction: column; align-items: flex-start; gap: 0;
    padding: 0.5rem var(--gutter) 1.5rem;
    background: var(--bg); border-bottom: 1px solid var(--rule);
    transform: translateY(-0.75rem); opacity: 0; visibility: hidden; pointer-events: none;
    transition: opacity 0.28s var(--ease), transform 0.28s var(--ease), visibility 0s linear 0.28s;
  }
  .nav[data-open="true"] {
    opacity: 1; visibility: visible; pointer-events: auto; transform: translateY(0);
    transition: opacity 0.28s var(--ease), transform 0.28s var(--ease), visibility 0s linear 0s;
  }
  .nav a { width: 100%; padding-block: 0.95rem; border-bottom: 1px solid var(--rule); font-size: 0.8125rem; opacity: 0; transform: translateY(-0.4rem); }
  .nav[data-open="true"] a { animation: nav-in 0.34s var(--ease) forwards; }
  .nav[data-open="true"] a:nth-child(1) { animation-delay: 0.04s; }
  .nav[data-open="true"] a:nth-child(2) { animation-delay: 0.08s; }
  .nav[data-open="true"] a:nth-child(3) { animation-delay: 0.12s; }
  .nav[data-open="true"] a:nth-child(4) { animation-delay: 0.16s; }
  .nav[data-open="true"] a:nth-child(5) { animation-delay: 0.20s; }
  .nav[data-open="true"] a:nth-child(6) { animation-delay: 0.24s; }
}
@keyframes nav-in { to { opacity: 1; transform: translateY(0); } }

/* ------------------------------------------------------------------ hero */

.hero {
  padding-block: clamp(2.5rem, 6vh, 4rem) clamp(1.75rem, 4vh, 2.75rem);
  display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem); align-items: end;
  /* The first screen is an arrival, not the top of a feed. The hero takes
     most of the viewport and settles its weight at the bottom, poster-wise;
     the strand bar surfaces right at the fold as the way in. */
  min-height: min(46rem, calc(100vh - var(--header-h) - 10.5rem));
  min-height: min(46rem, calc(100svh - var(--header-h) - 10.5rem));
  align-content: end;
}
.hero__eyebrow { display: block; margin-bottom: 1rem; }

/* The face belongs on the page. Masked at the foot so it sits in the ground
   rather than on top of it. */
/* Fills its own third of the grid — the column does the sizing, not a width. */
/* --------------------------------------------------------- hero portrait */
/* THE TIDE. The first thing on the page, so it has to be the picture and not
   the apparatus around it. The plate is anchored at its foot right corner -
   crisp on the right and along the bottom - and gives way on the two sides
   that meet the weather: the left edge and the top dissolve into the page.

   Both ramps are smoothstep-shaped and long. A straight fade changes slope
   where it starts and stops and the eye reads those two kinks as edges; this
   one has none, and it is already opaque well before it reaches him - 96% a
   quarter of the way in from the left, where his shoulder is still a tenth
   of the frame away, and 90% a tenth of the way down, just above the hair.
   The face and the bust are never touched.

   No cut corners: THE DISSOLVE DOES THE CUTTING, and does it better. Three
   were tried - bottom left, top left, top right - and two of them fell
   inside the water, where the picture had already gone and no chamfer could
   ever be seen. The third straddled the line: solid along the right edge,
   dissolving along the top, so it showed as a small nick that then
   evaporated. One visible chamfer on a picture soft on two sides reads as a
   chip, not as a bevel. The corner the picture stands on stays square, which
   is where the two rules meet. (The treatment before all this dissolved one
   corner only and left hard edges along the top and the right; its two
   rotated bevel hairlines were too small to read as anything but marks.) */

.hero__portrait {
  margin: 0; align-self: end; position: relative;
  /* Three ramps, all smoothstep-shaped, all sized off where he actually
     stands in the frame (measured, not guessed: nothing of him is further
     left than 35% of the width in the upper half, and his hair begins 13%
     down). The two edge ramps clear those lines; the third runs on the
     DIAGONAL and does the real work at the top left corner, where the frame
     is empty studio grey for its whole depth - it is opaque by 21% along
     the diagonal, and the nearest he comes to that corner is 25%. Softer,
     because every ramp is long and none of them kinks; stronger, because
     at the corner all three multiply and almost nothing survives. */
  --tide-x: linear-gradient(to right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0.012) 3%, rgba(0,0,0,0.05) 6%,
    rgba(0,0,0,0.12) 9%, rgba(0,0,0,0.23) 12%, rgba(0,0,0,0.37) 15%,
    rgba(0,0,0,0.53) 18%, rgba(0,0,0,0.68) 21%, rgba(0,0,0,0.81) 24%,
    rgba(0,0,0,0.91) 27%, rgba(0,0,0,0.97) 30%, #000 32%);
  --tide-y: linear-gradient(to bottom,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0.015) 1.2%, rgba(0,0,0,0.07) 2.4%,
    rgba(0,0,0,0.17) 3.6%, rgba(0,0,0,0.32) 4.8%, rgba(0,0,0,0.5) 6%,
    rgba(0,0,0,0.67) 7.2%, rgba(0,0,0,0.81) 8.4%, rgba(0,0,0,0.91) 9.6%,
    rgba(0,0,0,0.97) 10.8%, #000 12%);
  --tide-d: linear-gradient(to bottom right,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0.02) 2%, rgba(0,0,0,0.08) 4.5%,
    rgba(0,0,0,0.19) 7%, rgba(0,0,0,0.33) 9.5%, rgba(0,0,0,0.49) 12%,
    rgba(0,0,0,0.65) 14.5%, rgba(0,0,0,0.79) 16.5%, rgba(0,0,0,0.9) 18%,
    rgba(0,0,0,0.97) 19.5%, #000 21%);
}

.hero__portrait picture {
  display: block; overflow: hidden;   /* keeps the zoom inside the frame */
  -webkit-mask-image: var(--tide-x), var(--tide-y), var(--tide-d);
  -webkit-mask-composite: source-in, source-in;
  mask-image: var(--tide-x), var(--tide-y), var(--tide-d);
  mask-composite: intersect, intersect;
}

.hero__portrait img {
  width: 100%;
  transition: transform 1.6s var(--ease), filter 1.6s var(--ease);
}
.hero:hover .hero__portrait img { transform: scale(1.06); }

/* The rule obeys the same tide as the picture. As a bordered box it was
   clipped by the corner cuts, so each line simply began in mid-air at the
   top right and the bottom left - a rule starting nowhere, which reads as a
   fault. Drawn instead as two fading lengths, each one dies out toward the
   corner the photograph is dissolving into: the upright fades upward, the
   flat one fades to the left. One law over the whole object. */
.hero__portrait::before,
.hero__portrait::after {
  content: ''; position: absolute; z-index: 0; pointer-events: none;
  transition: inset 0.5s var(--ease), opacity 0.5s var(--ease);
  opacity: 0.85;
}
/* the upright, off the right edge, fading out as it climbs */
.hero__portrait::before {
  top: 1.1rem; bottom: -1.1rem; right: -1.1rem; width: 1px;
  background: linear-gradient(to bottom,
    transparent 0%, color-mix(in srgb, var(--accent-dim) 45%, transparent) 26%,
    var(--accent-dim) 52%, var(--accent-dim) 100%);
}
/* the flat one, under the foot, fading out as it runs left */
.hero__portrait::after {
  left: 1.1rem; right: -1.1rem; bottom: -1.1rem; height: 1px;
  background: linear-gradient(to right,
    transparent 0%, color-mix(in srgb, var(--accent-dim) 45%, transparent) 30%,
    var(--accent-dim) 58%, var(--accent-dim) 100%);
}
.hero:hover .hero__portrait::before { top: 0.55rem; bottom: -0.55rem; right: -0.55rem; opacity: 1; }
.hero:hover .hero__portrait::after  { left: 0.55rem; right: -0.55rem; bottom: -0.55rem; opacity: 1; }

@media (max-width: 44rem) {
  .hero { grid-template-columns: minmax(0, 1fr); }
  /* A tenth of the width it has on a desktop, so the cuts and the rule
     come in with it. */
  .hero__portrait { width: 10rem; order: -1; }
  .hero__portrait::before { top: 0.6rem; bottom: -0.6rem; right: -0.6rem; }
  .hero__portrait::after { left: 0.6rem; right: -0.6rem; bottom: -0.6rem; }
  .hero:hover .hero__portrait::before { top: 0.3rem; bottom: -0.3rem; right: -0.3rem; }
  .hero:hover .hero__portrait::after { left: 0.3rem; right: -0.3rem; bottom: -0.3rem; }
}

.hero .hero__turn { margin-top: 0.9rem; min-height: 3.4em; }
/* Monumental. Three words and a full stop can carry it, and nothing below
   the fold competes at this size, so the page opens at poster scale and
   calms down from there. */
.hero h1 {
  font-size: clamp(2.75rem, 6.4vw, 5.625rem); line-height: 1.02; font-weight: 500;
  letter-spacing: -0.028em; text-wrap: balance; margin: 0 0 1.25rem; max-width: 20ch;
}
.hero p { font-size: clamp(1.0625rem, 0.5vw + 1rem, 1.1875rem); color: var(--ink-soft); max-width: 62ch; margin: 0; }

/* the rotating example — fixed slot so the paragraph never reflows */
.rotator { display: inline-block; position: relative; color: var(--ink); }
.rotator span { display: inline-block; transition: opacity 0.45s var(--ease), transform 0.45s var(--ease); }
.rotator[data-swapping="true"] span { opacity: 0; transform: translateY(-0.25em); }

/* ------------------------------------------------------------- strand bar */

/* The band between the hero and the feed is an INSTRUMENT, not a title: a
   rule, the four filters, a rule. It carries no visible heading — the
   filters already name the strands and their counts give the scope, so a
   word above them only said again what they say, sat unclickable among
   things that click, and, set after a dash directly under the hero's last
   line, read as an attribution to the paragraph above it. The heading lives
   on for the document outline and for screen readers, out of sight.
   The rules run FULL BLEED like every other rule on the page, so the band
   belongs to the feed beneath it instead of floating in the text column. */
.strand-bar {
  position: relative;
  display: flex; flex-direction: column; align-items: flex-start;
  padding-block: 0.9rem;
}
.strand-bar::before, .strand-bar::after {
  content: ''; position: absolute;
  left: calc(-1 * var(--bleed)); right: calc(-1 * var(--bleed));
  height: 1px; background: var(--rule);
}
.strand-bar::before { top: 0; }
.strand-bar::after { bottom: 0; }
.strand-bar__lead { margin: 0; }

/* Present to the document outline and to a screen reader, absent to the eye. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}
/* Four filters WRAP rather than travel sideways: the scrolling version left
   the fourth chip peeking twenty pixels into its own fade — a cue nobody
   could read — and hiding a filter is worse than showing all four. (The
   chapter strip still travels: it has many more entries and must stay one
   line, so THE SCROLLING EDGE below is its affordance.) */
.strand-bar__row {
  display: flex; align-items: center; flex-wrap: wrap; gap: 0.4rem 0.5rem;
  max-width: 100%;
}
.strand-bar .strand { flex: 0 0 auto; }

/* ONE LINE ON A PHONE. The four chips WITH their counts measure 428px; a
   375px phone gives the row 335 and a 360px one gives 320. The counts are
   what breaks the line, not the chips — without them, and with the padding
   drawn in a little, the row measures 307px and fits every phone from 347px
   up at full size. So below 29.5rem the counts step out of the line: they
   stay in the accessibility tree and are still read aloud, they simply stop
   taking room, and they return the moment there is space for them.

   Wrapping was tried twice and is wrong both ways: 3 + 1 orphans a chip
   under three, and a 2 × 2 invents a hierarchy the page does not have —
   All and Academia above Production and Music, as if the first two outranked
   the rest, when All is the whole and the three strands are peers. Stacking
   also exposed a flaw one line hides: an active chip shows its border at the
   margin while an inactive one shows only its label, 13px further in, so any
   column of them reads as misaligned.

   Two classes deep throughout: the chip's own rule is declared further down
   the sheet, so a single-class selector here loses on source order — it did,
   silently, until the measurements said the type had not changed. */
@media (max-width: 29.5rem) {
  .strand-bar .strand-bar__row { gap: 0.35rem; }
  .strand-bar .strand {
    /* 0.7rem is house size for a travelling row — the chapter strip runs
       0.65rem — and it buys the line enough room to hold on a 360px
       Android as well as a 375px iPhone. */
    font-size: 0.7rem; letter-spacing: 0.09em; padding-inline: 0.5rem;
  }
  .strand-bar .strand__count {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}
/* Narrower than an iPhone SE: the same single line, drawn tighter still. */
@media (max-width: 21.5rem) {
  .strand-bar .strand-bar__row { gap: 0.3rem; }
  .strand-bar .strand { padding-inline: 0.4rem; letter-spacing: 0.07em; }
}
.strand {
  font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
  background: none; border: 1px solid transparent; border-radius: 2px;
  padding: 0.4rem 0.75rem; color: var(--ink-faint); cursor: pointer;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.strand:hover { color: var(--ink); border-color: var(--rule-firm); }
.strand[aria-pressed="true"] { color: var(--accent); border-color: var(--accent-dim); background: color-mix(in srgb, var(--accent) 10%, transparent); }
.strand__count { color: var(--ink-faint); font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------- entry internals */

.entry[hidden] { display: none !important; }
.entry .entry__meta { display: flex; flex-wrap: wrap; gap: 0.35rem 1rem; margin: 0 0 0.6rem; }
.entry h3 {
  font-size: clamp(1.375rem, 1.5vw + 1rem, 1.875rem); line-height: 1.22; font-weight: 500;
  letter-spacing: -0.015em; margin: 0 0 0.6rem; text-wrap: balance;
}
.entry p { margin: 0; color: var(--ink-soft); max-width: 56ch; }
.entry p em { color: var(--ink); font-style: italic; }
/* The where-row is plain grey labels until a prize joins it: the .award chip is
   a padded box, so the row has to centre its items - stretched, the labels ride
   ~6px above the chip's own text. */
.entry .entry__where { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem 1.25rem; margin: 1rem 0 0; }
.entry__media { margin: 0; overflow: hidden; background: var(--bg-sunk); }
/* Every picture drifts in slightly on hover — one behaviour, everywhere. */
.entry__media img { width: 100%; transition: transform 0.9s var(--ease), filter 0.55s var(--ease); }
.entry:hover .entry__media img { transform: scale(1.04); }

/* Academic and institutional graphics arrive in corporate blues and greens that
   fight the rest of the page. Held back by default, full colour on hover — the
   picture is still honest, it just waits its turn. */
.entry__media--tame img { filter: saturate(0.28) contrast(0.94) brightness(0.86); }

/* A lighter hand for photographs that are simply warm rather than corporate. */
.entry__media--tame-lite img { filter: saturate(0.62) brightness(0.94); }

.entry:hover .entry__media--tame img,
.entry:hover .entry__media--tame-lite img { filter: none; }

/* ------------------------------------------------------------ video card */

.video { position: relative; margin: 0; background: var(--bg-sunk); overflow: hidden; }
.video img { width: 100%; }
.video__play {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: grid; place-items: center; gap: 0.5rem;
  background: linear-gradient(to top, rgba(0,0,0,0.55), rgba(0,0,0,0.1));
  border: 0; cursor: pointer; color: #fff;
}
.video__play svg { width: 3.25rem; height: 3.25rem; fill: none; stroke: currentColor; stroke-width: 1; transition: transform 0.35s var(--ease), opacity 0.35s var(--ease); opacity: 0.9; }
.video__play:hover svg { transform: scale(1.12); opacity: 1; }
.video iframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; }

/* --------------------------------------------------------------- the more */

.feed__more { margin: 0; }
.feed__more a, .about__more a, .jump a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; }
.arrow { display: inline-block; transition: transform 0.3s var(--ease); }
a:hover .arrow { transform: translateX(0.3rem); }

/* Strand shortcuts under the feed, so an unfiltered visitor still has a way
   in. No rule of its own: the band it sits in is ruled top and bottom. */
.jump { display: flex; flex-wrap: wrap; gap: 0.75rem 2rem; align-items: baseline; }
.jump .label { margin-right: auto; }

/* ----------------------------------------------------------------- plate */

/* -------------------------------------------------------------- carousel */
/* Full-bleed, scroll-snapped, keyboard reachable. Native scrolling does the
   work; the buttons and dots only nudge it, so it stays usable if JS fails. */

/* z-index above the chapter rail (15): a full-bleed photograph takes the
   whole screen, and the rail slides behind it rather than sitting on top
   with a halo trying to keep itself readable. */
.gallery { margin: clamp(3rem, 8vw, 5.5rem) 0 0; position: relative; z-index: 16; background: var(--bg-sunk); }
.gallery__track {
  display: flex; overflow-x: auto; scroll-snap-type: x mandatory;
  scrollbar-width: none; -ms-overflow-style: none;
}
.gallery__track::-webkit-scrollbar { display: none; }
.gallery__slide { flex: 0 0 100%; scroll-snap-align: center; position: relative; margin: 0; }
.gallery__slide img {
  width: 100%; height: min(72vh, 39rem); object-fit: cover;
  object-position: var(--pos, center);
}
/* Captions live below the picture rather than over it — a bright photograph
   was swallowing white text, and no scrim looked good on every slide. */
.gallery__slide figcaption { display: none; }
.gallery__foot {
  display: flex; align-items: center; gap: 1rem var(--gutter);
  padding: 0.85rem var(--gutter) 1rem; min-height: 3rem;
}
.gallery__caption { flex: 1 1 auto; min-width: 0; }
.gallery__caption span { display: block; }

.gallery__nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 3rem; height: 3rem; display: grid; place-items: center;
  background: color-mix(in srgb, #000 45%, transparent);
  border: 1px solid color-mix(in srgb, #fff 18%, transparent);
  border-radius: 50%; color: #fff; cursor: pointer;
  transition: background-color 0.25s var(--ease), border-color 0.25s var(--ease), opacity 0.25s var(--ease);
}
.gallery__nav:hover { background: color-mix(in srgb, #000 70%, transparent); border-color: var(--accent); color: var(--accent); }
.gallery__nav[disabled] { opacity: 0.25; cursor: default; }
.gallery__nav--prev { left: clamp(0.5rem, 2vw, 1.5rem); }
.gallery__nav--next { right: clamp(0.5rem, 2vw, 1.5rem); }
.gallery__nav svg { width: 1.1rem; height: 1.1rem; fill: none; stroke: currentColor; stroke-width: 1.5; }

.gallery__dots { display: flex; gap: 0.5rem; flex: 0 0 auto; }
.gallery__dot {
  width: 0.4rem; height: 0.4rem; padding: 0; border-radius: 50%;
  background: var(--rule-firm); border: 0; cursor: pointer;
  transition: background-color 0.25s var(--ease), transform 0.25s var(--ease);
}
.gallery__dot[aria-current="true"] { background: var(--accent); transform: scale(1.5); }

@media (max-width: 44rem) {
  .gallery__nav { display: none; }
  .gallery__slide img { height: 60vh; }
}

/* ----------------------------------------------------------------- about */

.about { display: grid; grid-template-columns: 13rem minmax(0, 1fr); gap: clamp(1.5rem, 5vw, 3.5rem);
         align-items: start; padding-block: clamp(3rem, 8vw, 4.5rem); }
.about h2 { font-size: clamp(1.375rem, 1.5vw + 1rem, 1.75rem); font-weight: 500; margin: 0 0 0.9rem; letter-spacing: -0.015em; }
.about p { color: var(--ink-soft); max-width: 60ch; margin: 0; }
.about p + p { margin-top: 0.85rem; }
.about .about__more { margin-top: 1.5rem; }
@media (max-width: 48rem) {
  .about { grid-template-columns: minmax(0, 1fr); }
  .about__portrait { max-width: 10rem; }
}

/* ---------------------------------------------------------------- footer */

/* One line, read in the order people use it: the address, the two mails,
   the door off the map, and the social pair sent right on their own (the
   pair is injected by site.js; its margin does the sending). */
.site-footer { border-top: 1px solid var(--rule); padding-block: 1.375rem; }
/* Positioned so the whole row sits above the rose, which is absolutely
   placed and would otherwise paint over the links where the two meet. */
.site-footer__inner { display: flex; flex-wrap: wrap; gap: 0.75rem 2.25rem; align-items: baseline; position: relative; z-index: 1; }
.site-footer a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.08em; }
.footer-off { color: var(--ink-faint); }


/* --------------------------------------------------------------- lightbox */

.lightbox {
  position: fixed; inset: 0; z-index: 40;
  display: grid; place-items: center; padding: clamp(1rem, 4vw, 3rem);
  background: color-mix(in srgb, var(--bg-sunk) 92%, transparent);
  backdrop-filter: blur(6px);
  opacity: 0; visibility: hidden;
  /* `visibility` steps, it does not ramp. Delaying it by the length of the
     fade on the way out and not at all on the way in means the panel is
     really visible the instant it opens — which is what lets the close
     button take focus — and still fades out fully before it goes away. */
  transition: opacity 0.3s var(--ease), visibility 0s linear 0.3s;
}
.lightbox[data-open="true"] {
  opacity: 1; visibility: visible;
  transition: opacity 0.3s var(--ease), visibility 0s linear 0s;
}
.lightbox img { max-width: 100%; max-height: 88vh; object-fit: contain; }
.lightbox__close {
  position: absolute; top: 1rem; right: 1rem;
  background: none; border: 1px solid var(--rule-firm); border-radius: 2px;
  color: var(--ink); width: 2.5rem; height: 2.5rem; cursor: pointer;
  font-family: var(--mono); font-size: 1rem;
}
.lightbox__close:hover { color: var(--accent); border-color: var(--accent-dim); }
.zoomable { cursor: zoom-in; pointer-events: auto; }
/* The backdrop is pointer-events:none so it never eats text selection; a panel
   that holds a zoomable picture has to opt back in. */
.entry__media:has(.zoomable) { pointer-events: auto; }

/* ==========================================================================
   THE LAYOUT
   The picture is the ground the text sits on. It fills the entry exactly, so
   the cut is clean, and runs off the right edge of the viewport.
   ========================================================================== */

/* No bottom padding: the last entry must meet its closing rule flush, the
   way every other entry meets the divider beneath it. */
.feed { padding-block: 2rem 0; }
.entry { position: relative; padding-block: clamp(2.5rem, 6vw, 5rem); }

.entry__media {
  position: absolute; top: 0; bottom: 0;
  /* --bleed is the distance from the wrap's content edge to the edge of
     the screen. It used to be derived from the entry's own width, which is
     only the same number while the column sits centred; beside the chapter
     rail it is not, and the old sum overshot the screen by half a rail. */
  right: calc(-1 * var(--bleed));
  /* The diagonal cut retired in v44. The photographs end the way the very
     first sketch wanted them to — fading into the page: see THE WATERLINE
     below. */
  left: 37rem; width: auto; background: none; pointer-events: none;
  opacity: 0.85;
  transition: opacity 0.5s var(--ease),
              -webkit-mask-position 0.7s var(--ease),
              mask-position 0.7s var(--ease);
}
/* THE WATERLINE. Every photograph fades into the page across the same
   12rem wedge: fully transparent at the outer edge, fully opaque where
   the picture proper begins. The ramp is a SMOOTHSTEP — t²(3−2t), sampled
   every rem — not a straight line: a linear fade changes slope abruptly
   at both ends and the eye reads those two kinks as vertical lines
   (banding was worse still — hard stops are visible edges by definition).
   Easing the entry and the arrival to zero slope leaves nothing anywhere
   for an edge to be. One treatment for every entry alike, sleeves and
   graphics included; only the panorama keeps its own weather. On hover
   the water retreats: the mask slides left, the picture resolves to its
   true edge, and a dim accent hairline names it (the hairline sits under
   the water, so the same slide that bares the edge lets it through). The
   mask exists only where the media band does — at stacked widths the
   photograph owns its whole frame. */
@media (min-width: 72.0625rem) {
  .entry:not(.entry--panorama) .entry__media {
    --waterline: linear-gradient(to right,
      rgba(0, 0, 0, 0)     0,
      rgba(0, 0, 0, 0.020) 1rem,
      rgba(0, 0, 0, 0.074) 2rem,
      rgba(0, 0, 0, 0.156) 3rem,
      rgba(0, 0, 0, 0.259) 4rem,
      rgba(0, 0, 0, 0.376) 5rem,
      rgba(0, 0, 0, 0.500) 6rem,
      rgba(0, 0, 0, 0.624) 7rem,
      rgba(0, 0, 0, 0.741) 8rem,
      rgba(0, 0, 0, 0.844) 9rem,
      rgba(0, 0, 0, 0.926) 10rem,
      rgba(0, 0, 0, 0.980) 11rem,
      #000 12rem);
    -webkit-mask-image: var(--waterline); mask-image: var(--waterline);
    -webkit-mask-size: calc(100% + 12rem) 100%; mask-size: calc(100% + 12rem) 100%;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: 0 0; mask-position: 0 0;
  }
  .entry:not(.entry--panorama):hover .entry__media {
    -webkit-mask-position: -12rem 0; mask-position: -12rem 0;
  }
  .entry:not(.entry--panorama) .entry__media::before {
    content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 1px;
    background: var(--accent-dim); opacity: 0; z-index: 2;
    transition: opacity 0.5s var(--ease) 0.12s;
  }
  .entry:not(.entry--panorama):hover .entry__media::before { opacity: 0.7; }
}
.entry__media img,
.entry__media picture {
  width: 100%; height: 100%; object-fit: cover;
  /* Each picture says where its subject is; --pos is set per entry. */
  object-position: var(--pos, center);
}
.entry:hover .entry__media { opacity: 1; }
.entry__body { position: relative; z-index: 1; max-width: 34rem; }
.entry p { max-width: 42ch; }

.entry + .entry { border-top: 0; }
.entry + .entry::before {
  content: ''; position: absolute; top: 0;
  left: calc(-1 * var(--bleed)); right: calc(-1 * var(--bleed));
  height: 1px; background: var(--rule);
  /* Above the media band: the diagonal's solid wedge is opaque now (the
     old dissolve was transparent), and without this the divider visibly
     stopped where the photograph began. */
  z-index: 3;
}

/* Below this the text column and a 74% picture cannot both fit, so the
   picture drops underneath instead of sitting behind the words. */
@media (max-width: 72rem) {
  .entry__media, .entry--video .entry__media {
    /* inset:auto, not just position:static — the full-width rule parks the
       band at left:37rem, and a variant that needs to position itself here
       (the etching, which must contain its own streamlines) would inherit
       that offset and walk off the right edge of the screen. */
    position: static; inset: auto; width: 100%; opacity: 1;
    -webkit-mask-image: none; mask-image: none; margin-top: 1.25rem;
  }
  .entry__media img { height: auto; }
  .entry__body { max-width: none; }
  .entry--video { grid-template-columns: minmax(0, 1fr); }
}

/* ------------------------------------------------------------ video rows */
/* A video row reads like any other entry: the player fills the picture slot
   between the dividers and softens on its left edge. The softening lifts the
   moment someone presses play, because a playing video needs its whole frame. */

.entry--video { position: relative; }

.entry--video .entry__media {
  position: absolute; top: 0; bottom: 0;
  right: calc(-1 * var(--bleed));
  left: 37rem; width: auto; opacity: 1; pointer-events: auto; max-width: none;
}
.entry--video[data-playing="true"] .entry__media {
  -webkit-mask-image: none; mask-image: none;
}
/* A playing video owns its whole frame: the waterline steps aside. */
.entry--video[data-playing="true"] .entry__media::before { display: none; }
.entry--video .video { height: 100%; display: block; }
.entry--video .video picture { display: block; height: 100%; }
.entry--video .video::after { content: none; }
.entry--video .video img { width: 100%; height: 100%; object-fit: cover; }
.entry--video .video iframe { height: 100%; aspect-ratio: auto; }

@media (max-width: 72rem) {
  .entry--video .entry__media {
    position: static; width: 100%; margin-top: 1.25rem;
    -webkit-mask-image: none; mask-image: none;
  }
  .entry--video .video, .entry--video .video picture { height: auto; }
  .entry--video .video img { height: auto; }
  .entry--video .video iframe { height: auto; aspect-ratio: 16 / 9; }
}

/* The feed's foot. Two edge-to-edge rules, exactly the ones that divide the
   entries: the first closes the last entry flush, the way every entry is
   closed; the second closes the shortcuts under it. So the way out of the
   feed reads as the feed's last row rather than as a caption hanging off
   the bottom of the page. The band lays itself out — the year link is
   hidden on "All" (no archive page yet), and a flex gap simply collapses
   when it is. */
.feed-tail {
  position: relative; padding-block: 3rem;
  display: flex; flex-direction: column; gap: 1.6rem;
}
.feed-tail::before, .feed-tail::after {
  content: ''; position: absolute;
  left: calc(-1 * var(--bleed)); right: calc(-1 * var(--bleed));
  height: 1px; background: var(--rule);
}
.feed-tail::before { top: 0; }
.feed-tail::after { bottom: 0; }



/* Full-bleed pseudo-elements can overhang the right edge by a gutter or two.
   `clip` rather than `hidden` contains them without creating a scroll
   container, which would change how position:sticky resolves. */
main { overflow-x: clip; }

/* Escape hatch shown once a video is playing, in case the embed is refused. */
.video__out {
  position: absolute; right: 0.6rem; bottom: 0.5rem; z-index: 2;
  color: var(--ink-soft); text-decoration: none;
  background: color-mix(in srgb, #000 55%, transparent);
  padding: 0.3rem 0.55rem; border-radius: 2px;
}
.video__out:hover { color: var(--accent); }

/* ==========================================================================
   SUBPAGES
   A page hero, then sectioned content. Publication lists are the load-bearing
   element on Academia, so they get real typographic structure rather than
   being dumped into paragraphs.
   ========================================================================== */

/* Label, headline, lead – and a hairline right across the sheet beneath,
   which the content column's own rule hangs from once the rail begins. */
.page-hero { padding-block: clamp(3.5rem, 8vh, 4.75rem) clamp(2.25rem, 5vh, 3.5rem); }
.page-hero h1 {
  font-size: clamp(2.5rem, 4.8vw, 3.75rem); line-height: 1; font-weight: 500;
  letter-spacing: -0.025em; margin: 0.875rem 0 1.375rem; max-width: 18ch;
}
.page-hero p { font-size: 1.09375rem; line-height: 1.6; color: var(--ink-soft); max-width: 62ch; margin: 0; }
.page-hero .label { display: block; }
.page-hero::after, .about-hero::after {
  content: ''; position: absolute; bottom: 0; z-index: 1;
  left: calc(-1 * var(--bleed)); right: calc(-1 * var(--bleed));
  height: 1px; background: var(--rule);
}

/* A chapter: 40px above its head, 56px under its last line, and a hairline
   between neighbours. The line bleeds to the right edge of the screen like
   the photographs do; on the rail pages it starts at the content column
   (see THE RAIL, IN THE GRID) so it never crosses the chapter list, and
   below the rail's width – where the column is the sheet – it bleeds both
   ways as before. Every chapter is a sibling in one column now, so plain
   adjacency finds every boundary. */
.section { padding-block: 2.5rem 3.5rem; position: relative; }
.section + .section::before {
  content: ''; position: absolute; top: 0;
  left: calc(-1 * var(--bleed)); right: calc(-1 * var(--bleed));
  height: 1px; background: var(--rule);
}.section h3 {
  font-size: clamp(1.125rem, 0.8vw + 1rem, 1.375rem); font-weight: 500;
  letter-spacing: -0.01em; margin: 0 0 0.9rem;
}
.section h3 + .pubs { margin-top: -0.2rem; }
.section h3:not(:first-of-type) { margin-top: 2.25rem; }

/* --- publication list --- */
.pubs { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.4rem; }
.pubs li {
  display: grid; grid-template-columns: 4.5rem minmax(0, 1fr);
  gap: 0 clamp(0.75rem, 2vw, 1.75rem); align-items: baseline;
}
.pubs .pub__year { font-variant-numeric: tabular-nums; padding-top: 0.15rem; }
.pubs .pub__body { max-width: 72ch; }
.pubs .pub__body p { margin: 0; color: var(--ink-soft); }
.pubs .pub__body em { color: var(--ink); font-style: italic; }
.pubs .pub__meta {
  margin-top: 0.4rem; display: flex; flex-wrap: wrap;
  align-items: center; gap: 0.4rem 1.1rem;
}
@media (max-width: 40rem) {
  .pubs li { grid-template-columns: minmax(0, 1fr); gap: 0.35rem; }
}

/* --- dated rows used by awards, chairing, visits --- */
.rows { list-style: none; margin: 0; padding: 0; display: grid; gap: 1.1rem; }
.rows li { display: grid; grid-template-columns: 9rem minmax(0, 1fr); gap: 0 clamp(0.75rem, 2vw, 1.75rem); align-items: baseline; }
.rows .row__when { font-variant-numeric: tabular-nums; }
.rows .row__what { color: var(--ink-soft); max-width: 62ch; }
.rows .row__what strong { color: var(--ink); font-weight: 500; }
@media (max-width: 40rem) {
  .rows li { grid-template-columns: minmax(0, 1fr); gap: 0.3rem; }
}

/* --- the CV call-out --- */
.cv-card {
  display: flex; flex-wrap: wrap; align-items: center; gap: 1rem 2rem;
  padding: 1.5rem clamp(1.25rem, 3vw, 2rem);
  border: 1px solid var(--rule-firm); border-radius: 2px;
  background: color-mix(in srgb, var(--surface) 70%, transparent);
}
.cv-card p { margin: 0; color: var(--ink-soft); flex: 1 1 22rem; }
.cv-card a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; }

/* A prize is the one thing on a publication line a reader should catch without
   reading — so it gets a boxed, accented chip rather than another grey label. */
.award {
  display: inline-flex; align-items: center; gap: 0.45rem;
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--accent);
  padding: 0.3rem 0.6rem; border: 1px solid var(--accent-dim); border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.award::before { content: '★'; font-size: 0.75em; line-height: 1; }
.rows--awards .row__when {
  display: inline-flex; justify-content: center;
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.12em;
  color: var(--accent); padding: 0.3rem 0.6rem;
  border: 1px solid var(--accent-dim); border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}

/* The bottom of Academia was two columns fighting each other; it is now four
   even blocks that reflow instead of a hard split. */
.grid-blocks {
  display: grid; gap: clamp(2rem, 4vw, 3rem) clamp(1.5rem, 4vw, 3.5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
  align-items: start;
}
.block h3 { margin-top: 0; }
.block p { color: var(--ink-soft); margin: 0; max-width: 54ch; }
.block p + p { margin-top: 0.8rem; }
.block .rows { margin-top: 0.25rem; }
/* A block that carries a dated list takes the whole row. Two abreast, the
   9rem date column left its entries a ~210px strip to wrap in, and the odd
   one out (Summer schools) sat alone beside half a sheet of nothing. Full
   width, the rows read like the Awards and Chairing chapters above them. */
.grid-blocks > .block:has(> .rows) { grid-column: 1 / -1; }
/* …and a date column wide enough for "Nov 2025 · Jan 2026" on one line. */
@media (min-width: 40.0625rem) {
  .block .rows li { grid-template-columns: 10rem minmax(0, 1fr); }
}

/* --------------------------------------------------------- about portrait */
/* The face was sitting in a plain rectangle. It now has a cut corner and an
   offset frame that closes on hover — a small piece of editorial furniture
   rather than a placeholder box. */
.about__portrait { margin: 0; position: relative; overflow: visible; background: none; }
.about__portrait picture {
  display: block; position: relative; z-index: 1;
  clip-path: polygon(0 0, calc(100% - 2.25rem) 0, 100% 2.25rem, 100% 100%, 2.25rem 100%, 0 calc(100% - 2.25rem));
  background: var(--bg-sunk);
}
.about__portrait img { transition: transform 1s var(--ease), filter 0.6s var(--ease); }
.about__portrait::before {
  content: ''; position: absolute; inset: 1rem -1rem -1rem 1rem;
  border: 1px solid var(--accent-dim); z-index: 0;
  clip-path: polygon(0 0, calc(100% - 2.25rem) 0, 100% 2.25rem, 100% 100%, 2.25rem 100%, 0 calc(100% - 2.25rem));
  transition: inset 0.5s var(--ease), border-color 0.5s var(--ease);
}
.about:hover .about__portrait::before { inset: 0.5rem -0.5rem -0.5rem 0.5rem; border-color: var(--accent); }
.about:hover .about__portrait img { transform: scale(1.03); }
@media (max-width: 48rem) { .about__portrait::before { inset: 0.75rem -0.75rem -0.75rem 0.75rem; } }

/* A prize needs room to read as a prize — it drops to its own line under the
   citation rather than crowding the links beside it.
   The prize always gets its own row with the same gap above, whether or not
   there are links before it — otherwise two chips sit at different heights. */
.pubs .pub__body .pub__prize { margin: 0.8rem 0 0.15rem; }
.rows--awards { gap: 0.85rem; }
.rows--awards li { align-items: center; }

/* ==========================================================================
   MUSIC
   Four components the other pages do not need: the festival editions, a
   tracklist, a manifesto pulled out of its document, and the role line that
   sits under a band's name.
   ========================================================================== */

/* --- roles under a band name --- */
/* Which hat, in which band. Reads as a caption, not as prose. */
.roles {
  display: flex; flex-wrap: wrap; gap: 0.3rem 1.1rem;
  margin: 0 0 0.75rem; padding: 0;      /* the default ul indent has to go */
}
.roles li { list-style: none; }

/* --- tracklists --- */
/* Ten winds and six older tracks. Numbered in mono so the column of figures
   stays straight, with the title in serif because it is a name, not data. */
.tracks { list-style: none; margin: 0; padding: 0; counter-reset: track; }
.tracks li {
  display: grid; grid-template-columns: 2.4rem minmax(0, 1fr) auto;
  gap: 0 0.75rem; align-items: baseline;
  padding-block: 0.5rem; border-bottom: 1px solid var(--rule);
}
.tracks li:first-child { border-top: 1px solid var(--rule); }
.tracks li::before {
  counter-increment: track; content: counter(track, decimal-leading-zero);
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.1em;
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
.tracks .track__name { letter-spacing: 0.06em; }
.tracks .track__time {
  font-family: var(--mono); font-size: 0.6875rem; letter-spacing: 0.08em;
  color: var(--ink-faint); font-variant-numeric: tabular-nums;
}
/* The wind names are the record's whole structure, so they get the room. */
.tracks--winds .track__name { font-size: 1.0625rem; letter-spacing: 0.14em; }

/* --- a manifesto, lifted out of its document --- */
/* The festival published one every year. They are the best writing in the
   archive and they are quoted, not paraphrased. */
.statement { margin: 0; max-width: 48ch; padding-left: clamp(1rem, 3vw, 2rem);
             border-left: 1px solid var(--accent-dim); }
.statement p {
  font-size: clamp(1.0625rem, 0.8vw + 1rem, 1.3125rem); line-height: 1.5;
  font-style: italic; color: var(--ink); margin: 0; text-wrap: pretty;
}
.statement p + p { margin-top: 0.75rem; }
.statement footer { margin-top: 1rem; }

/* --- the four editions of Črna Šagra --- */
/* The posters are the record of the thing. Four of them side by side say
   "this ran for four years" faster than any sentence, so they lead and the
   facts sit underneath. */
.editions {
  display: grid; align-items: start;
  gap: clamp(2rem, 4vw, 3rem) clamp(1.25rem, 2.5vw, 2rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}
.edition { margin: 0; }
.edition__poster {
  display: block; margin: 0 0 1.1rem; overflow: hidden;
  background: var(--bg-sunk); border: 1px solid var(--rule);
  transition: border-color 0.4s var(--ease);
}
.edition:hover .edition__poster { border-color: var(--accent-dim); }
.edition__poster img { width: 100%; transition: transform 0.8s var(--ease); }
.edition:hover .edition__poster img { transform: scale(1.03); }

.edition__head { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem 0.75rem; margin: 0 0 0.7rem; }
.edition__year {
  display: inline-flex; font-family: var(--mono); font-size: 0.6875rem;
  letter-spacing: 0.12em; color: var(--accent); padding: 0.28rem 0.55rem;
  border: 1px solid var(--accent-dim); border-radius: 2px;
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  font-variant-numeric: tabular-nums;
}
.edition__patron { margin: 0 0 0.8rem; color: var(--ink-soft); }
.edition__patron em { color: var(--ink); font-style: italic; }
.edition__bill { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.3rem; }
.edition__bill li { color: var(--ink-soft); font-size: 0.9375rem; line-height: 1.35; }
.edition__bill .label { display: block; margin-top: 0.05rem; }

/* --- a record, with its cover --- */
/* Narrower than a feed entry and it keeps its picture: the cover is the
   object, so it sits beside the facts rather than bleeding off the page. */
.record { display: grid; grid-template-columns: minmax(0, 19rem) minmax(0, 1fr);
          gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
/* No bevel here: a sleeve is artwork someone else composed, and cutting the
   corner off it is vandalism rather than design. */
.record__cover { margin: 0; overflow: hidden; background: var(--bg-sunk); }
.record__cover img { width: 100%; transition: transform 0.9s var(--ease); }
.record:hover .record__cover img { transform: scale(1.03); }
.record h3 { margin-top: 0; }
.record__meta { display: flex; flex-wrap: wrap; gap: 0.35rem 1.25rem; margin: 0 0 1rem; }
.record__body > p { color: var(--ink-soft); max-width: 56ch; margin: 0 0 1rem; }
.record__body > p em { color: var(--ink); font-style: italic; }
.record__links { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; margin-top: 1.25rem; }
.record__links a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; }
@media (max-width: 44rem) {
  .record { grid-template-columns: minmax(0, 1fr); }
  .record__cover { max-width: 14rem; }
}

/* Small positional rules the Music page needs, kept out of the markup. */
.record + .record { margin-top: clamp(2.5rem, 5vw, 4rem); }
.record__cover figcaption { margin-top: 0.6rem; }
/* Needs to outrank `.record__body > p`, which is one element more specific. */
.record__body > .record__credits { margin: 1.25rem 0 0; }
/* And so does the links row, for the same reason — its own margin-top was
   declared but never applied, so the Bandcamp line sat flush against
   whatever came before it. On the two records with a credits line that was
   invisible; on the one without (Kvlt ov Vitis et Olea) the link landed
   hard against the tracklist. All three now stand the same distance off. */
.record__body > .record__links { margin-top: 1.25rem; }
/* A footnote under a line-up – the free bus, the first foreign band. */
.edition__note { margin: 0.7rem 0 0; color: var(--ink-soft); font-size: 0.9375rem; line-height: 1.4; }
/* A statement or a dated list following prose inside a block. */
.block .statement, .block .rows { margin-top: 1.25rem; }

/* ==========================================================================
   THE RAIL, IN THE GRID
   Sept 2026 redesign. The chapter list used to be a fixed instrument in the
   outer margin, with a veil to part the hairlines around it. It is now a
   column of the page: from 68.75rem the page body is a two-column grid,
   rail left, chapters right, a rule between them, and the rail rides along
   sticky under the header. Numbered rows on hairlines; the chapter whose
   top has passed 40% of the viewport is lit (site.js); a chapter's parts
   fold out beneath it while the reader is inside. Below the breakpoint the
   rail is away and the chapter strip under the header takes over.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
/* Clear the sticky header when a chapter is jumped to — and the chapter
   strip riding under it at the widths where the rail is away. */
.section[id], [id].feed { scroll-margin-top: calc(var(--header-h) + var(--strip-h, 0px) + 1.5rem); }

.chapters { display: none; }
.page-content { min-width: 0; }

@media (min-width: 68.75rem) {
  .page-body {
    display: grid; grid-template-columns: 15.625rem minmax(0, 1fr);
    align-items: start;
  }
  .chapters {
    display: block; position: sticky; top: calc(var(--header-h) + 1rem);
    max-height: calc(100vh - var(--header-h) - 1rem); overflow: auto;
    scrollbar-width: thin;
    padding: 2.75rem 2rem 2.5rem 0;
  }
  .page-content {
    border-left: 1px solid var(--rule);
    padding: 2.75rem 0 5rem 1.5rem;
  }
  .page-content > .section:first-child { padding-top: 0; }
  /* The dividers start at the column, so they never cross the rail. */
  .page-content .section + .section::before,
  .page-content .entry + .entry::before { left: 0; }

  .chapters__head { display: block; margin-bottom: 1.125rem; font-size: 0.65625rem; letter-spacing: 0.14em; }
  .chapters ol { list-style: none; margin: 0; padding: 0; }
  .chapters > ol { counter-reset: rail; border-bottom: 1px solid var(--rule); }

  .chapters > ol > li > a {
    display: grid; grid-template-columns: 1.625rem 1fr; gap: 0.625rem; align-items: baseline;
    padding: 0.6875rem 0.125rem; border-top: 1px solid var(--rule);
    font-family: var(--mono); font-size: 0.65625rem; letter-spacing: 0.12em;
    text-transform: uppercase; color: var(--ink-soft); text-decoration: none;
    transition: color 0.25s var(--ease);
  }
  /* The number is the counter, never typed: the rail and the chapter heads
     count the same list, so they can never disagree. */
  .chapters > ol > li > a::before {
    counter-increment: rail; content: counter(rail, decimal-leading-zero);
    color: var(--ink-faint); font-variant-numeric: tabular-nums;
    transition: color 0.25s var(--ease);
  }
  .chapters > ol > li > a:hover, .chapters > ol > li > a:focus-visible { color: var(--ink); }
  .chapters > ol > li > a[aria-current="true"] { color: var(--ink); }
  .chapters > ol > li > a[aria-current="true"]::before { color: var(--accent); }

  /* Level two: the parts of a chapter, folded away until the reader is
     inside their parent. Animated with grid-template-rows so the height is
     never measured in script. Indented past the number, no rule of their
     own, and quieter. */
  .chapters__sub {
    display: grid; grid-template-rows: 0fr; opacity: 0;
    transition: grid-template-rows 0.4s var(--ease), opacity 0.3s var(--ease);
  }
  .chapters__sub > ol { overflow: hidden; min-height: 0; }
  .chapters li.is-open > .chapters__sub { grid-template-rows: 1fr; opacity: 1; }
  .chapters__sub a {
    display: block; padding: 0.3rem 0.125rem 0.3rem 2.375rem;
    font-family: var(--mono); font-size: 0.625rem; letter-spacing: 0.1em;
    text-transform: uppercase; color: var(--ink-faint); text-decoration: none;
    transition: color 0.25s var(--ease);
  }
  .chapters__sub li:first-child a { padding-top: 0.125rem; }
  .chapters__sub li:last-child a { padding-bottom: 0.875rem; }
  .chapters__sub a:hover, .chapters__sub a:focus-visible,
  .chapters__sub a[aria-current="true"] { color: var(--ink); }
}
/* ==========================================================================
   CHAPTER HEADINGS
   These were a mono label the size of a caption, which read as furniture
   rather than as the top of a chapter. Now: a numbered accent line, then the
   title in serif at heading size.
   ========================================================================== */

main { counter-reset: chapter; }

/* The counter is a line of its own above the title (Sept 2026: 32px
   heads, one ramp for the whole site). */
.section > h2 {
  font-family: var(--serif);
  font-size: 2rem; line-height: 1.15;
  font-weight: 500; letter-spacing: -0.025em; text-transform: none;
  color: var(--ink); text-wrap: balance;
  margin: 0 0 1.75rem; position: relative;
}
.section > h2::before {
  counter-increment: chapter;
  content: counter(chapter, decimal-leading-zero);
  display: block; margin-bottom: 0.625rem;
  font-family: var(--mono); font-size: 0.6875rem; font-weight: 500;
  line-height: 1; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--accent); font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   MUSIC, ROUND TWO
   ========================================================================== */

/* --- band blocks, aligned row for row --- */
/* Four parts per band – dates, name, roles, prose – and across the columns
   they should sit on the same four lines. Subgrid does it honestly; where it
   is missing the blocks just stack in normal flow. */
.block__when { display: block; margin: 0 0 0.55rem; }
.grid-blocks--aligned .block h3 { margin: 0 0 0.7rem; }
.grid-blocks--aligned .roles { margin: 0 0 0.9rem; }
.block__body p { margin: 0; color: var(--ink-soft); max-width: 54ch; }
.block__body p + p { margin-top: 0.8rem; }

/* The shared 22rem track is too wide for three of these to sit in one row
   inside a 74rem wrap, and the third band kept dropping to a row of its own
   where it could not line up with anything. These blocks are short, so they
   get a narrower track. */
.grid-blocks--aligned {
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
}

@supports (grid-template-rows: subgrid) {
  .grid-blocks--aligned { align-items: stretch; }
  .grid-blocks--aligned .block {
    display: grid; grid-template-rows: subgrid; grid-row: span 4; gap: 0;
  }
}

/* --- a quotation sitting inside a feed entry --- */
/* The one in the economic-model block had room; this one did not. */
.entry__body .statement { margin: 1.5rem 0; }

/* --- the manifestos, folded away under the posters --- */
.manifesto { border-top: 1px solid var(--rule); }
.manifesto:last-of-type { border-bottom: 1px solid var(--rule); }
.manifesto > summary {
  display: flex; align-items: baseline; gap: 0.75rem 1.25rem; flex-wrap: wrap;
  padding-block: 1.05rem; cursor: pointer; list-style: none;
  transition: color 0.25s var(--ease);
}
.manifesto > summary::-webkit-details-marker { display: none; }
.manifesto > summary::marker { content: ''; }
.manifesto > summary:hover { color: var(--accent); }
.manifesto__sign {
  order: -1; flex: 0 0 1.1rem; color: var(--accent);
  font-family: var(--mono); font-size: 0.8125rem; line-height: 1;
  /* The sign swings like a needle instead of swapping glyphs: the plus
     turns 135° into a saltire on opening, with a small compass overshoot. */
  display: inline-grid; place-items: center;
  width: 1.1rem; height: 1.1rem;
  rotate: 0deg;
  transition: rotate 0.55s cubic-bezier(0.34, 1.45, 0.45, 1);
}
.manifesto[open] .manifesto__sign { rotate: 135deg; }
.manifesto__sign::before { content: '+'; }
.manifesto__title { font-size: 1.0625rem; }
.manifesto__lead { margin-left: auto; }
.manifesto__body { padding: 0.25rem 0 1.9rem 1.85rem; max-width: 66ch; }
.manifesto__body p { margin: 0 0 0.8rem; color: var(--ink-soft); }
.manifesto__body p:last-child { margin-bottom: 0; }
.manifesto__body em { color: var(--ink); font-style: italic; }
.manifesto__note {
  margin-top: 1.25rem; padding-top: 1rem; border-top: 1px solid var(--rule);
}

/* --- the 2022 poster, printed and played --- */
.poster-swap { display: flex; flex-wrap: wrap; gap: 0.3rem; margin: 1rem 0 0; }
.poster-swap button {
  font-family: var(--mono); font-size: 0.625rem; letter-spacing: 0.1em;
  text-transform: uppercase; background: none; cursor: pointer;
  border: 1px solid var(--rule-firm); border-radius: 2px;
  padding: 0.3rem 0.5rem; color: var(--ink-faint);
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.poster-swap button:hover { color: var(--ink); }
.poster-swap button[aria-pressed="true"] {
  color: var(--accent); border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.edition__poster picture[hidden] { display: none; }
.manifestos__lead { color: var(--ink-soft); max-width: 60ch; margin: 0 0 1.5rem; }
.manifesto__body .label { display: block; }
.manifesto__byline { margin: 0 0 0.6rem; }
.manifesto__source { margin: 1rem 0 0; }

/* On a phone the manifesto indent costs more than it buys. */
@media (max-width: 40rem) {
  .manifesto__body { padding-left: 0; }
}

/* ==========================================================================
   PRODUCTION
   ========================================================================== */

/* --- the conference identities, one row per year --- */
/* Wide institutional banners, so they lie down in a list rather than trying
   to be pictures. Held back by default like every other corporate graphic. */
.banners { list-style: none; margin: 1.75rem 0 0; padding: 0; display: grid; gap: 1.25rem; }
.banners li {
  display: grid; grid-template-columns: minmax(0, 15rem) minmax(0, 1fr);
  gap: 0 clamp(1rem, 2.5vw, 2rem); align-items: center;
}
/* No cropping here. The banners are shown whole, at whatever ratio the file
   has, so a set exported at one consistent size simply lines up. */
.banners figure { margin: 0; overflow: hidden; background: var(--bg-sunk); border: 1px solid var(--rule); }
.banners img { width: 100%; filter: saturate(0.3) contrast(0.94) brightness(0.82); transition: filter 0.5s var(--ease); }
.banners li:hover img { filter: none; }
.banners .banner__what { color: var(--ink-soft); }
.banners .banner__what strong { color: var(--ink); font-weight: 500; }
.banners .label { display: block; margin-top: 0.3rem; }
@media (max-width: 40rem) {
  .banners li { grid-template-columns: minmax(0, 1fr); gap: 0.6rem; }
  /* max-width: 15rem lived here once — reviewed on a phone it read as a
     wasteful right margin, so the banners use the whole column now. */
}

/* --- a video with no still yet --- */
/* The four older films have no local poster frame in the archive. The card
   still works: it is a real click-to-load player, just without a picture. */
.video--blank { aspect-ratio: 16 / 9; display: grid; place-items: center; }
.entry--video .video--blank { aspect-ratio: auto; }
.video--blank .video__play {
  position: static; width: 100%; height: 100%;
  background: linear-gradient(160deg,
    color-mix(in srgb, var(--accent) 8%, var(--bg-sunk)), var(--bg-sunk));
}
.video--blank .video__play svg { color: var(--ink-soft); }
.video--blank:hover .video__play svg { color: var(--accent); }

/* ==========================================================================
   ABOUT
   ========================================================================== */

.about-hero {
  display: grid; grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3.5rem); align-items: end;
  padding-block: clamp(3rem, 8vh, 5rem) clamp(2rem, 4vh, 3rem);
}
.about-hero h1 {
  font-size: clamp(2.5rem, 4.8vw, 4.5rem); line-height: 1.03; font-weight: 500;
  letter-spacing: -0.026em; margin: 0.9rem 0 1.25rem; max-width: 18ch;
}
.about-hero p { font-size: clamp(1.0625rem, 0.5vw + 1rem, 1.1875rem); color: var(--ink-soft); max-width: 62ch; margin: 0; }
.about-hero p + p { margin-top: 0.9rem; }
.about-hero .label { display: block; }
.about-figure { margin: 0; align-self: end; }
.about-figure img { width: 100%; transition: transform 1.2s var(--ease); }
.about-figure:hover img { transform: scale(1.03); }
.about-figure figcaption { margin-top: 0.7rem; }
@media (max-width: 48rem) {
  .about-hero { grid-template-columns: minmax(0, 1fr); }
  .about-figure { max-width: 13rem; order: -1; }
}

/* --- a plain list of facts --- */
.facts { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.9rem; }
.facts li { display: grid; grid-template-columns: 8rem minmax(0, 1fr); gap: 0 clamp(0.75rem, 2vw, 1.75rem); align-items: baseline; }
.facts .fact__what { color: var(--ink-soft); max-width: 60ch; }
.facts .fact__what strong { color: var(--ink); font-weight: 500; }
@media (max-width: 40rem) { .facts li { grid-template-columns: minmax(0, 1fr); gap: 0.3rem; } }

/* The homepage feed hangs off this label, so it is the page's h2. Reset the
   heading's own metrics; the .label class does all the styling. */
h2.strand-bar__lead { margin: 0; font-weight: 400; }

/* Bring the homepage's closing block up to the weight the subpages carry. */
.about h2 {
  font-family: var(--serif);
  font-size: clamp(1.75rem, 1.7vw + 1.15rem, 2.5rem);
  font-weight: 500; letter-spacing: -0.025em; margin: 0 0 1.1rem;
}

/* ==========================================================================
   THE ABOUT PORTRAIT — a print proof
   Not the bevel again. Crop marks, a registration line that runs off into
   the margin, and a slug underneath: the marks a picture carries before it
   is trimmed. They spread on hover, as if the sheet were being placed.
   ========================================================================== */

.portrait-proof { position: relative; margin: 0; --tick: 1.75rem; --gap: 0.9rem; }
.portrait-proof > picture { display: block; overflow: hidden; background: var(--bg-sunk); position: relative; z-index: 1; }
.portrait-proof img { width: 100%; transition: transform 1.4s var(--ease); }
.portrait-proof:hover img { transform: scale(1.035); }

.proof__mark {
  position: absolute; width: var(--tick); height: var(--tick);
  border: 0 solid var(--accent-dim); pointer-events: none; z-index: 2;
  transition: inset 0.55s var(--ease), border-color 0.55s var(--ease), opacity 0.55s var(--ease);
  opacity: 0.75;
}
.proof__mark--tl { top: calc(-1 * var(--gap)); left: calc(-1 * var(--gap)); border-top-width: 1px; border-left-width: 1px; }
.proof__mark--tr { top: calc(-1 * var(--gap)); right: calc(-1 * var(--gap)); border-top-width: 1px; border-right-width: 1px; }
.proof__mark--bl { bottom: calc(-1 * var(--gap)); left: calc(-1 * var(--gap)); border-bottom-width: 1px; border-left-width: 1px; }
.proof__mark--br { bottom: calc(-1 * var(--gap)); right: calc(-1 * var(--gap)); border-bottom-width: 1px; border-right-width: 1px; }
.portrait-proof:hover .proof__mark { opacity: 1; border-color: var(--accent); }
.portrait-proof:hover .proof__mark--tl,
.portrait-proof:hover .proof__mark--tr { top: calc(-1.5 * var(--gap)); }
.portrait-proof:hover .proof__mark--bl,
.portrait-proof:hover .proof__mark--br { bottom: calc(-1.5 * var(--gap)); }
.portrait-proof:hover .proof__mark--tl,
.portrait-proof:hover .proof__mark--bl { left: calc(-1.5 * var(--gap)); }
.portrait-proof:hover .proof__mark--tr,
.portrait-proof:hover .proof__mark--br { right: calc(-1.5 * var(--gap)); }

/* There was a registration line here, running out of the frame to the left.
   It kept landing across the face at some widths, so it is gone: the crop
   marks and the slug carry the print-proof idea on their own. */

.proof__slug {
  display: flex; justify-content: space-between; gap: 1rem;
  margin-top: calc(var(--gap) + 0.7rem);
}
@media (max-width: 48rem) {
  .portrait-proof { --tick: 1.1rem; --gap: 0.6rem; }
}

/* A part of a chapter rather than a chapter in its own right: no number, and
   a heading a size down, so the rail's two levels and the page agree. */
.section--part > h2 { padding-top: 0; font-size: clamp(1.375rem, 1vw + 1rem, 1.875rem); }
.section--part > h2::before { content: none; }

/* ==========================================================================
   404 — OFF THE MAP
   The one page nobody is meant to reach, so it gets to be the loudest.
   One twelve-point wind rose after the Vento disc, anchored to the centre of
   the screen, turning about its own centre as the wheel moves. The ten winds
   drift across behind it, and a needle follows the pointer. Everything stops
   dead under prefers-reduced-motion.
   ========================================================================== */

.lost {
  position: relative;
  /* Pulled up under the sticky header so the stage is exactly the viewport:
     its centre is then the screen's centre, and the nought lands on it. */
  margin-top: calc(-1 * var(--header-h));
  /* dvh, so a phone's retracting toolbar does not push the rose off-centre */
  min-height: 100vh;
  min-height: 100dvh;
  display: grid; place-items: center; overflow: hidden;
  background: radial-gradient(ellipse at 50% 50%,
    color-mix(in srgb, var(--accent) 7%, var(--bg-sunk)) 0%, var(--bg-sunk) 62%, var(--bg) 100%);
}

/* --- the rose --- */
.lost__rose {
  position: absolute; top: 50%; left: 50%; z-index: 0;
  width: min(104vh, 92vw); aspect-ratio: 1;
  translate: -50% -50%;
  rotate: var(--turn, 0deg);          /* the wheel writes this */
  opacity: 0.5; pointer-events: none;
}
/* The idle drift lives on the inner element, because an animation on `rotate`
   would otherwise override the wheel's own rotation on the same property. */
.lost__rose svg {
  display: block; width: 100%; height: 100%; overflow: visible;
  animation: rose-turn 420s linear infinite;
}
.rose-star { fill: color-mix(in srgb, var(--accent) 15%, transparent);
             stroke: var(--accent-dim); stroke-width: 1; vector-effect: non-scaling-stroke; }
.rose-tick { fill: none; stroke: var(--rule-firm); stroke-width: 1; vector-effect: non-scaling-stroke; }
.rose-core { fill: var(--bg-sunk); stroke: var(--accent-dim); stroke-width: 1;
             vector-effect: non-scaling-stroke; }
@keyframes rose-turn { to { rotate: 360deg; } }

/* --- the needle, on the same centre --- */
/* Sheathed until called for: it draws itself out from the centre toward
   whatever link the hand is weighing — snapped out like lightning, put
   away almost as fast. The aim is set while it is hidden, so it never
   visibly swings; it only ever strikes. */
.lost__needle {
  position: absolute; top: 50%; left: 50%; z-index: 1;
  width: min(40vh, 34vw); height: 2px; translate: 0 -50%;
  transform-origin: 0 50%; rotate: var(--deg, 0deg);
  scale: 0 1;
  transition: scale 0.13s cubic-bezier(0.2, 0.9, 0.25, 1);
  pointer-events: none;
}
.lost__needle.is-out { scale: 1 1; }
.lost__needle::before {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(to right, transparent, var(--accent) 55%, var(--accent));
  clip-path: polygon(0 40%, 88% 0, 100% 50%, 88% 100%, 0 60%);
}
.lost__needle::after {
  content: ''; position: absolute; left: -5px; top: 50%; translate: 0 -50%;
  width: 10px; height: 10px; border-radius: 50%;
  background: var(--bg); border: 1px solid var(--accent);
}

/* --- the winds --- */
.lost__winds { position: absolute; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
.lost__winds b {
  position: absolute; left: 0; white-space: nowrap; font-weight: 400;
  font-family: var(--mono); text-transform: uppercase;
  color: var(--ink-faint); opacity: 0.45;
  animation: wind-drift linear infinite;
  animation-duration: var(--dur, 60s); animation-delay: var(--delay, 0s);
  top: var(--y, 50%); font-size: var(--size, 0.75rem); letter-spacing: var(--track, 0.4em);
}
@keyframes wind-drift {
  from { transform: translateX(-40vw); }
  to   { transform: translateX(120vw); }
}

/* --- the text --- */
/* Only the numerals are in flow, so the grid centres them and the nought
   lands on the rose. The rest hangs underneath, out of flow. */
.lost__inner { position: relative; z-index: 2; text-align: center; }
.lost__code {
  position: relative; margin: 0;
  font-family: var(--serif); font-weight: 500;
  font-size: clamp(6rem, 26vw, 20rem); line-height: 0.8; letter-spacing: -0.05em;
  color: var(--ink);
  text-shadow: 0 0 70px color-mix(in srgb, var(--bg) 85%, transparent);
}
.lost__digit { display: inline-block; animation: lost-sway 9s ease-in-out infinite; }
.lost__digit:nth-of-type(2) { animation-delay: -3s; }
.lost__digit:nth-of-type(3) { animation-delay: -6s; }
@keyframes lost-sway {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-0.06em) rotate(-1.2deg); }
}
.lost__say {
  position: absolute; top: 100%; left: 50%; translate: -50% 0;
  width: min(46ch, 86vw); margin-top: 2rem;
}
.lost__say h1 {
  font-family: var(--serif); font-weight: 500;
  font-size: clamp(1.25rem, 2vw + 1rem, 2rem); letter-spacing: -0.02em;
  margin: 0 0 0.9rem; text-wrap: balance;
}
.lost__say p { color: var(--ink-soft); margin: 0; text-wrap: pretty; }
.lost__back { margin-top: 2.5rem !important; display: flex; flex-wrap: wrap;
              gap: 0.75rem 2rem; justify-content: center; }
.lost__back a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; }

@media (prefers-reduced-motion: reduce) {
  .lost__rose svg, .lost__winds b, .lost__digit { animation: none !important; }
  .lost__needle { transition: none; }
}

/* ==========================================================================
   THE WEATHER
   The facelift layer. The 404 was the only page with the wind in it; these
   rules let it out. Everything here sits on top of the settled layout — no
   text string changes, no class renames, and the text round trip still lands.
   Every moving part stops under prefers-reduced-motion: the global rule at
   the top flattens the transitions and animations, and site.js does not
   start the canvas or hand out reveal classes at all.
   ========================================================================== */

/* --- grain ----------------------------------------------------------------
   A fixed sheet of photographic grain; the page scrolls underneath it. This
   is what stops the grounds reading as flat hex fields — the same reason the
   photography survives being this dark. Kept under 5%: felt, not seen. */
body::after {
  content: ''; position: fixed; inset: 0; z-index: 90; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  opacity: 0.04;
}
:root[data-theme="light"] body::after { opacity: 0.05; }

/* --- small matter ---------------------------------------------------------- */
::selection { background: color-mix(in srgb, var(--accent) 30%, transparent); }
html { scrollbar-color: var(--rule-firm) var(--bg); }

/* --- crossing between pages -------------------------------------------------
   Same-origin navigations cross-fade instead of flashing white. The header
   is named, so it holds still while the page changes underneath it; browsers
   without view transitions simply navigate, which is the correct fallback. */
@view-transition { navigation: auto; }
.site-header { view-transition-name: site-header; }
@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: vt-leave 0.2s var(--ease) both; }
  ::view-transition-new(root) { animation: vt-arrive 0.34s var(--ease) both; }
}
@keyframes vt-leave  { to { opacity: 0; } }
@keyframes vt-arrive { from { opacity: 0; transform: translateY(0.5rem); } }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) {
    animation-duration: 0.01ms !important;
  }
}

/* --- the compass -------------------------------------------------------------
   A small wind rose ahead of the wordmark, on every page. The needle is the
   scroll position — north at the top, a full turn by the foot of the page —
   so the site carries its own instrument instead of a progress bar. Injected
   by site.js; decorative, and invisible to a screen reader. */
.compass { flex: 0 0 auto; width: 1.2rem; height: 1.2rem; margin-right: -0.55rem; opacity: 0.9; }
/* Where there is room, the instrument earns its place. */
@media (min-width: 64rem) {
  .compass { width: 1.6rem; height: 1.6rem; margin-right: -0.35rem; opacity: 1; }
}
.compass svg { display: block; width: 100%; height: 100%; overflow: visible; }
.compass__ring { fill: none; stroke: var(--rule-firm); vector-effect: non-scaling-stroke; }
.compass__tick { fill: none; stroke: var(--rule-firm); vector-effect: non-scaling-stroke; }
.compass__n   { fill: var(--accent); }
.compass__s   { fill: var(--ink-faint); opacity: 0.55; }
.compass__hub { fill: var(--bg); stroke: var(--accent); vector-effect: non-scaling-stroke; }
@media (max-width: 21rem) { .compass { display: none; } }

/* --- the wind ----------------------------------------------------------------
   A canvas behind every hero: fine streaklines advected through a slowly
   drifting flow field, the engraving hairlines of the Šagra posters given
   somewhere to go. The pointer stirs it. site.js draws at very low alpha in
   the page's own ink, with the odd verdigris strand; under reduced motion it
   renders one still etching and stops. */
.hero, .page-hero, .about-hero { position: relative; }
.hero > :not(.wind), .page-hero > :not(.wind), .about-hero > :not(.wind) {
  position: relative; z-index: 1;
}
.wind {
  /* A canvas is a replaced element: left+right alone will not stretch it,
     so the box is stated outright — the hero's own box plus the bleed out
     to both viewport edges, and a little past its foot. The whisper of
     blur is the depth of field: the weather sits a plane behind the page,
     soft under sharp type, and it melts the last trace of polyline out of
     the strands. On the content pages the canvas reaches well past the
     hero and the ENGINE dissolves the strands over that tail (density and
     ink both), so the weather settles into the page instead of stopping
     at a line — the 404 keeps its exact stage. */
  position: absolute; z-index: 0; display: block;
  top: 0; left: calc(var(--gutter) - var(--bleed));
  width: calc(100% + (var(--bleed) - var(--gutter)) * 2);
  height: calc(100% + 1.5rem);
  pointer-events: none;
  filter: blur(1.15px);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 3.5rem, #000 calc(100% - 2.5rem), transparent);
          mask-image: linear-gradient(to bottom, transparent, #000 3.5rem, #000 calc(100% - 2.5rem), transparent);
}
/* (The extended-dissolve experiment was reverted on review: the canvas is
   the hero's own box again. The engine's falloff code is inert at this
   height — the fade zone is the same 1.5rem it always was.) */
/* The portrait's calm is a halo, not a mask stripe. A dimming band cut
   into the canvas kept missing: it was measured in percentages of the
   viewport while the face is measured by the grid, and a gust could carry
   strands into whatever the band spared. This sheet of ground colour sits
   behind the portrait itself — injected by site.js, between the canvas and
   the picture — so wherever the portrait goes, the air around it goes calm,
   and strands blown at the face dissolve before they arrive. The rail's
   veil does the same job on the subpage heroes. */
.portrait-calm {
  position: absolute; z-index: -1; pointer-events: none;
  inset: -26% -36% -28% -42%;
  background: radial-gradient(ellipse closest-side at 52% 45%,
    var(--bg) 56%, color-mix(in srgb, var(--bg) 78%, transparent) 74%, transparent 96%);
}
@media (max-width: 44rem) {
  /* Below this the portrait sits top-left and small; the right-hand thinning
     would only dim open sky. */
  .hero .wind {
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 3.5rem, #000 calc(100% - 2.5rem), transparent);
    -webkit-mask-composite: source-over;
    mask-image: linear-gradient(to bottom, transparent, #000 3.5rem, #000 calc(100% - 2.5rem), transparent);
    mask-composite: add;
  }
}

/* --- arrival -------------------------------------------------------------------
   The first screen assembles itself: eyebrow, then the headline, then the
   rest, each rising a line's height. Runs once per visit to the page. */
@keyframes rise {
  from { opacity: 0; transform: translateY(0.9rem); }
  to   { opacity: 1; transform: none; }
}
.hero__eyebrow,
.page-hero > .label,
.about-hero .label                    { animation: rise 0.7s var(--ease) both; }
.hero h1, .page-hero h1, .about-hero h1 { animation: rise 0.7s var(--ease) 0.08s both; }
.hero p, .page-hero p, .about-hero p    { animation: rise 0.7s var(--ease) 0.18s both; }
.hero__portrait, .about-figure          { animation: rise 0.9s var(--ease) 0.26s both; }


/* --- reveals ---------------------------------------------------------------
   Blocks below the fold rise a line's height as the reader reaches them.
   site.js hands out .reveal only when motion is allowed, and only to what is
   still under the fold at load — the first screen never waits for it. If the
   script does not arrive, nothing is ever hidden. */
.reveal { opacity: 0; translate: 0 1.1rem; transition: opacity 0.8s var(--ease), translate 0.8s var(--ease); transition-delay: var(--stagger, 0s); }
.reveal.is-in { opacity: 1; translate: 0 0; }

/* Entries rise differently: the BOX stands still — so its top divider
   stays put while it draws — and only the CONTENTS rise the 1.1rem into
   it. The media band's angled corner therefore genuinely collides with
   the divider at the moment the rise completes, and that collision is
   what triggers the blade strike (site.js listens for the transition
   end). The hidden state lives on :not(.is-in), so the settled state
   falls back to the stylesheet and hover keeps its say. */
.entry.reveal { opacity: 1; translate: none; }
.entry.reveal .entry__body {
  transition: opacity 0.8s var(--ease), translate 0.8s var(--ease);
  transition-delay: var(--stagger, 0s);
}
.entry.reveal .entry__media {
  transition: opacity 0.9s var(--ease), translate 0.9s var(--ease),
              filter 1.3s var(--ease),
              -webkit-mask-position 0.7s var(--ease),
              mask-position 0.7s var(--ease);
  transition-delay: var(--stagger, 0s);
}
.entry.reveal:not(.is-in) .entry__body {
  opacity: 0; translate: 0 1.1rem;
}
/* The picture RESOLVES as it arrives — the hero's depth-of-field language
   carried down the page: it rises out of a soft blur the way a strand
   sits a plane behind the type. */
.entry.reveal:not(.is-in) .entry__media {
  opacity: 0; translate: 0 1.1rem; filter: blur(9px);
}

/* --- rules that draw themselves ----------------------------------------------
   The full-bleed hairlines print left to right as their section arrives,
   instead of simply being there. The line grows as a background, so the
   mask that parts it around the chapter rail never moves. */
@media (prefers-reduced-motion: no-preference) {
  .section.rule-draw::before, .entry.rule-draw::before,
  .feed-tail.rule-draw::before, .feed-tail.rule-draw::after {
    background-color: transparent;
    background-image: linear-gradient(var(--rule), var(--rule));
    background-repeat: no-repeat; background-position: 0 0; background-size: 0% 100%;
    transition: background-size 1.5s var(--ease) 0.1s;
  }
  .section.rule-in::before, .entry.rule-in::before,
  .feed-tail.rule-in::before, .feed-tail.rule-in::after {
    background-size: 100% 100%;
  }
  /* Entry dividers sweep LINEARLY and briskly: the strike is timed to
     the moment the drawing tip reaches the blade's corner, and a constant
     speed is the only sweep a timer can honestly follow. Sections keep
     their slower ease. */
  .entry.rule-draw::before { transition-timing-function: linear; transition-duration: 0.9s; }
}
/* The sweep is welcome everywhere again: with the rail's veil doing the
   parting, a printing line slides under the feather and out the far side
   instead of popping through a static hole. */

/* --- the lead entry -----------------------------------------------------------
   The markup has carried data-weight="lead" from the start; the stylesheet
   finally honours it. One entry per feed gets record-sleeve scale, and the
   column stops reading as sixteen identical stops. */
.entry[data-weight="lead"] h3 {
  font-size: clamp(1.75rem, 1.8vw + 1.05rem, 2.75rem); letter-spacing: -0.022em;
}

/* --- the bevel reaches the small furniture -------------------------------------
   The cut corner was spent on the portrait and nowhere else. Now everything
   a reader picks up — chips, filters, the two header buttons, the CV card —
   carries the same signature, and border-radius leaves the site entirely.

   The notch is a ground-coloured triangle laid over the corner, not a
   clip-path: clipping the box shaved its own right border off at some
   rendered widths, so whether a chip kept its right-hand line depended on
   the fraction of a pixel its label happened to measure. */
.strand, .poster-swap button, .award, .rows--awards .row__when, .edition__year, .icon-btn, .cv-card {
  border-radius: 0; position: relative; --cut: 0.55rem;
}
.icon-btn { --cut: 0.6rem; }
.cv-card { --cut: 1.4rem; }
/* The triangle overshoots the box by 2px up and to the right. It grows along
   its own diagonal, so the bevel line is exactly where it was – but Firefox
   at fractional zoom placed the old 1px overlap a sliver short, and the
   right border showed through the whole height of the cut. */
.strand::after, .poster-swap button::after, .award::after,
.rows--awards .row__when::after, .edition__year::after, .icon-btn::after, .cv-card::after {
  content: ''; position: absolute; top: -2px; right: -2px;
  width: calc(var(--cut) + 2px); height: calc(var(--cut) + 2px);
  background: var(--bg); pointer-events: none;
  clip-path: polygon(0 0, 100% 0, 100% 100%);
}
.video__out, .skip-link { border-radius: 0; }

/* --- the header, once the page moves ------------------------------------------
   The ground sinks to the deeper tone and the rule under it firms; the
   padding closes at the same time (see THE COLLAPSE in the header region). */
[data-scrolled="true"] .site-header {
  background: color-mix(in srgb, var(--bg-sunk) 90%, transparent);
  border-bottom-color: var(--rule-firm);
}
.site-header { transition: top 0.4s var(--ease), background-color 0.4s var(--ease), border-color 0.4s var(--ease); }

/* --- the rose at the foot -------------------------------------------------------
   Every page ends where the 404 lives: the Vento rose, half off the sheet,
   turning far too slowly to catch. Injected by site.js on every page except
   the 404, which already owns the full-size one. */
.site-footer { position: relative; overflow: clip; }
.footer-rose {
  position: absolute; top: 50%; right: -4.5rem; translate: 0 -50%;
  width: 17rem; aspect-ratio: 1; opacity: 0.45; pointer-events: none;
}
/* display:block, or the inline svg sits on a text baseline and the whole
   rose rides ~3px above the box centre the rotation pivots on — measured
   as a 3px wobble circle before anyone could name it. */
.footer-rose svg { display: block; width: 100%; height: 100%; overflow: visible; animation: rose-turn 560s linear infinite; }
/* The rose after the Malorshiga setlists (Sept 2026): hairlines throughout –
   sixteen slender points with a spine each, a ring graduated every 2.5
   degrees, a heavier engraved band just outside it, and a hub. The geometry
   is built by site.js; these are only its inks. */
.rose-band { fill: var(--accent-dim); opacity: 0.5; }
.rose-ring, .rose-ticks, .rose-rays, .rose-spines, .rose-hub {
  stroke: var(--accent-dim); stroke-width: 1; vector-effect: non-scaling-stroke;
}
.rose-ring, .rose-ticks, .rose-spines { fill: none; }
.rose-ticks { opacity: 0.75; }
.rose-rays { fill: color-mix(in srgb, var(--accent) 9%, transparent); stroke-linejoin: miter; }
.rose-spines { opacity: 0.55; }
.rose-hub { fill: var(--bg); }
.rose-dot { fill: var(--accent-dim); }
/* On a phone the sheet is narrow enough that a 12rem rose reached across
   the links and turned behind the words. It is a watermark in the corner,
   never a layer over type: smaller, pushed further off, and dimmer. */
@media (max-width: 44rem) {
  .footer-rose { width: 9rem; right: -3.75rem; opacity: 0.3; }
}

/* A click on any filter settles the reader at the top of the feed; the hero
   has the whole first screen, so the band is also the way down. (Its own
   heading is invisible by design — see "strand bar" above.) */
.strand-bar { scroll-margin-top: calc(var(--header-h) + 1rem); }

/* --- margin notes, homepage only ------------------------------------------------
   On the subpages the left margin belongs to the chapter rail. The homepage
   has no rail, and at rail widths its margin stood empty while every
   photograph ran off the right edge — the whole sheet leaned right. Each
   entry's kind and date now hang in the margin instead, book-fashion, on
   the same column the rail keeps everywhere else.
   (Sept 2026: the margin rail is gone – the two numbers it measured against
   live here now, and this rule leaves with the old feed when the Ledger
   lands, Phase 2.) */
@media (min-width: 90rem) {
  :root:not(:has(.chapters)) .entry .entry__meta {
    --rail-w: 9.25rem;
    --rail-l: clamp(0.75rem, calc(var(--bleed) - 10.25rem), 3.5rem);
    position: absolute; top: 0.3rem;
    left: calc(-1 * var(--bleed) + var(--rail-l));
    width: var(--rail-w); margin: 0;
    flex-direction: column; align-items: flex-start; gap: 0.45rem;
  }
}

/* --- the photograph meets the words -------------------------------------------------
   At full width the reader's eye rests near the centre of the screen, and
   the centre kept landing on nothing: text finished to its left, the
   photograph began to its right, and the focal line ran down a blank
   channel. The pictures reach further left under a longer, deeper
   dissolve, arriving beneath the prose the way they do at tablet width —
   the text column itself stays on the wrap's margin, where it belongs.
   (An indent was tried and retired: a column floating off the margin
   reads as an accident, not a decision.) */
/* (The 90rem deep-dissolve override retired with the dissolve itself: the
   diagonal edge holds its line at the text column, and the growing bleed
   closes the centre channel on wide screens on its own.) */

/* --- the 404 fits the screen ------------------------------------------------------
   Header, stage and footer share one viewport with nothing left over: the
   page that tells you there is nothing here should not have anywhere to
   scroll to. The stage gives, the rose stays clipped by design, and the
   compass points north because the page has no depth to measure. */
body:has(.lost) {
  min-height: 100vh; min-height: 100dvh;
  display: flex; flex-direction: column;
}
body:has(.lost) main {
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
}
body:has(.lost) .lost {
  flex: 1 1 auto; min-height: 0; margin-top: 0;
}
/* The stage is one viewport, so every rem the footer gives back is air
   between the two links and the rule above it; the composition also sits
   a breath higher than true centre. */
body:has(.lost) .site-footer { padding-block: 1.1rem 1.3rem; }
body:has(.lost) .lost__inner { translate: 0 -1.4rem; }

/* The deep end of the easter egg: click the compass off the map and every
   word steps aside — nothing left but the rose, the water, and the hand.
   The compass itself stays, because it is the way back. */
body:has(.lost) .wordmark,
body:has(.lost) .site-header .nav,
body:has(.lost) .header-tools,
body:has(.lost) .lost__inner,
body:has(.lost) .lost__winds,
body:has(.lost) .site-footer {
  transition: opacity 0.7s var(--ease);
}
body.lost-play .wordmark,
body.lost-play .site-header .nav,
body.lost-play .header-tools,
body.lost-play .lost__inner,
body.lost-play .lost__winds,
body.lost-play .site-footer {
  opacity: 0; pointer-events: none;
}

/* --- the chapter strip ----------------------------------------------------
   Below 68.75rem the rail column has nowhere to stand, and Music is fourteen
   thousand pixels tall on a phone. The same chapters lie flat in a slim
   strip appended to the sticky header: mono caps, the one the reader is
   inside in ink with a verdigris hairline under it, the rest faint. The
   list scrolls sideways where it must, under feathered edges and with no
   scrollbar — the lit chapter keeps itself centred from site.js. */
.chapter-strip { display: none; border-top: 1px solid var(--rule); }
.chapter-strip ol {
  list-style: none; margin: 0; padding: 0;
  display: flex; align-items: stretch; gap: 1.7rem;
  height: var(--strip-h, 2.4rem);
  padding-inline: var(--gutter);
  overflow-x: auto; scrollbar-width: none; position: relative;
  /* The fade is state, not decoration — see THE SCROLLING EDGE. */
}
.chapter-strip ol::-webkit-scrollbar { display: none; }
/* The list items must hand their height down, or the labels ride the top
   of the strip while the space pools below them. Between items, a quiet
   tick — the same mark the roles list uses. */
.chapter-strip ol { gap: 0; }
.chapter-strip li { display: flex; align-items: stretch; }
.chapter-strip li + li::before {
  content: "·"; align-self: center; color: var(--rule-firm);
  margin-inline: 0.85rem;
}
.chapter-strip a {
  position: relative; display: flex; align-items: center;
  font-family: var(--mono); font-size: 0.65rem; letter-spacing: 0.13em;
  text-transform: uppercase; white-space: nowrap;
  color: var(--ink-faint);
  /* The mark of the current chapter hugs its own label — an underline a
     few pixels under the text — not the strip's distant floor, which read
     as a misaligned rule. */
  text-decoration: underline 1px transparent;
  text-underline-offset: 0.5em;
  transition: color 0.3s var(--ease), text-decoration-color 0.4s var(--ease);
}
.chapter-strip a:hover { color: var(--ink-soft); }
.chapter-strip a[aria-current="true"] { color: var(--ink); text-decoration-color: var(--accent); }
@media (max-width: 68.6875rem) {
  :root:has(.chapters) { --strip-h: 2.4rem; }
  .chapter-strip { display: block; }
  /* The open mobile menu must cover the strip, not slide beneath it. */
  .nav { z-index: 2; }
}

/* --- the feedback round of 2026-08-25: mobile manners ---------------------- */

/* A tap leaves no standing highlight box; keyboard focus is untouched
   (the :focus-visible outline above still fires for keyboards only). */
body { -webkit-tap-highlight-color: transparent; }

/* The strand bar rides sideways on a phone like the chapter strip does,
   instead of wrapping into ragged rows. */
/* (The strand bar no longer travels sideways at any width — its four filters
   wrap instead, so none of them can hide. See "strand bar" above.) */

/* --- THE SCROLLING EDGE ----------------------------------------------------
   A fade that is always on is not a signal: it looks the same whether more
   is waiting or not, and it dims the first item at rest for nothing. site.js
   marks each sideways row with the side that still holds content, so the
   fade only ever appears where there IS more to see, changes sides as the
   row travels, and lifts completely once everything fits. The fade is wide
   enough to read as a fade rather than a clipped word. */
.scroll-row { --edge: 2.4rem; }
.scroll-row[data-edge="none"] { -webkit-mask-image: none; mask-image: none; }
.scroll-row[data-edge="right"] {
  -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - var(--edge)), transparent);
  mask-image: linear-gradient(90deg, #000 calc(100% - var(--edge)), transparent);
}
.scroll-row[data-edge="left"] {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--edge));
  mask-image: linear-gradient(90deg, transparent, #000 var(--edge));
}
.scroll-row[data-edge="both"] {
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 var(--edge),
                        #000 calc(100% - var(--edge)), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 var(--edge),
                #000 calc(100% - var(--edge)), transparent);
}

/* The hero eyebrows break in balance, not wherever the line runs out. */
.page-hero > .label, .hero__eyebrow { text-wrap: balance; }

/* Two rows in the site justify a label against a set of links: the feed's
   way out ("Or go straight to") and the footer. Edge to edge that reads as
   a printed line; on a phone it came apart — the auto margin threw the
   FIRST link to the far edge and the rest wrapped underneath, aligned with
   nothing. Below 44rem the label takes a line of its own and the links form
   one even row beneath it, left-aligned like everything else on the page.
   The row gap opens up too: these are the smallest tap targets here. */
@media (max-width: 44rem) {
  .jump { gap: 1rem 1.5rem; }
  .jump .label, .site-footer .label { flex-basis: 100%; margin-right: 0; }

  /* The footer goes further: not a wrapped row but a plain left-aligned
     column, one door per line, in the order they are actually used —
     address, the two social doors, the record shop. A short stack of
     single lines is easier to hit and easier to read down than two ragged
     rows, and the pair unpacks with it. */
  .site-footer__inner { flex-direction: column; align-items: flex-start; gap: 0.85rem; }
  /* Two classes deep on purpose: the base .footer-elsewhere rule (the pair's
     wide-screen inline-flex, gap 2.25rem) is declared further down the sheet
     and would otherwise win on source order and space these two apart. */
  .site-footer .footer-elsewhere {
    display: flex; flex-direction: column; gap: 0.85rem; align-items: flex-start;
    margin-left: 0;   /* the wide-screen auto margin pushed the pair right */
  }
  .site-footer .label { margin-bottom: 0.15rem; }
}

/* The CLARIN banners use the whole width of a phone. */
@media (max-width: 48rem) {
  .banners li { grid-template-columns: 1fr; gap: 0.6rem 0; }
}

/* The carousel caption holds two lines of room, so a wrapping description
   no longer bounces the footer; and the gallery keeps its distance. */
.gallery__foot { min-height: 4.1rem; align-items: start; padding-block: 0.85rem 1.1rem; }
.gallery { margin-bottom: clamp(1.5rem, 4vw, 3rem); }

/* The footer's two elsewhere-links travel as a pair, and the pair keeps the
   far side of the sheet to itself. */
.footer-elsewhere { display: inline-flex; gap: 2.25rem; margin-left: auto; }

/* The escape hatch above a playing video furls once it has said its piece. */
.video__out { transition: opacity 0.6s var(--ease); }
.video__out.is-furled { opacity: 0; pointer-events: none; }

/* --- the editions, folded by year ----------------------------------------- */
.edition-picker { display: flex; flex-wrap: wrap; gap: 0.4rem 0.5rem; margin: 0 0 2rem; }
.edition-picker button {
  font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase;
  background: none; border: 1px solid transparent; border-radius: 2px;
  padding: 0.4rem 0.75rem; color: var(--ink-faint); cursor: pointer;
  transition: color 0.25s var(--ease), border-color 0.25s var(--ease), background-color 0.25s var(--ease);
}
.edition-picker button:hover { color: var(--ink); border-color: var(--rule-firm); }
.edition-picker button[aria-pressed="true"] {
  color: var(--accent); border-color: var(--accent-dim);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.edition[hidden] { display: none; }
/* The picker is a phone tool: at desktop the four editions stand side by
   side as they always did, and the chips never appear. */
@media (min-width: 48rem) {
  .edition-picker { display: none; }
}
/* Folding is soft: what stays arrives, rather than appearing. */
.editions.is-refolding .edition:not([hidden]) { animation: edition-in 0.4s var(--ease); }
@keyframes edition-in {
  from { opacity: 0; translate: 0 0.6rem; }
}
/* Stacked editions get a rule between them, so one archive year does not
   run into the next — but only between VISIBLY adjacent editions. The
   plain sibling selector counted hidden years too, so a picked 2018
   carried a rule while a picked 2017 did not, and switching years made
   the page jump. */
@media (max-width: 48rem) {
  .edition:not([hidden]) + .edition:not([hidden]) {
    border-top: 1px solid var(--rule); padding-top: 1.9rem;
  }
}

/* The 2022 posters trade places by light, not by jump cut. Scoped to
   pictures that carry a bill, so the other editions' single posters are
   never touched. */
.edition__poster { display: grid; }
.edition__poster picture[data-bill] {
  grid-area: 1 / 1;
  transition: opacity 0.5s var(--ease);
}
.edition__poster picture[data-bill]:not(.is-shown) { opacity: 0; pointer-events: none; }

/* The manifestos open and close as a motion, not a teleport — driven from
   site.js (measured height, so it works in every engine), the body just
   clips while it travels. */
.manifesto__body { overflow: clip; }

/* Bands and their roles read as a list again: a tick between the roles, so
   "Creative lead" and "Drums" are two things, not one broken phrase — and
   a role never breaks inside itself. */
.roles { column-gap: 0.55rem; }
.roles li { white-space: nowrap; }
/* The separator TRAILS its role instead of leading the next one. Leading, it
   was the first thing on a wrapped line — "· DIRECTION" reads as a bullet,
   not as a continuation. Trailing, a wrap always leaves the dot at the end
   of the line above, where it says "there is more". */
.roles li:not(:last-child)::after {
  content: "·"; color: var(--rule-firm); margin-left: 0.55rem;
}

/* The CLARIN banners open like everything else does. */
.banners figure { width: 100%; }
.banners .zoomable { cursor: zoom-in; }

/* (.entry__media--fit lived here: a capped, top-pinned figure for entries
   whose body ran long. It was a patch on the symptom — the one entry that
   used it now uses .entry--shelf below, which fixes the cause by giving the
   list a row of its own. Removed with its last user.) */

/* --- an entry that carries a list -------------------------------------------
   "Six annual conferences" is prose, a photograph AND a list of six banners.
   Inside the ordinary entry layout the list was squeezed into the 34rem text
   column while the capped photograph ran out half way down, so the lower
   right of the sheet stood empty and the two halves never met. Here the top
   row behaves like every other entry — words left, picture right, running
   off the edge of the sheet — and the list takes the full width beneath them
   both, two abreast, so six years read as three lines instead of six. */
.entry__shelf { margin-top: clamp(1.75rem, 4vw, 2.5rem); }
.entry__media figcaption { margin-top: 0.7rem; }

/* Paragraphs inside an entry are paragraphs: most Production entries run to
   two or three, and at zero margin they read as one block. */
.entry__body p:not([class]) + p:not([class]) { margin-top: 0.8rem; }

/* Where entries stack (below the desktop layout), the last one in a chapter
   drops its own bottom padding: added to the chapter's, it left a gap twice
   the size of every other chapter ending. */
@media (max-width: 72rem) {
  .section > .entry:last-child { padding-bottom: 0; }
}

@media (min-width: 72.0625rem) {
  .entry--shelf {
    display: grid;
    grid-template-columns: minmax(0, 34rem) minmax(0, 1fr);
    column-gap: clamp(2rem, 4vw, 3.5rem);
    row-gap: clamp(2rem, 4vw, 3rem);
  }
  .entry--shelf .entry__body { grid-area: 1 / 1; max-width: none; }
  .entry--shelf .entry__media {
    grid-area: 1 / 2;
    position: relative; inset: auto; width: auto;
    /* Same reach as the absolute band it replaces: --bleed is exactly the
       distance from the wrap's content edge to the edge of the screen. */
    margin-right: calc(-1 * var(--bleed));
    display: flex; flex-direction: column;
  }
  .entry--shelf .entry__shelf { grid-area: 2 / 1 / 3 / -1; margin-top: 0; }

  /* The caption sits below the plate, so the waterline and its hover
     hairline move off the figure and onto the picture — masked as part of
     the figure, the caption fades out at its own left edge (it did).
     Doubled class on purpose: the rule being overridden is
     `.entry:not(.entry--panorama) .entry__media`, and the :not() argument
     counts toward specificity, so a two-class selector cannot win here. */
  .entry.entry--shelf .entry__media { -webkit-mask-image: none; mask-image: none; }
  .entry.entry--shelf .entry__media::before { content: none; }
  .entry--shelf .entry__media picture {
    display: block; position: relative;
    /* The shared rule puts height:100% on the picture, which inside a flex
       column resolves against an indefinite height and left the box 52px
       taller than the photograph inside it — enough to push the caption off
       its own plate. Here the picture is exactly its picture. */
    height: auto; flex: 0 0 auto;
    -webkit-mask-image: var(--waterline); mask-image: var(--waterline);
    -webkit-mask-size: calc(100% + 12rem) 100%; mask-size: calc(100% + 12rem) 100%;
    -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
    -webkit-mask-position: 0 0; mask-position: 0 0;
    transition: -webkit-mask-position 0.7s var(--ease), mask-position 0.7s var(--ease);
  }
  .entry--shelf:hover .entry__media picture {
    -webkit-mask-position: -12rem 0; mask-position: -12rem 0;
  }
  .entry--shelf .entry__media picture::after {
    content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 1px;
    background: var(--accent-dim); opacity: 0; z-index: 2;
    transition: opacity 0.5s var(--ease) 0.12s;
  }
  .entry--shelf:hover .entry__media picture::after { opacity: 0.7; }

  /* The six banners lie three abreast, each picture above its caption. They
     used to stand on the entry's own two columns, so that column two began
     where the photograph does – but once the chapter rail moved into the
     page grid (Sept 2026) that column was ~220px: the picture took all of it
     and the captions wrapped a word to a line. Three even columns across the
     content width give every banner the same ~250px and its caption a line. */
  .entry__shelf .banners {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 1.75rem clamp(1.25rem, 2.5vw, 2rem);
  }
  .entry__shelf .banners li { grid-template-columns: minmax(0, 1fr); gap: 0.7rem; align-items: start; }
  .entry__shelf .banners .banner__what { margin: 0; }
  /* Smaller than the full plate, but anchored to the TOP of the frame
     (--pos:center top on the figure) so the crop only ever takes from the
     foot of the picture — a head is the one thing that must not be trimmed.
     display:block because an inline img rides a text baseline: it left 52px
     of leading inside the picture, pushing the caption off its own plate. */
  .entry--shelf .entry__media img {
    height: clamp(19rem, 28vw, 26rem); display: block;
  }
  /* The caption sits under the solid side of the photograph, closed to the
     page's right edge (the line of the menu and the footer links). At the
     left it hung under the part the waterline dissolves, 11px from nothing. */
  .entry--shelf .entry__media figcaption {
    margin-top: 1rem; text-align: right; padding-right: var(--bleed);
  }
}

/* --- tempo: the panorama --------------------------------------------------
   One entry per feed may open the whole window: the photograph runs
   bleed to bleed as a landscape pause, the words resting on a quiet
   scrim at its foot. A breath between stanzas — the stitch skips it and
   resumes at its own anchor below. */
.entry--panorama { min-height: 56vh; display: flex; align-items: flex-end; }
.entry--panorama .entry__media {
  left: calc(-1 * var(--bleed)); right: calc(-1 * var(--bleed));
  top: 0; bottom: 0; width: auto; opacity: 1;
}
.entry--panorama .entry__media::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--bg) 94%, transparent) 0,
    color-mix(in srgb, var(--bg) 72%, transparent) 34rem,
    transparent 62rem);
}
.entry--panorama .entry__body { position: relative; z-index: 1; max-width: 38rem; }
@media (max-width: 72rem) {
  .entry--panorama { min-height: 0; display: block; }
  .entry--panorama .entry__media { position: static; margin-top: 1.25rem; }
  .entry--panorama .entry__media::after { display: none; }
}

/* --- worked with -----------------------------------------------------------
   Provenance, quietly: the marks of the rooms this work has served, one
   ink, one height, lifting a little under the hand. No box, no carousel,
   no colour — a printer's row at the foot of the practice. */
.provenance { margin-top: clamp(2.5rem, 5vw, 4rem); }
/* The label stands on a hairline across the whole row, in the same neutral
   rule as the chapter dividers – a heading for the logos, not an accent
   dash in front of a word (Sept 2026: the dash read as template). */
.provenance__lead {
  margin: 0 0 1.5rem; padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--rule); color: var(--ink-soft);
}
.provenance__row {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 1.6rem 2.6rem;
}
.provenance__row img {
  height: 2.2rem; width: auto; display: block;
  opacity: 0.42; transition: opacity 0.35s var(--ease);
}
.provenance__row li:hover img { opacity: 0.85; }
:root[data-theme="light"] .provenance__row img { filter: invert(1) brightness(0.35); }

/* --- writing about music ---------------------------------------------------
   The journalism, kept subtle: one block of prose, the Istria feature as
   the headline piece, and the picked reviews as a quiet mono cloud. */
.writing p { max-width: 62ch; color: var(--ink-soft); margin: 0 0 1rem; }
.writing__features { display: flex; flex-wrap: wrap; gap: 0.5rem 1.75rem; margin: 0 0 1.5rem; }
.writing__features a { font-family: var(--mono); font-size: 0.75rem; letter-spacing: 0.1em; text-transform: uppercase; }
/* "Selected reviews" opens the list, so it carries the weight of a small
   heading rather than the faintness of a label — as a plain label it read
   as a third link in the row above it. Two classes deep because `.writing p`
   is more specific than a lone class, which is why its own margins had never
   applied either. */
.writing .writing__lead {
  font-size: 0.75rem; font-weight: 500; color: var(--ink);
  margin: 1.9rem 0 1rem;
}
.writing__list { list-style: none; margin: 0; padding: 0; max-width: 42rem; }
.writing__list li + li { border-top: 1px solid var(--rule); }
/* A review is one thing – a record by a band – so the band and the record
   sit on one line, joined, rather than in two columns a hand's width apart
   where the reader has to work out which is which (they did: "not clear
   what is what"). The year keeps its own quiet column at the margin. */
.writing__list a {
  display: grid; grid-template-columns: 3.4rem minmax(0, 1fr);
  gap: 0 1.25rem; align-items: baseline;
  padding-block: 0.6rem; text-decoration: none;
}
.writing__list strong { font-weight: 500; color: var(--ink-soft); transition: color 0.25s var(--ease); }
.writing__list em { color: var(--ink-faint); font-style: italic; }
.writing__list em::before { content: " – "; font-style: normal; color: var(--rule-firm); }
.writing__list a:hover strong { color: var(--accent); }

/* --- the etching where a picture is missing -------------------------------
   Imageless entries get a figure of streamlines in the page's own hairline
   ink — injected by site.js, seeded by position — with the entry's kind set
   small among them. The feed's central channel stays closed, and nothing
   pretends to be a photograph. */
.entry__media--etch {
  /* NO geometry of its own. Every attempt to make the etch a special
     plate — relative positioning, bounded heights, pinned tops, borders —
     broke it in a new way for a round apiece. It is the media slot,
     nothing else: the exact band every photograph uses, top to bottom,
     flush to the dividers, diagonal on the left, bleeding right. Only
     the surface differs. */
  background: linear-gradient(160deg,
    color-mix(in srgb, var(--accent) 5%, var(--bg-sunk)), var(--bg-sunk));
}
@media (max-width: 72rem) {
  /* Stacked below the text a photograph brings its own height; the
     etching has none, so it is given one here and nowhere else.
     It also needs to be a containing block here and ONLY here: the
     stacked rule turns .entry__media static, and the plate's absolute
     children — the streamlines and the kind — would otherwise anchor to
     the whole .entry and lie across the words. (They appeared to behave
     while the reveal's blur was mid-flight, because a filter is itself a
     containing block; they escaped the moment it landed, and never held
     at all under reduced motion.) At full width the media band is
     absolute already, so it stays geometry-free there. */
  .entry__media--etch { height: clamp(8rem, 36vw, 12rem); position: relative; }
}
.entry__media--etch .etch { position: absolute; inset: 0; width: 100%; height: 100%; }
.entry__media--etch .etch path {
  fill: none; stroke: var(--rule-firm); stroke-width: 1;
  vector-effect: non-scaling-stroke; opacity: 0.62;
}
.entry__media--etch .etch path:nth-child(3n) { stroke: var(--accent-dim); opacity: 0.5; }
/* The kind sits like an engraving's caption: a short accent dash and the
   word, no box, no chip — anchored bottom RIGHT, because the media fade
   dissolves the left edge of every figure toward the text and swallowed
   the caption whole when it lived there. */
.entry__media--etch .etch__kind {
  position: absolute; left: 50%; top: 50%; translate: -50% -50%;
  display: inline-flex; align-items: center; gap: 0.7rem;
  font-size: 0.75rem; letter-spacing: 0.2em; color: var(--ink-soft);
  padding: 0; background: none;
}
.entry__media--etch .etch__kind::before {
  content: ""; width: 1.7rem; height: 1px; background: var(--accent-dim);
}

/* --- stilling the winds ---------------------------------------------------
   The header control site.js injects beside the theme toggle. Breathing
   shows three streamlines with their curls; stilled, the same lines with
   the curls combed out. State rides on the root, like the theme does. */
.wind-toggle .lull { display: none; }
:root[data-wind="still"] .wind-toggle .gust { display: none; }
:root[data-wind="still"] .wind-toggle .lull { display: block; }

/* --- print ----------------------------------------------------------------
   Academia doubles as a CV, so paper is a real reader here. Black on white,
   the weather and every instrument furled, chapters flattened out of the
   margins, URLs printed after external links so the paper still leads
   somewhere. Reveal states are forced visible: on paper everything has
   already arrived. */
@media print {
  :root, :root[data-theme="dark"], :root[data-theme="light"] {
    --bg: #ffffff; --bg-sunk: #ffffff; --surface: #ffffff;
    --ink: #000000; --ink-soft: #2E2C28; --ink-faint: #55524B;
    --rule: #C8C4BC; --rule-firm: #97928A;
    --accent: #3E6A59; --accent-dim: #6D9384;
  }
  html { font-size: 13px; }
  body { background: #fff !important; color: #000; }
  body::before, body::after { display: none !important; }

  /* Entrance animations hold their from-state (opacity 0) on a medium that
     never plays them — the whole page-hero printed as half a page of
     nothing. No animation runs on paper, so none may hold anything. */
  * { animation: none !important; }
  .hero, .page-hero, .about-hero { min-height: 0; }

  .wind, .proto-note, .skip-link, .header-tools, .compass, .nav,
  .chapter-strip, .chapters, .footer-rose, .strand-bar, .lightbox,
  iframe, video {
    display: none !important;
  }

  .site-header { position: static; background: none; border: 0; }
  .site-footer { border-top: 1px solid var(--rule); }
  /* The rail column goes with the rail: one column of chapters on paper. */
  .page-body { display: block; }
  .page-content { border: 0; padding: 0; }

  .reveal { opacity: 1 !important; translate: none !important; transition: none !important; }
  /* Rules that had not yet drawn themselves are still 0%-wide backgrounds
     when the sheet is sent to paper — force every one of them out full. */
  .rule-draw::before, .rule-in::before,
  .rule-draw::after, .rule-in::after {
    scale: 1 1 !important; transform: none !important;
    background-size: 100% 100% !important;
  }

  main a[href^="http"]::after {
    content: " (" attr(href) ")";
    font-family: var(--mono); font-size: 0.85em; letter-spacing: 0;
    text-transform: none; color: var(--ink-faint); word-break: break-all;
  }

  h1, h2, h3 { break-after: avoid; }
  .block, .pubs li, .entry, .award, .cv-card, .edition, .rows .row, figure {
    break-inside: avoid;
  }
  img { max-width: 100% !important; filter: none !important; }
  @page { margin: 15mm 16mm; }
}

/* ==========================================================================
   REVERBERATIONS — the blog. Index, posts and the home band are GENERATED
   by content/build-blog.py; the look lives here. Classes are name-neutral
   on purpose: renaming the blog is one constant in the generator, not a
   find-and-replace in this sheet.
   ========================================================================== */

/* --- the home band: three quiet lines between the feed and the gallery --- */
.postband { margin-block: clamp(2.5rem, 6vw, 4rem); }
.postband__head {
  margin: 0 0 0.35rem;
  font-family: var(--mono);
  font-size: 0.78rem;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.postband__list { list-style: none; margin: 0; padding: 0; }
.postband__item {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.3rem 1.5rem;
  padding-block: 0.8rem;
}
.postband__item + .postband__item { border-top: 1px solid var(--rule); }
.postband__item > .label { flex: 0 0 7.5rem; }
.postband__title { margin: 0; font-size: 1.0625rem; font-weight: 500; line-height: 1.35; }
.postband__more { margin: 1rem 0 0; }

/* --- the blog index ---------------------------------------------------- */
.post-head { padding-block: clamp(2.5rem, 7vw, 4.5rem) 0; }
.post-head h1 {
  margin: 0.4rem 0 0;
  font-size: clamp(1.9rem, 1rem + 3vw, 2.75rem);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.01em;
}
.post-standfirst {
  margin: 0.9rem 0 0;
  max-width: 38rem;
  font-size: 1.125rem;
  line-height: 1.55;
  color: var(--ink-soft);
}
.postlist { margin-top: clamp(1.8rem, 4vw, 2.75rem); }
.postrow { padding-block: 1.5rem; border-top: 1px solid var(--rule); }
.postrow:last-child { border-bottom: 1px solid var(--rule); }
.postrow__meta { display: flex; flex-wrap: wrap; gap: 0.35rem 1.25rem; margin: 0 0 0.5rem; }
.postrow__title { margin: 0; font-size: 1.35rem; font-weight: 600; line-height: 1.25; }
.postrow__summary { margin: 0.45rem 0 0; max-width: 40rem; color: var(--ink-soft); }
.postlist__foot { margin: 1.5rem 0 clamp(3rem, 7vw, 5rem); }

/* --- a post ------------------------------------------------------------- */
/* Full width of the wrap on desktop - David's call (27 Aug 2026): the sheet
   is not left empty on the right. The index keeps its own measures. */
.post { padding-bottom: clamp(3rem, 7vw, 5rem); }
.post .post-standfirst { max-width: none; }
.post__hero { margin: clamp(1.6rem, 4vw, 2.5rem) 0 0; }
.post__body { margin-top: clamp(1.6rem, 4vw, 2.4rem); }
.post__body h2 { margin: 2.1em 0 0.55em; font-size: 1.35rem; font-weight: 600; line-height: 1.3; }
.post__body h3 { margin: 1.7em 0 0.45em; font-size: 1.125rem; font-weight: 600; }
.post__body p { margin: 0 0 1.05em; }
.post__body ul, .post__body ol { margin: 0 0 1.05em; padding-left: 1.25rem; }
.post__body li { margin-bottom: 0.35em; }
.post__body li::marker { color: var(--ink-faint); }
.post__body blockquote {
  margin: 1.6rem 0;
  padding: 0.15rem 0 0.15rem 1.1rem;
  border-left: 2px solid var(--accent-dim);
  color: var(--ink-soft);
}
.post__body blockquote p { margin: 0; }
.post__body hr { border: 0; border-top: 1px solid var(--rule-firm); width: 5.5rem; margin: 2.5rem 0; }
.post__body p code, .post__body li code {
  font-family: var(--mono);
  font-size: 0.85em;
  background: var(--bg-sunk);
  border: 1px solid var(--rule);
  padding: 0.08em 0.32em;
}
/* A cell is as long as its code and a breath more (the right padding is the
   copy button's lane); a truly long line still scrolls inside. The wrapper
   is JS-made so the button holds its corner while the code scrolls under
   it - without scripts the bare pre carries the same sizing. */
.codeblock {
  margin: 1.5rem 0;
  padding: 0.85rem 4.25rem 0.85rem 1rem;
  background: var(--bg-sunk);
  border: 1px solid var(--rule);
  overflow-x: auto;
  width: fit-content;
  min-width: 14rem;
  max-width: 100%;
  font-family: var(--mono);
  font-size: 0.8125rem;
  line-height: 1.6;
}
.codeblock code { font-family: inherit; }
.codewrap { position: relative; width: fit-content; min-width: 14rem; max-width: 100%; margin: 1.5rem 0; }
.codewrap .codeblock { margin: 0; width: auto; min-width: 0; max-width: none; }
.codeblock__copy {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  padding: 0.15rem 0.5rem;
  border: 1px solid var(--rule);
  background: var(--bg-sunk);
  color: var(--ink-faint);
  font-family: var(--mono);
  font-size: 0.6875rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  user-select: none;
}
.codeblock__copy:hover { color: var(--accent); border-color: var(--rule-firm); }
.codeblock__copy.is-done { color: var(--accent); border-color: var(--accent-dim); }
.post__figure { margin: 2rem 0; }
.post__figure figcaption { margin-top: 0.55rem; }
.post__foot { margin-top: 3rem; padding-top: 1.1rem; border-top: 1px solid var(--rule); }

/* ==========================================================================
   THE FRONT DOOR (interim, September 2026)
   While the full site is rebuilt, home is a door rather than a feed: who
   this is, the rooms already open, and one honest line about the rest. The
   hero keeps its weather; the doors take the third the portrait used to
   stand in, and the first screen ends at the footer.
   ========================================================================== */
/* The 404 is the same door, reached by a wrong turn. */
[data-page="door"] .hero, [data-page="notfound"] .hero {
  min-height: calc(100vh - var(--header-h) - 4.25rem);
  min-height: calc(100svh - var(--header-h) - 4.25rem);
  padding-block: clamp(2.5rem, 7vh, 4.5rem);
}
.doors { align-self: end; animation: rise 0.9s var(--ease) 0.26s both; }
.doors ul { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule-firm); }
.door {
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.3rem 1rem;
  align-items: baseline; padding: 1rem 0 1.05rem;
  border-bottom: 1px solid var(--rule);
  color: inherit; text-decoration: none;
  transition: border-color 0.35s var(--ease);
}
.door__name {
  font-family: var(--mono); font-size: 0.8125rem; font-weight: 500;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink);
  transition: color 0.25s var(--ease);
}
.door .arrow { font-family: var(--mono); font-size: 0.875rem; color: var(--accent-dim); transition: transform 0.3s var(--ease), color 0.25s var(--ease); }
.door__what { grid-column: 1 / -1; font-size: 0.9375rem; line-height: 1.45; color: var(--ink-soft); }
.door:hover, .door:focus-visible { border-bottom-color: var(--accent-dim); }
.door:hover .door__name, .door:focus-visible .door__name,
.door:hover .arrow, .door:focus-visible .arrow { color: var(--accent); }
.door:focus-visible .arrow { transform: translateX(0.3rem); }
.door--down:hover .arrow, .door--down:focus-visible .arrow { transform: translateY(0.2rem); }
.hero .doors__note {
  margin-top: 1.25rem; font-size: 0.875rem; line-height: 1.55;
  color: var(--ink-faint); max-width: none; text-wrap: balance; animation: none;
}
/* Short page headlines break evenly instead of leaving one word behind. */
.page-hero h1 { text-wrap: balance; }
@media (max-width: 44rem) {
  .doors { margin-top: 0.75rem; }
}
