/* ============================================================
   HEART'S LIBRARY — an extension of codyheart.design
   ------------------------------------------------------------
   The design system here is ported from Cody's portfolio
   (codyheart.design), which itself follows the register of
   andreavollendorf.com: Geist body at 14px/450, a serif for
   the wordmark and display headings (Gooper here), Geist Mono
   labels, the ink-opacity scale, the surface/flush-ring card
   language, quiet 12px chrome, and the sticky blur nav with
   duotone dropdown menus. Site-specific pieces (the shelf,
   chips, book rows) are built in that same vocabulary.

   Motion is CSS-only: content is never hidden waiting on a
   script — entrances are short one-shot animations that the
   style engine completes on its own.

   Regards and Cheltenham Classic (previous display faces) are
   retired but kept in /fonts — re-enable one by restoring its
   @font-face and pointing --serif at it.
   ============================================================ */

/* ---------- Fonts (self-hosted, same files as the portfolio) ---------- */
@font-face {
  font-family: 'Geist';
  src: url('../fonts/Geist-Variable.woff2') format('woff2');
  font-weight: 100 900; font-style: normal; font-display: swap;
}@font-face {
  font-family: 'Geist Mono';
  src: url('../fonts/GeistMono-Variable.woff2') format('woff2');
  font-weight: 100 900; font-style: normal; font-display: swap;
}/* The expressive end of the family — page titles wear these */
/* The photography wing's display face — park.photos' serif. */
/* Sprig — the serif the whole site wears now. Real weights exist,
   unlike Instrument, so bold settings render true. */
/* Cheltenham Classic, back from retirement — the portfolio's own
   serif, so the wings read as one family. */
/* Recoleta — the display face for titles. Real cuts 300-900. */
@font-face {
  font-family: 'Recoleta';
  src: url('../fonts/Recoleta-Regular.woff2') format('woff2'),
       url('../fonts/Recoleta-Regular.woff') format('woff');
  font-weight: 400; font-style: normal; font-display: swap;
}@font-face {
  font-family: 'Recoleta';
  src: url('../fonts/Recoleta-Medium.woff2') format('woff2'),
       url('../fonts/Recoleta-Medium.woff') format('woff');
  font-weight: 500; font-style: normal; font-display: swap;
}@font-face {
  font-family: 'Recoleta';
  src: url('../fonts/Recoleta-SemiBold.woff2') format('woff2'),
       url('../fonts/Recoleta-SemiBold.woff') format('woff');
  font-weight: 600; font-style: normal; font-display: swap;
}@font-face {
  font-family: 'Recoleta';
  src: url('../fonts/Recoleta-Bold.woff2') format('woff2'),
       url('../fonts/Recoleta-Bold.woff') format('woff');
  font-weight: 700; font-style: normal; font-display: swap;
}/* Circular Std — Book / Medium / Bold / Black, with italics. */
/* Roony — a handmade display face, bought for this site. woff2 with a
   woff behind it, which is what the webfont licence ships. One weight;
   there is no bolder cut to reach for. */
/* ---------- Tokens (verbatim from codyheart.design) ---------- */
:root {
  --background: #fff;
  --foreground: #171717;
  --surface: #f6f7f9;
  --surface-hover: #f0f1f4;
  --border: rgba(0, 0, 0, .08);
  --accent: #4c74ff;
  --selection: rgba(76, 116, 255, .18);

  --ink-85: rgba(0, 0, 0, .85);
  --ink-80: rgba(0, 0, 0, .8);
  --ink-78: rgba(0, 0, 0, .78);
  --ink-65: rgba(0, 0, 0, .65);
  --ink-55: rgba(0, 0, 0, .55);
  --ink-50: rgba(0, 0, 0, .5);
  --ink-45: rgba(0, 0, 0, .45);
  --ink-40: rgba(0, 0, 0, .4);
  --ink-30: rgba(0, 0, 0, .3);

  /* TYPE SCALE — one ladder, referenced by name. Sizes used to be
     literals scattered across the sheet (11, 12.5, 13, 14, 22…), which
     is how a system drifts: every new element picked a number that
     looked right next to whatever it sat beside. These are the only
     sizes the photography pages use; to make something bigger, move it
     a step, don't invent a value.

     Nudged up a step overall for readability — Circular's x-height sits
     lower than Geist's, so the old 13/14px lines read smaller in it
     than they measured. Roughly a 1.25 ratio above the base. */
  /* SPACING SCALE. Same reasoning as the type ladder: the gaps around
     titles were 16 / -2 / 14 / 4 — numbers picked one at a time.

     The rule these encode is PROXIMITY. A byline belongs to the title
     above it, so the gap inside that pair has to be visibly tighter
     than the gap to whatever follows — otherwise the three lines read
     as three separate things, which is exactly what was happening:
     16px above the byline and 14px below it is no grouping at all.
     Pairs get --space-2, the group gets --space-6 beneath it, and the
     ratio does the work. */
  --space-1: 4px;
  --space-2: 6px;
  --space-3: 10px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 36px;

  --type-2xs: 12px;
  --type-xs: 13px;
  --type-sm: 14px;
  --type-base: 15px;
  --type-md: 17px;
  --type-lg: 21px;
  --type-xl: 26px;
  --type-2xl: 33px;
  --type-3xl: 46px;

  --title-face: 'Recoleta', Georgia, serif;
  --sans: 'Geist', system-ui, -apple-system, sans-serif;
  /* Cheltenham Classic carries every serif role — settled after a
     same-day tour: Instrument (unreadable small), Sprig (no), Gooper
     (slabby, weak at small sizes). One face, display through caption. */
  --serif: 'Geist', system-ui, -apple-system, sans-serif;
  /* THE WORDMARK. It ran in Roony for a while; those files are no
     longer in fonts/, so the declaration was resolving to Georgia on
     every page — a silent fallback, which is the worst kind. It takes
     the title face now. If Roony comes back, restore its @font-face
     and point this at it: it has NO lowercase (its lowercase
     codepoints are drawn as capitals), so it suits this all-caps
     lockup and nothing else on the site. */
  --wordmark-face: 'Geist Mono', ui-monospace, 'Cascadia Mono', monospace;
  --display: 'Geist', system-ui, -apple-system, sans-serif;
  --mono: 'Geist', system-ui, -apple-system, sans-serif;

  --ease: cubic-bezier(.25, .1, .25, 1);
  --ease-out-cubic: cubic-bezier(.215, .61, .355, 1);
  --ease-out-quart: cubic-bezier(.165, .84, .44, 1);
  --ease-out-quint: cubic-bezier(.23, 1, .32, 1);
  --ease-out-expo: cubic-bezier(.19, 1, .22, 1);

  --shadow-flush: inset 0 0 0 1px rgba(0, 0, 0, .06);
  --shadow-raised: 0 1px 2px rgba(0, 0, 0, .04), inset 0 0 0 1px rgba(0, 0, 0, .06);
  --shadow-floating: 0 4px 24px rgba(0, 0, 0, .06), inset 0 0 0 1px rgba(0, 0, 0, .08);

  --z-overlay: 40;
  --z-nav: 50;
  --z-dropdown: 60;

  --gutter: 24px;

  /* The bar's height: .nav__inner's 20px of padding either side of a
     44px control. Only the burger sheet reads it — it starts at the
     bar's bottom edge rather than under it — so if you change the
     bar's padding or the size of the tallest thing in it, change this
     to match or the sheet will overlap it or leave a gap. */
  --bar-h: 84px;

  /* The one mark the library keeps: the wordmark heart. */
  --heart: #E5484D;

  /* The lightbox mat. Light mode keeps the reference's white wall.
     It is deliberately its own token and not --background: if the page
     ground ever picks up a tint, the mat behind a photograph must not
     follow it. */
  --lb-mat: #fff;

  /* The journal post's own grey. It is NOT --surface: that is the site's
     panel tint (#f6f7f9) and reads as almost-white behind a block of
     text. This is the incoming-bubble grey from iOS Messages, which is
     the register the journal is aiming at — enough darker that the post
     is an object on the page rather than a faint tint of it. */
  --bubble: #E9E9EB;
}
@media (min-width: 1024px) {
  :root { --gutter: 80px; }
}

/* ---------- Reset & base ---------- */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  scrollbar-width: thin;
  scrollbar-color: rgba(0, 0, 0, .28) transparent;
  overflow-y: scroll;
}
html::-webkit-scrollbar { width: 10px; }
html::-webkit-scrollbar-track { background: transparent; }
html::-webkit-scrollbar-thumb {
  background-color: rgba(0, 0, 0, .2);
  background-clip: padding-box;
  border: 2px solid transparent;
  border-radius: 5px;
}
html::-webkit-scrollbar-thumb:hover { background-color: rgba(0, 0, 0, .32); }

body {
  background: var(--background);
  color: var(--ink-80);
  font-family: var(--sans);
  font-size: var(--type-base);
  font-weight: 450;
  /* UNITLESS. This was 1.45rem — a fixed 23.2px inherited by every
     element regardless of its own size, so 24px headings got leading
     smaller than their font (lines collided when they wrapped) and 9px
     labels got 2.5x leading. 15px x 1.55 = 23.25px, so body copy is
     unchanged to within a rounding error, but children now scale. */
  line-height: 1.55;
  letter-spacing: -.005rem;
  -webkit-font-smoothing: antialiased;
  padding-bottom: env(safe-area-inset-bottom, 0);
}

h1, h2, h3 { text-wrap: balance; }
h1, h2 { font-family: var(--serif); }
p { text-wrap: pretty; }
a { color: inherit; text-decoration: none; }
button { color: inherit; font: inherit; background: none; border: none; cursor: pointer; }
img { max-width: 100%; display: block; }
ul, ol { list-style: none; }
hr { border: 0; }

::selection { background: var(--selection); }

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

a:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible {
  outline: 2px solid var(--foreground);
  outline-offset: 2px;
  border-radius: 2px;
}

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--foreground); color: var(--background);
  /* inline-flex + min-height rather than more padding: this is the
     first thing a keyboard or screen-reader user lands on, and it was
     36px tall */
  display: inline-flex; align-items: center; min-height: 44px;
  padding: 0 var(--space-4); border-radius: 0 0 8px 0;
  font-size: var(--type-xs); z-index: 100;
}
.skip:focus { left: 0; }

/* ---------- Shared micro-interactions (portfolio) ---------- */
.link-hover { transition: color .15s ease, background-color .15s ease; }
.link-hover:hover { color: var(--ink-85); }
.press-scale { transition: transform .1s; }
.press-scale:active { transform: scale(.97); }
/* A frame gives a little under the press, the way the reference's
   photos do — the give is on the frame, not the picture inside it,
   so the crop never shifts. */
.shot-btn,
.frame-btn,
.tile--video {
  transition: transform .16s var(--ease-out-quart);
}
.shot-btn:active,
.frame-btn:active,
.tile--video:active { transform: scale(.97); }
@media (prefers-reduced-motion: reduce) {
  .shot-btn, .frame-btn, .tile--video { transition: none; }
  .shot-btn:active, .frame-btn:active,
  .tile--video:active { transform: none; }
}

/* The heart beats on hover — the portfolio wordmark's pulse. */
@keyframes heart-pulse {
  0%, 100% { transform: scale(1); }
  15%      { transform: scale(1.15); }
  30%      { transform: scale(1); }
  45%      { transform: scale(1.15); }
  60%      { transform: scale(1); }
}
.wordmark:hover .wordmark__dot { animation: heart-pulse 1.3s ease-in-out infinite; }

/* Emphasis spans in headings — the portfolio's petal-gradient
   italics, in the name's rose-brick → poppy. */
.accent { /* Recoleta ships no italic and .page-head h1 sets font-synthesis:
     none, which inherits — so this rendered upright anyway. The
     gradient and the weight carry it. */ font-weight: 700; color: #D9603B; }
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .accent {
    background-image: linear-gradient(100deg, #A64A3F, #D9603B);
    -webkit-background-clip: text;
    background-clip: text;
    -webkit-text-fill-color: transparent;
    color: transparent;
    /* background-clip:text only paints inside the span's background
       box, and Gooper's italic ascenders reach past it. Vertical
       padding on an inline box grows the paint area without touching
       line layout; the horizontal pair is offset by the margins. */
    padding: .25em .15em;
    margin: 0 -.15em;
  }
}

/* ============================================================
   NAV — sticky, blurred, hides on scroll down (portfolio port)
   ============================================================ */
/* The bar holds still. It used to hide on scroll down and ride on
   `will-change: transform`, which promoted a layer the blur then
   had to re-sample every frame — over the full-bleed wall that
   flickered badly. No transform, no promotion, and a background
   solid enough that the blur is a grace note rather than the thing
   keeping the type legible over photographs. */
/* The bar scrolls away with the page. It used to stick, and the blur
   and the near-opaque ground came with that: both existed so type
   stayed legible over photographs passing underneath it. Nothing
   passes underneath it now, so both go — a translucent ground over
   nothing is just a slightly wrong white, and the blur was the one
   thing on the site expensive enough to flicker over the full-bleed
   wall. Solid background, no filter, no z-index race. */
.nav {
  position: static;
  background-color: var(--background);
}
/* …except while the burger sheet is open, when the bar has to stay on
   screen because the sheet is relying on it: the wordmark, the theme
   glyph and the burger-turned-cross are all up here and none of them
   is duplicated below.

   `sticky` is the obvious answer and it does not work. The open sheet
   locks the scroll with `overflow: hidden` on body, which makes body a
   scroll container of its own, and a sticky child pins to THAT — to
   body's box, which is still sitting wherever the page was scrolled
   to. Open the menu 1200px down the wall and the bar pins at -1236.

   So: fixed, with the body padded by exactly the height it just lost.
   The bar is the first thing in the flow (the skip link is absolute),
   nothing else sets a top padding, and the two revert on the same tick
   — so taking it out of flow moves nothing. The class outlives
   `menu-locked` by the length of the sheet's fade (js/review.js), or
   the sheet would paint over the bar on the way out. */
html.menu-open .nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: var(--z-nav);
}
html.menu-open body { padding-top: var(--bar-h); }

.nav__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px var(--gutter);
  position: relative;
}

/* ---------- The wordmark ----------
   "Cody Heart" with the heart set as the full stop after the name.
   One lockup on every page — every interior page used to trade it for
   a "← Home" back link, which meant the name only ever appeared on
   the front door. It is written by scripts/shell.js, so it exists in
   exactly one place; the heart takes --heart rather than repeating a
   hex literal in nine files. */
.wordmark {
  display: inline-flex;
  /* the heart leads now, so it centres on the type rather than sitting
     on the baseline as punctuation did */
  align-items: center;
  flex: none;
  gap: .42em;
  line-height: 1;
  color: var(--foreground);
  /* The mark itself is 14px tall — far under a finger. This gives it a
     44px hit area without changing where anything sits, since the
     pseudo-element is absolutely positioned and paints nothing. */
  position: relative;
}
.wordmark::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
/* Set in the mono, uppercase — the register of a byline stamp or the
   margin of a contact sheet. The serif read as a book title, which is
   the wrong claim for a site that is documentation first. */
.wordmark__name {
  font-family: var(--wordmark-face);
  font-size: var(--type-sm);
  /* one weight ships, so asking for 600 would only get 400 back */
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: .04em;
}
/* The SVG's own baseline is its bottom edge, so with the tight
   viewBox in scripts/shell.js this lands the heart exactly on the
   text baseline — no nudge needed. The old rule pushed it down 1.5px
   to compensate for padding that was inside the viewBox; both the
   padding and the nudge are gone. */
.wordmark__dot {
  display: block;
  flex: none;
  /* Sized in em off the wordmark's own type so the mark tracks the
     name, and pitched just over the cap height of the uppercase it
     leads (~10px at 14px type). It was a 6px full stop when it sat
     after the "t"; 1.5em made it taller than the whole wordmark. */
  width: 0.9em;
  height: 0.84em;
  color: var(--heart);
}

/* The page name, fading in once you're past the masthead. It used to
   be absolutely positioned at left:50% while the link row was right-
   aligned and grew leftward, so on any page with a wide enough nav —
   collections.html at every width from 768px to about 1100px — the
   title was drawn straight through the links, half over and half
   under them, and swallowed their clicks. As an ordinary flex child
   between the wordmark and the links it cannot overlap by
   construction; when the bar tightens it ellipsizes instead. */
.nav__scrolltitle {
  display: none;
  flex: 1 1 auto;
  min-width: 0;
  margin: 0 var(--space-4);
  border: 0;
  background: none;
  font-family: inherit;
  text-align: center;
  font-size: var(--type-xs);
  font-weight: 550;
  color: var(--ink-85);
  opacity: 0;
  transform: translateY(-6px);
  pointer-events: none;
  cursor: pointer;
  transition: opacity .2s var(--ease-out-quart), transform .2s var(--ease-out-quart);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}/* a 20px-tall button, and the only scroll-to-top on the page */
.nav__scrolltitle { position: relative; }
.nav__scrolltitle::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px;
  transform: translateY(-50%);
}

.nav__scrolltitle.is-visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
@media (min-width: 768px) { .nav__scrolltitle { display: block; } }

.nav__links {
  display: none;
  align-items: center;
  gap: var(--space-4);
  /* the links live on the right; the wordmark holds the left alone */
  margin-left: auto;
}
/* Seven links now — the burger holds on a little longer. */
@media (min-width: 768px) { .nav__links { display: flex; } }

.nav__link {
  font-size: var(--type-2xs);
  font-weight: 450;
  color: var(--ink-55);
  height: 32px;
  padding: 0 12px;
  display: flex;
  align-items: center;
  gap: var(--space-1);
  border-radius: 6px;
  transition: color .15s ease, background-color .15s ease;
}
.nav__link:hover { color: var(--ink-85); background: var(--surface); }
/* THE BAR IS A TOUCH SURFACE TOO. The desktop nav appears at 768px,
   which is a folding phone unfolded and every tablet in landscape —
   fingers, not mice. The visible pill stays 32px; the pseudo-element
   adds the missing 12px of hit area. The dropdown is positioned
   against .nav__drop, not against the button, so making the button a
   containing block does not move the menu. */
.nav__link { position: relative; }
.nav__link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.nav__link[aria-current="page"], .nav__link.is-current { color: var(--ink-85); }
.nav__link .chev {
  transition: transform .2s var(--ease-out-quart);
  will-change: transform;
}
.nav__link[aria-expanded="true"] .chev { transform: rotate(180deg); }

/* Dropdown */
.nav__drop { position: relative; }
.nav__menu {
  position: absolute;
  right: 0;
  top: 100%;
  margin-top: 8px;
  z-index: var(--z-dropdown);
  background: var(--background);
  border-radius: 8px;
  overflow: hidden;
  /* the default suits the Elsewhere menu, whose rows are one word;
     the trips menu overrides it below */
  width: 280px;
  box-shadow: var(--shadow-floating);
  opacity: 0;
  transform: translateY(-8px);
  pointer-events: none;
  /* visibility, not just opacity — exactly as .mobile-menu does below.
     A transparent menu still keeps its links in the tab order, and the
     shell marks it aria-hidden, so a keyboard user was tabbing through
     up to ten invisible links that a screen reader had been told did
     not exist. Delayed to the end of the fade so the exit still plays. */
  visibility: hidden;
  transition: opacity .15s var(--ease-out-quart), visibility 0s linear .15s;
}
.nav__menu.is-open {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
  visibility: visible;
  transition: opacity .2s var(--ease-out-quart), transform .2s var(--ease-out-quart), visibility 0s;
}
.nav__menu-pad { padding: 8px; }

.menu-item {
  display: flex;
  align-items: center;
  gap: 12px;
  border-radius: 8px;
  padding: 12px;
  transition: background-color .15s ease;
}
.menu-item:hover { background: var(--surface-hover); }
.menu-item__tile {
  width: 56px;
  height: 56px;
  border-radius: 8px;
  background: #fff;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-flush);
}
.menu-item__tile svg { width: 28px; height: 28px; display: block; }
/* Collections in the dropdown wear a cover frame — a mini wall tile,
   same continuous-corner language at menu scale — and their menu
   runs wider than the plain one, because a row here carries a whole
   billing ("Tame Impala: Deadbeat Tour with Djo") over a byline with
   a date and a venue in it. At the shared 280px both wrapped to
   three lines. This rule used to be keyed to `#menu-collections`, an
   id the shell stopped writing, so it had been matching nothing.
   Only the Elsewhere menu, whose rows are one word, keeps 280. */
#menu-trips { width: 396px; }
.menu-item__tile--photo {
  overflow: hidden;
  border-radius: 14px;
  corner-shape: squircle;
}
.menu-item__tile--photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* A collection whose only frame is a clip has no still to wear.
   The tile holds the slot open instead of showing the white ground
   the duotone icons are drawn on — which on a dark page read as a
   lit square with nothing in it. */
.menu-item__tile--empty {
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--border);
}
.menu-item__title {
  display: block;
  font-size: var(--type-xs);
  font-weight: 550;
  color: var(--ink-85);
  line-height: 1.25;
}
.menu-item__desc {
  display: block;
  font-size: var(--type-2xs);
  color: var(--ink-55);
  line-height: 1.25;
  margin-top: 2px;
}

/* The way through to the full ledger is a row like any other — tile,
   title, line under it. It used to be small print set apart at the
   bottom, which made the most useful link in the menu the least
   prominent thing in it. Its tile takes the cover frames' rounded
   square and holds a mark instead of a photograph, so it sits level
   with the collections above rather than trailing them. */
.menu-item__tile--all {
  border-radius: 14px;
  corner-shape: squircle;
}

/* Duotone icons: grey by default, colored on row hover */
.icon-duo .duo-a { fill: #E1E3E8; transition: fill .15s ease; }
.icon-duo .duo-b { fill: #8790A1; transition: fill .15s ease; }
@media (hover: hover) {
  .menu-item:hover .icon-duo.c-blue   .duo-a { fill: #C2E5FF; }
  .menu-item:hover .icon-duo.c-blue   .duo-b { fill: #0090FF; }
  .menu-item:hover .icon-duo.c-orange .duo-a { fill: #FFD19A; }
  .menu-item:hover .icon-duo.c-orange .duo-b { fill: #F76B15; }
  .menu-item:hover .icon-duo.c-purple .duo-a { fill: #EAD5F9; }
  .menu-item:hover .icon-duo.c-purple .duo-b { fill: #8E4EC6; }
  .menu-item:hover .icon-duo.c-green  .duo-a { fill: #C4E8D1; }
  .menu-item:hover .icon-duo.c-green  .duo-b { fill: #30A46C; }
  .menu-item:hover .icon-duo.c-pink   .duo-a { fill: #F6CEE7; }
  .menu-item:hover .icon-duo.c-pink   .duo-b { fill: #D6409F; }
  .menu-item:hover .icon-duo.c-teal   .duo-a { fill: #BFEEE4; }
  .menu-item:hover .icon-duo.c-teal   .duo-b { fill: #12A594; }
}
/* In the mobile menu the icons are always colored */
.icon-duo.is-colored.c-blue   .duo-a { fill: #C2E5FF; }
.icon-duo.is-colored.c-blue   .duo-b { fill: #0090FF; }
.icon-duo.is-colored.c-orange .duo-a { fill: #FFD19A; }
.icon-duo.is-colored.c-orange .duo-b { fill: #F76B15; }
.icon-duo.is-colored.c-purple .duo-a { fill: #EAD5F9; }
.icon-duo.is-colored.c-purple .duo-b { fill: #8E4EC6; }
.icon-duo.is-colored.c-green  .duo-a { fill: #C4E8D1; }
.icon-duo.is-colored.c-green  .duo-b { fill: #30A46C; }
.icon-duo.is-colored.c-pink   .duo-a { fill: #F6CEE7; }
.icon-duo.is-colored.c-pink   .duo-b { fill: #D6409F; }
.icon-duo.is-colored.c-teal   .duo-a { fill: #BFEEE4; }
.icon-duo.is-colored.c-teal   .duo-b { fill: #12A594; }

/* Hamburger */
.nav__burger-wrap { display: block; }
@media (min-width: 768px) { .nav__burger-wrap { display: none; } }
.nav__burger {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  /* 44x44 is the floor for a touch target (Apple HIG, WCAG 2.5.5) and
     this is the only way into the nav on a phone. The negative margin
     grew with the box so the glyph sits exactly where it did: the
     optical inset is 44/2 - 10 = 12px, same as 40/2 - 8. */
  width: 44px;
  height: 44px;
  /* `-var(...)` is not a value; this was dropped whole, and the glyph
     sat 10px further from the right edge than the wordmark sits from
     the left. */
  margin-right: calc(-1 * var(--space-3));
  border-radius: 8px;
}
.nav__burger span {
  position: absolute;
  width: 18px;
  height: 2px;
  border-radius: 999px;
  background: var(--foreground);
  transition: transform .2s var(--ease-out-quart);
}
.nav__burger span:first-child { transform: translateY(-4px) rotate(0deg); }
.nav__burger span:last-child { transform: translateY(4px) rotate(0deg); }
.nav__burger[aria-expanded="true"] span:first-child { transform: translateY(0) rotate(45deg); }
.nav__burger[aria-expanded="true"] span:last-child { transform: translateY(0) rotate(-45deg); }

/* The burger sheet. It hangs off the bottom of the bar rather than
   covering it — see `html.menu-open .nav` above — so it starts at
   --bar-h and the bar keeps painting its own strip. Sitting over the
   bar is what forced the sheet to carry a second wordmark's worth of
   chrome: its own close button, and a second theme control because
   the bar's was underneath and untouchable. Both are gone. */
.mobile-menu {
  position: fixed;
  top: var(--bar-h);
  left: 0;
  right: 0;
  bottom: 0;
  z-index: calc(var(--z-nav) - 1);
  background: var(--background);
  opacity: 0;
  pointer-events: none;
  /* visibility, not just opacity: a transparent sheet still keeps its
     links in the tab order, so a keyboard user tabbing the header
     lands on focus that is nowhere on screen — on desktop too, where
     the burger is display:none but this container is not. Delayed to
     the end of the fade so the transition still plays on the way out. */
  visibility: hidden;
  transition: opacity .15s var(--ease-out-quart), visibility 0s linear .15s;
}
.mobile-menu.is-open {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition: opacity .2s var(--ease-out-quart), visibility 0s;
}
.mobile-menu__inner {
  position: relative;
  /* sides are the bar's own gutter, so the sheet's text hangs off the
     same left edge as the wordmark directly above it. The bottom pad
     clears the home indicator on a notched phone. */
  padding: var(--space-2) var(--gutter) max(var(--space-5), env(safe-area-inset-bottom));
  height: 100%;
  overflow-y: auto;
  opacity: 0;
  transform: translateY(-12px);
  transition: opacity .15s var(--ease-out-quart);
}
.mobile-menu.is-open .mobile-menu__inner {
  opacity: 1;
  transform: translateY(0);
  transition: opacity .2s var(--ease-out-quart), transform .2s var(--ease-out-quart);
}
/* The sheet runs to 767px — a small window or a tablet held upright —
   and at that width full-bleed rows read as a stretched phone screen
   rather than a considered one, so the contents keep a column. */
.mobile-menu__primary,
.mobile-menu__section { max-width: 520px; }

/* The four links are the point of the sheet, so they are the only
   thing in it set above body size. */
.mobile-menu__primary {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.mobile-menu__link {
  display: block;
  font-size: var(--type-md);
  line-height: 1.35;
  font-weight: 550;
  letter-spacing: -.01em;
  color: var(--foreground);
  padding: var(--space-3) var(--space-4);
  margin: 0 calc(-1 * var(--space-4));
  border-radius: 10px;
  transition: background-color .15s ease;
}
/* the sheet says where you are; the bar's aria-current is covered */
.mobile-menu__link[aria-current="page"] { background: var(--surface); }
.mobile-menu__link:active { background: var(--surface-hover); }

.mobile-menu__section {
  margin-top: var(--space-3);
  padding-top: var(--space-4);
  border-top: 1px solid var(--border);
}
/* the site's small-label register — same as .crumb and .prop-label */
.mobile-menu__label {
  font-size: var(--type-2xs);
  line-height: 1.2;
  text-transform: uppercase;
  letter-spacing: .04em;
  font-weight: 500;
  color: var(--ink-55);
  margin-bottom: 8px;
}

/* the burger's collection list, matching the desktop menu */
.mobile-menu__triplist { display: flex; flex-direction: column; gap: 2px; }
.mobile-menu__item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  border-radius: 10px;
  /* the negative margin bleeds the tap surface into the gutter while
     the tile stays on the sheet's left edge. It was `-var(--space-4)`,
     which is not a value — the declaration was dropped whole and every
     row sat 16px right of the links above it. */
  padding: var(--space-2) var(--space-4);
  margin: 0 calc(-1 * var(--space-4));
  transition: background-color .15s ease;
}
.mobile-menu__item:active { background: var(--surface-hover); }
.mobile-menu__item .menu-item__tile {
  width: 42px;
  height: 42px;
  border-radius: 12px;
  corner-shape: squircle;
}
.mobile-menu__item .menu-item__tile svg { width: 24px; height: 24px; }
/* Same in the burger: the row carries a tile and reads like the
   collections above it, so it needs none of the small-print styling
   it used to wear. */

/* Elsewhere is three destinations, not three paragraphs. As full-width
   rows carrying a byline apiece it ran 240px and finished below the
   fold; as one row of three cards it is 73px and finishes above it.
   The handles the bylines carried are on the pages themselves. */
.mobile-menu__list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--space-2);
}
.mobile-menu__card {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: 8px var(--space-2);
  border-radius: 14px;
  corner-shape: squircle;
  background: var(--surface);
  text-align: center;
  transition: background-color .15s ease;
}
.mobile-menu__card:active { background: var(--surface-hover); }
/* the icon sits on the card, so it wants no ground of its own — and
   the tile's ground is a literal #fff, which on a dark page was a lit
   square the icon floated in */
.mobile-menu__card .menu-item__tile {
  width: 32px;
  height: 32px;
  background: transparent;
  box-shadow: none;
}
.mobile-menu__card .menu-item__tile svg { width: 26px; height: 26px; }
.mobile-menu__card .menu-item__title { font-size: var(--type-2xs); color: var(--ink-85); }
.mobile-menu__card .menu-item__desc { display: none; }

html.menu-locked, body.menu-locked { overflow: hidden; }
/* and a flick inside the menu shouldn't hand the scroll onward */
.mobile-menu__inner { overscroll-behavior: contain; }

/* ============================================================
   LAYOUT — one article column, portfolio width
   ============================================================ */
.wrap {
  max-width: 48rem;
  margin: 0 auto;
  padding-left: var(--space-5);
  padding-right: var(--space-5);
}
/* Full-bleed variant: the gallery walls run the whole viewport,
   sharing the nav's side gutters */
.wall {
  max-width: none;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

main { display: block; }

/* ============================================================
   PAGE HEAD — the portfolio article header
   ============================================================ */
.page-head {
  position: relative;
  padding-top: 32px;
  /* the masthead's own group ends here — everything inside it pairs at
     --space-2, and this is the step out to the page content */
  padding-bottom: var(--space-6);
}
@media (min-width: 640px) { .page-head { padding-top: 80px; } }

.crumb {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-55);
  font-weight: 500;
  margin-bottom: 12px;
}
.crumb .sep { margin: 0 2px; }
/* 11px type gives a 13px-tall link. Same invisible hit area as the
   wordmark and the pills: the crumb keeps its size, the finger gets 44.
   Only the height is expanded — the crumb is a horizontal run of links
   and a wider box would overlap its neighbours. */
.crumb a {
  position: relative;
  transition: color .15s ease;
}
.crumb a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.crumb a:hover { color: var(--ink-65); }

/* Page titles are the one place the site raises its voice: Gooper
   at its heaviest, in italic. Same size and face in both themes —
   the dark override that used to resize them is gone. */
.page-head h1 {
  font-family: var(--title-face);
  font-style: normal;
  font-weight: 600;
  font-synthesis: none;
  font-size: clamp(var(--type-2xl), 3.6vw, var(--type-3xl));
  line-height: 1.06;
  letter-spacing: -.015em;
  color: var(--ink-85);
  margin-bottom: var(--space-2);
}

.deck {
  font-size: var(--type-base);
  font-weight: 450;
  line-height: 1.45rem;
  letter-spacing: -.005em;
  color: var(--ink-80);
  max-width: 620px;
  /* a step out from the title; the stats line below steps out again */
  margin-top: var(--space-2);
}

/* Properties — the portfolio meta-grid */
.props {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  column-gap: 32px;
  row-gap: var(--space-4);
  margin-top: var(--space-5);
  padding-top: 20px;
  border-top: 1px solid var(--border);
}
@media (min-width: 640px) { .props { grid-template-columns: repeat(3, 1fr); } }
.prop { display: flex; flex-direction: column; }
.prop-label {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-55);
  font-weight: 500;
  margin-bottom: 2px;
}
.prop-value {
  font-size: var(--type-sm);
  font-weight: 450;
  line-height: 1.45;
  color: var(--ink-65);
}

/* The circular portrait, riding the head's top-right corner */
.author-avatar {
  position: absolute;
  top: 36px;
  right: 24px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: var(--shadow-raised);
  opacity: 0;
  transition: opacity .3s var(--ease-out-quart);
}

/* ============================================================
   SECTION FURNITURE — labels with a hairline (portfolio)
   ============================================================ */
.rubric {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-bottom: var(--space-5);
}
.rubric-label {
  font-size: var(--type-xs);
  font-weight: 550;
  letter-spacing: -.005em;
  color: var(--ink-78);
  white-space: nowrap;
}
.rubric::after {
  content: "";
  flex: 1;
  height: 1px;
  background: rgba(0, 0, 0, .08);
  transform-origin: left center;
  transform: scaleX(var(--rubric-scale, 1));
}

.kicker {
  display: inline-block;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-55);
}

/* Shared by every list that renders from a data file. */
.noscript-note { padding: var(--space-5) 0; color: var(--ink-55); }

/* --- Colophon + follow --- */
.colophon-note { padding-top: 64px; padding-bottom: var(--space-5); }
.colophon-note h2 {
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--ink-85);
  margin-bottom: 12px;
}
.colophon-note > p {
  font-size: var(--type-sm);
  line-height: 1.45rem;
  letter-spacing: -.005em;
  color: var(--ink-80);
  max-width: 620px;
}
.colophon-note .ornament { display: none; }

/* Link rows on the other pages */
.link-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 4px 0 8px;
}
.link-row a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 32px;
  padding: 0 14px;
  border-radius: 999px;
  font-size: var(--type-xs);
  font-weight: 500;
  color: var(--ink-65);
  background: #fff;
  box-shadow: var(--shadow-raised);
  transition: color .15s ease, transform .1s;
}
.link-row a:hover { color: var(--ink-85); }
/* The pill is 32px because that is the shape Cody approved; the finger
   needs 44. An absolutely-positioned pseudo-element adds the missing
   12px of hit area above and below without changing the pill, the row
   height, or anything else on the page. */
.link-row a { position: relative; }
.link-row a::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.link-row a:active { transform: scale(.97); }
.link-row .arrow { color: var(--ink-55); transition: transform .2s var(--ease-out-quart); }
.link-row a:hover .arrow { transform: translateX(2px); }

/* Count line closing a rendered list (pins.html) */
.list-trailer {
  padding: 20px 0 8px;
  border-top: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-55);
}
.list-trailer .live { color: var(--ink-85); }

/* ============================================================
   DASHBOARDS
   ============================================================ */
.dash-note {
  padding-top: 8px;
  font-size: var(--type-sm);
  line-height: 1.45rem;
  letter-spacing: -.005em;
  color: var(--ink-80);
}
.dash-feature { padding-top: 48px; }
.statrow {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  margin: var(--space-4) 0;
}
@media (min-width: 640px) { .statrow { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 900px) { .statrow { grid-template-columns: repeat(4, 1fr); } }
.stat {
  position: relative;
  background: var(--surface);
  border-radius: 8px;
  padding: 20px;
}
.stat::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 8px;
  pointer-events: none;
  box-shadow: var(--shadow-flush);
}
.stat .figure {
  font-size: var(--type-lg);
  font-weight: 500;
  letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-85);
  margin-bottom: var(--space-1);
}
.stat .what { font-size: var(--type-xs); line-height: 1.45; color: var(--ink-55); }
.stat .source {
  margin-top: var(--space-3);
  font-family: var(--mono);
  font-size: var(--type-2xs);
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-55);
}
.dash-body { margin-top: var(--space-5); max-width: 620px; }
.dash-body .companion { margin-bottom: 8px; }
.dash-body h2 {
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--ink-85);
  margin-bottom: 12px;
}
.dash-body > p { font-size: var(--type-sm); line-height: 1.45rem; letter-spacing: -.005em; color: var(--ink-80); }
.behind {
  margin-top: var(--space-4);
  font-size: var(--type-xs);
  color: var(--ink-55);
  line-height: 1.6;
}
.behind .label {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-40);
  margin-right: var(--space-2);
}
.behind .sep { color: var(--ink-30); margin: 0 var(--space-1); }
.btn-open {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  height: 32px;
  padding: 0 14px;
  margin-top: 20px;
  border-radius: 999px;
  font-size: var(--type-xs);
  font-weight: 500;
  background: var(--ink-85);
  color: var(--background);
  transition: background-color .15s ease, transform .1s;
}
.btn-open:hover { background: var(--ink-78); }
/* 32px pill, 44px target — see .link-row a for the reasoning */
.btn-open { position: relative; }
.btn-open::after {
  content: "";
  position: absolute;
  left: 0; right: 0; top: 50%;
  height: 44px;
  transform: translateY(-50%);
}
.btn-open:active { transform: scale(.97); }

/* ============================================================
   ABOUT
   ============================================================ */
/* The portrait floats right inside the essay, the reference's
   about-page pattern; on phones it sits full-width up top. */
.about-essay { padding-top: var(--space-4); }
.portrait { margin: 0 0 var(--space-5); }
@media (min-width: 640px) {
  .portrait { float: right; width: 38%; margin: 4px 0 16px 32px; }
}
.portrait .cover-clip { border-radius: 8px; overflow: hidden; position: relative; box-shadow: var(--shadow-flush); }
.portrait img { width: 100%; object-fit: cover; }
.portrait .portrait-clip { aspect-ratio: 540 / 655; }
.portrait figcaption { margin-top: 8px; font-size: var(--type-2xs); line-height: 1.5; color: var(--ink-55); }
.about-essay .lede { color: var(--ink-85); }

.pull-quote {
  position: relative;
  clear: both;
  margin: 28px 0;
  background: var(--surface);
  border-radius: 8px;
  padding: var(--space-5);
}
.pull-quote::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 8px;
  pointer-events: none;
  box-shadow: var(--shadow-flush);
}
.pull-quote .ornament { display: block; font-size: 16px; color: var(--heart); margin-bottom: var(--space-3); }
.pull-quote blockquote {
  font-family: var(--serif);
  font-size: 19px;
  font-weight: 500;
  font-style: italic;
  line-height: 1.4;
  letter-spacing: -.005em;
  color: var(--ink-85);
}
.pull-quote figcaption {
  margin-top: 12px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-40);
}

.atlas {
  margin: 20px 0;
  background: var(--surface);
  border-radius: 8px;
  padding: 16px 20px;
  position: relative;
}
.atlas::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 8px;
  pointer-events: none;
  box-shadow: var(--shadow-flush);
}
.atlas-label {
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .08em;
  text-transform: uppercase;
  /* --ink-40 measured 2.82:1 light / 3.83:1 dark on 10px text. --ink-55
     is the quietest step that still clears AA (4.74 / 6.45). */
  color: var(--ink-55);
  margin-bottom: var(--space-2);
}
.atlas-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 7px 0;
}
.atlas-row + .atlas-row { border-top: 1px solid var(--border); }
.atlas-when {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-55);
  white-space: nowrap;
}
.atlas-where { font-size: var(--type-sm); font-weight: 500; color: var(--ink-85); text-align: right; }

.signoff {
  font-family: var(--serif);
  font-style: italic;
  font-weight: 500;
  font-size: var(--type-md);
  color: var(--ink-85);
  margin-top: var(--space-5);
}

.about-essay a, .colophon-note a, .dash-note a, .deck a {
  font-weight: 500;
  text-decoration: underline;
  text-decoration-color: rgba(0, 0, 0, .22);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color .15s ease;
}
.about-essay a:hover, .colophon-note a:hover, .dash-note a:hover, .deck a:hover { color: var(--ink-85); }

/* ============================================================
   FOOTER — portfolio footer, with the library's four stars
   ============================================================ */
.footer {
  margin-top: 80px;
  padding: 32px var(--gutter);
  border-top: 1px solid var(--border);
}
.footer__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-5);
  font-size: var(--type-2xs);
  color: var(--ink-55);
}
@media (min-width: 640px) {
  .footer__inner { flex-direction: row; align-items: center; }
}
.footer__left {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 12px;
}
/* Four six-pointed stars — the Chicago wink, in the wordmark red */
.footer__stars {
  color: var(--heart);
  font-size: 11px;
  letter-spacing: .5em;
  margin-right: -.5em;
}
.footer__links { display: flex; align-items: center; gap: var(--space-4); }
/* Padding out to 44px and pulling the same amount back in margin: the
   target is a finger tall, the footer's height is untouched. */
.footer__links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin-block: -12px;
  transition: color .15s ease;
}
/* --ink-85 is exactly this rgba in light mode and flips to near-white
   in dark. It was the literal, which meant hovering a footer link under
   html.dark painted it near-black on the near-black ground. */
.footer__links a:hover { color: var(--ink-85); }

/* "Live" pill — the dashboards really are live apps */
.live-pill {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 22px;
  padding: 0 9px;
  border-radius: 999px;
  background: #EBF9F0;
  box-shadow: inset 0 0 0 1px rgba(48, 164, 108, .18);
  font-family: var(--sans);
  font-size: 11px;
  font-weight: 550;
  font-style: normal;
  letter-spacing: 0;
  color: #1F7A4D;
  vertical-align: 4px;
  margin-left: var(--space-3);
}
.live-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: #30A46C;
  animation: live-pulse 2.4s ease-in-out infinite;
}
@keyframes live-pulse {
  0%, 100% { box-shadow: 0 0 0 0 rgba(48, 164, 108, .35); }
  50%      { box-shadow: 0 0 0 4px rgba(48, 164, 108, 0); }
}

/* ============================================================
   YEAR RULES — a mono stat line under a page head, and the
   serif year headings the collections index groups by
   (js/hobby.js; #experienceList restyles .log-year below).
   ============================================================ */
.log-stats {
  font-family: var(--mono);
  font-size: var(--type-2xs);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--ink-55);
  margin-top: var(--space-2);
}
/* THE NAME, on a collection page, is set small — nearer a byline
   than a masthead. Every other page keeps the full display size, and
   should: on those the heading IS the page. Here the pictures are,
   and a 44px black italic standing directly above the lead frame was
   the loudest thing on a page whose whole argument is the photograph
   underneath it. Both reference sites do the same — a photographer's
   name set quietly and the work set large. */
/* Small enough to be CONTEXT rather than a headline: on these pages
   the name tells you what you're looking at and then gets out of the
   way, the way a plate beside a print does. It went 44 → 28 → 18.

   And NOT in Gooper. Every other page head is set in it — heavy,
   italic, 800 — which is a display face doing display work at 44px.
   Shrunk to 18 it stopped reading as a headline and started reading
   as a dense little slab, fighting the clean mono line underneath.
   This is Cheltenham, the site's own serif, upright at 500: a text
   face at a text size, which is what a caption is. Real weights, so
   `font-synthesis: none` has nothing to fake. */
/* ONE FAMILY on the photography pages: the nav's Geist, top to
   bottom. The full tour, so none of it gets retried — Gooper (a
   display face doing display work, too dense once shrunk to a
   caption), Cheltenham (right size, wrong voice), Instrument Serif
   (ships ONE weight, 400, so its headings came out lighter than the
   450 sans underneath them — an inverted hierarchy no size could fix,
   because there is no heavier cut to reach for).

   Geist is variable 100-900, so a heading is the same face as the line
   below it carrying more weight. No second family, nothing to
   harmonise, and weight is a dial rather than a shipped file.

   Regards was tried late — the site's own former display serif, high
   contrast, still sitting undeclared in fonts/. Too much at these
   sizes. Roony was tried last, set in caps so its missing lowercase
   couldn't bite: legible as two words in the wordmark, unreadable as
   "TAME IMPALA: DEADBEAT TOUR WITH DJO", because an all-caps display
   face has no word-shapes to scan by over a long string.

   That is every face in fonts/. Geist holds the headings not because
   it won on looks but because it is the only one that can: variable
   weight, real lowercase, legible at 17px. A replacement has to clear
   all three, and none of the others do.

   Sits after `.page-head h1`, which is the same weight of selector,
   so source order is what beats Gooper here. */
.xp-head h1 {
  font-family: var(--title-face);
  font-style: normal;
  font-weight: 600;
  font-size: clamp(var(--type-xl), 2.4vw, var(--type-2xl));
  line-height: 1.2;
  letter-spacing: 0;
}
/* the collection page's is a plate, not a masthead — smaller again */
#galleryTitle {
  font-size: var(--type-xl);
  line-height: 1.25;
  margin-bottom: 0;
}
/* A collection page's masthead is NOT the centred 640px column the
   other page heads use. It sits on the page's own left edge — the
   same `--gutter` the nav's wordmark and the wall's frames stand on —
   so the title, the bar above it and the grid below it all share one
   line down the page, and the lead frame is the only thing that
   breaks out of it. */

/* THE SHOOT'S DETAILS, on a collection page: when, where, how many
   frames, what they were taken on. One run in one voice, because they
   are one kind of thing.

   It was briefly split — the date large and full-ink here, the counts
   small and grey in a .log-stats line under the note — and the masthead
   then ran display-italic, big mono, serif, small mono down four
   consecutive lines. Four treatments for a name, a sentence and a set
   of facts. The facts are the facts; they get one size.

   A step up from .log-stats (12.5 against 11, ink-55 against ink-40)
   because on this page the line is doing real work rather than
   counting things — the date in it is what makes the page a record of
   a day. It is not doing enough work to shout. */
/* The page's two faces and no third one: Cheltenham above for the
   name, the nav's Geist here for the facts. This line was set in the
   mono for a while, on the theory that a date and a camera are data —
   but it sat under a serif title on a page with no other mono on it,
   so it read as a stray voice rather than a register. Same sans as
   the bar at the top of the page.

   Not uppercased either. It carries equipment, and equipment names
   are case-carrying — "100-400mm f/4.5-5.6" is a convention, not
   decoration, and caps mangle it rather than merely shouting it. */
.xp-when {
  font-family: var(--sans);
  font-size: var(--type-sm);
  font-weight: 450;
  letter-spacing: 0;
  line-height: 1.5;
  color: var(--ink-55);
  margin: var(--space-4) 0 var(--space-6);
}

/* ============================================================
   GALLERIES — the park.photos register: big rounded-rectangle
   covers, two across and full-bleed, the trip title rising in
   over a soft scrim on hover (always visible on touch), a
   small circular arrow chip, and the whole tile clicking
   through to its gallery.
   ============================================================ */
.galleries-sec { padding-top: 0; }

/* Section heads in park's display voice, shared by the continents
   ledger and the trips wall */
.sec-head { margin-top: clamp(64px, 9vw, 110px); }

/* ============================================================
   COLOUR THEME — three states, one control
   ============================================================
   `html[data-theme]` is what was CHOSEN (auto · light · dark);
   `html.dark` is what that resolves to right now. Auto follows the
   sun, computed before paint by the inline head script. Auto used to
   be unreachable: the old switch was a two-way flip that wrote
   localStorage on the first click, so once you touched it the
   sun-driven default was gone for good.

   LIGHT is the default a first-time visitor gets — auto is still on
   the control, it is just no longer what you land on. Nothing in this
   file encodes that; the default lives in the head script and in
   `set()` in js/review.js, and the selectors below only read whatever
   those two agreed on.

   The bar carries one cycling glyph. The mobile menu carries the
   labelled three-way, because below 768px the open overlay covers
   the bar entirely — the switch used to sit underneath it, visible
   but untouchable until you closed the menu again.

   Both are written by scripts/shell.js. */
.theme-btn {
  flex: none;
  width: 32px;
  height: 32px;
  margin-left: var(--space-2);
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-55);
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background-color .15s ease, color .15s ease;
}/* 32px glyph, 44px target. The 6px left margin absorbs the expansion
   on that side exactly, and the button is last in the bar so the right
   side grows into the nav's own gutter — it overlaps no neighbour. */
.theme-btn { position: relative; }
.theme-btn::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 44px; height: 44px;
  transform: translate(-50%, -50%);
}

.theme-btn:hover { background: var(--surface-hover); color: var(--ink-85); }
.theme-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* the burger pulls itself 8px into the gutter; at desktop widths the
   glyph is the last item in the bar and needs the same optical hug */
@media (min-width: 768px) { .theme-btn { margin-right: -var(--space-2); } }
/* Below the burger breakpoint the glyph stays — this used to be
   `display: none`, because the sheet covered the bar and left the
   switch visible but untouchable, and a second labelled control had
   to be built into the sheet to compensate. The sheet opens under the
   bar now, so the one glyph serves both states and the copy is gone.
   The links and the scrolltitle are hidden at this width, so the auto
   margin is what keeps the glyph off the wordmark and against the
   burger; the 6px clears the burger's 44px box exactly, since this
   button's own 44 is an ::after 6px wider than the 32px circle. */
@media (max-width: 767.98px) {
  .theme-btn { margin-left: auto; margin-right: var(--space-2); }
}

.theme-btn__stack { position: relative; display: block; width: 17px; height: 17px; }
.theme-i {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  transform: rotate(-45deg) scale(.55);
  transition: opacity .18s var(--ease-out-quart), transform .3s var(--ease-out-quart);
}
html[data-theme="auto"]  .theme-i--auto,
html[data-theme="light"] .theme-i--sun,
html[data-theme="dark"]  .theme-i--moon { opacity: 1; transform: none; }

/* `.theme-pick` — the labelled three-way the burger sheet used to
   carry — is deleted, markup and CSS. It only existed because the
   sheet covered the bar and took the glyph with it. `[data-theme-set]`
   is still wired in js/review.js and still works, so a control that
   sets a state directly can come back without touching that; but the
   site should have one way to set the theme, and it is the glyph. */

/* The flip used to be a hard cut across the whole page. A short
   dissolve on the surfaces that actually carry the theme reads as a
   dimmer rather than a switch. `.theme-anim` is added by review.js
   only around a deliberate change, so the theme the head script sets
   before first paint never animates in. */
html.theme-anim body,
html.theme-anim .nav,
html.theme-anim .footer,
html.theme-anim .mobile-menu__inner {
  transition: background-color .32s var(--ease-out-quart), color .32s var(--ease-out-quart);
}

/* ============================================================
   WHERE I'VE BEEN — the filterable cloud: trips, shows, and
   what's next, set big in the display serif with rounded photo
   thumbs; shows carry a ♪ badge, plans an →, and the four
   filter pills swap the view.
   ============================================================ */
.places-sec { padding-top: 0; }
/* Section furniture inside the main pane, vollendorf's register */
.sec-label {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: clamp(72px, 9vw, 110px);
}
.sec-label span {
  font-size: var(--type-xs);
  font-weight: 550;
  letter-spacing: -.005em;
  color: var(--ink-85);
  white-space: nowrap;
}
.sec-label::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--border);
}
.sec-label + .sec-sub { margin-top: var(--space-4); }
.filter-pill.is-active {
  background: #17170F;
  border-color: transparent;
  color: #FBF9F0;
}
html.dark .filter-pill.is-active { background: #FBF9F0; color: #131313; }
.place {
  display: inline-flex;
  align-items: center;
  gap: clamp(12px, 1.5vw, 22px);
}
.place-thumb {
  width: clamp(44px, 4.6vw, 64px);
  height: clamp(44px, 4.6vw, 64px);
  border-radius: clamp(12px, 1.3vw, 16px);
  object-fit: cover;
  display: block;
  transition: transform .3s var(--ease-out-quart);
}
.place-badge {
  flex: none;
  width: clamp(44px, 4.6vw, 64px);
  height: clamp(44px, 4.6vw, 64px);
  border-radius: clamp(12px, 1.3vw, 16px);
  border: 1px solid var(--border);
  background: var(--surface);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--display);
  font-size: clamp(24px, 2.6vw, 38px);
  color: var(--ink-55);
  transition: transform .3s var(--ease-out-quart);
}
.place-name {
  font-family: var(--display);
  font-weight: 400;
  font-synthesis: none;
  font-size: clamp(24px, 3vw, 40px);
  letter-spacing: -.005em;
  line-height: 1.1;
  color: var(--ink-85);
  transition: color .15s ease;
}
.place-tag {
  align-self: flex-start;
  margin-top: clamp(4px, .8vw, 12px);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--border);
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-40);
  white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .place:hover .place-thumb,
  .place:hover .place-badge { transform: scale(1.08) rotate(-2deg); }
  a.place:hover .place-name { color: #E5484D; }
}
.place--soon .place-name { color: var(--ink-30); }
@keyframes place-pop {
  from { opacity: 0; transform: translateY(12px) scale(.97); }
  to { opacity: 1; transform: none; }
}
.place-in { animation: place-pop .45s var(--ease-out-quart) both; }
@media (prefers-reduced-motion: reduce) {
  .place-in { animation: none; }
}
.gcard {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  border-radius: 28px;
  overflow: hidden;
  background: var(--surface);
}
.gcard img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .7s var(--ease-out-quart);
}
.gcard::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  border-radius: inherit;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
  pointer-events: none;
}
.gcard::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(to top, rgba(23, 23, 23, .45), rgba(23, 23, 23, 0) 40%);
  opacity: 0;
  transition: opacity .3s ease;
  pointer-events: none;
}
.gcard-label {
  position: absolute;
  left: 22px;
  bottom: 17px;
  right: 70px;
  z-index: 2;
  color: #fff;
  font-size: 16px;
  font-weight: 550;
  letter-spacing: -.005em;
  line-height: 1.3;
  text-shadow: 0 1px 14px rgba(0, 0, 0, .3);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .3s ease, transform .35s var(--ease-out-quart);
}
.gcard-meta {
  display: block;
  font-size: var(--type-xs);
  font-weight: 450;
  color: rgba(255, 255, 255, .82);
  margin-top: 1px;
}
.gcard-go {
  position: absolute;
  right: 16px;
  bottom: 16px;
  z-index: 2;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: #fff;
  color: var(--ink-85);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: var(--type-base);
  box-shadow: 0 2px 12px rgba(0, 0, 0, .18);
  opacity: 0;
  transform: translateY(6px);
  transition: opacity .3s ease, transform .35s var(--ease-out-quart);
}
@media (hover: hover) and (pointer: fine) {
  .gcard:hover img { transform: scale(1.025); }
  .gcard:hover::after, .gcard:focus-visible::after,
  .gcard:hover .gcard-label, .gcard:focus-visible .gcard-label,
  .gcard:hover .gcard-go, .gcard:focus-visible .gcard-go { opacity: 1; transform: none; }
}
@media (hover: none) {
  .gcard::after, .gcard-label, .gcard-go { opacity: 1; transform: none; }
}

/* The rest of the library, one floor down */
.backstacks { padding-top: 64px; }

/* Gallery pages: prev/next trail */

/* ============================================================
   THE STUDIO — the vincelo register: the trip ledger fixed on
   the left (date · count · name, scroll-synced), the stream
   scrolling on the right. On small screens the ledger moves
   into the burger menu and the stream stands alone.
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}
/* Photography pages run wide: a small left column and as much room for
   frames as the window will give. Only the feed is capped, below. */
.studio { padding: 0 clamp(20px, 2vw, 36px); }
.studio-main .colophon-note h2 {
  font-family: var(--display);
  font-weight: 400;
  font-synthesis: none;
  font-size: var(--type-lg);
  letter-spacing: -.005em;
}
.studio-main .wall {
  padding-left: 0;
  padding-right: 0;
  max-width: none;
}
.studio-main .wrap { max-width: none; }
/* The vertical nav carries two things and nothing else: the mark
   up top, then the galleries, Everything down to the oldest. Rows
   take the menu's tile hover — a rounded rectangle sitting a step
   outside the text, which is why the mark and the foot are inset to
   match the names rather than the rectangles. */
.side-trip {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 14px;
  padding: 5px 10px;
  border-radius: 8px;
  transition: background-color .15s ease;
}
.side-trip:hover,
.side-trip.is-active { background: var(--surface-hover); }
/* Notion's tree at this register: the specific events nest under
   their section as a dense list — indented, smaller, names only. */
.side-sub { margin: 1px 0 5px; }
.side-sub .side-trip { padding: 3px 10px 3px 24px; }
.side-sub .side-name {
  font-size: 12.5px;
  font-weight: 450;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.side-trip-meta {
  font-size: 11px;
  font-weight: 450;
  color: var(--ink-40);
  white-space: nowrap;
  transition: color .15s ease;
}
.side-trip.is-active .side-trip-meta { color: #E5484D; }
/* Everything sits above a hairline, apart from the trips it stands
   for. The rule floats in the gap rather than riding the row, so the
   hover rectangle keeps its shape. */
.side-trip.is-all {
  position: relative;
  margin-bottom: 21px;
}
.side-trip.is-all::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -10px;
  height: 1px;
  background: var(--border);
}
/* The statement, vollendorf's hero pattern at sidebar scale: a
   quiet serif line with a smaller sans line beneath */
.side-state {
  margin-top: 18px;
  font-family: var(--display);
  font-weight: 400;
  font-synthesis: none;
  font-size: 19px;
  line-height: 1.35;
  letter-spacing: -.005em;
  color: var(--ink-85);
  max-width: 24ch;
}
.side-name {
  font-size: var(--type-xs);
  font-weight: 550;
  line-height: 1.4;
  letter-spacing: -.005em;
  color: var(--ink-55);
  transition: color .15s ease;
}
.side-trip:hover .side-name { color: var(--ink-85); }
.side-trip.is-active .side-name { color: var(--ink-85); }
/* The ledger inside the burger menu on small screens */
.mobile-menu__triplist { display: flex; flex-direction: column; }

/* ============================================================
   THE STREAM — the wall of every frame: masonry columns per
   trip, thin roll lines between trips, captions as pills on
   the photos, and a lightbox
   ============================================================ */
.stream-sec { padding-top: clamp(40px, 6vw, 72px); }
.stream-head .photo-stats {
  margin: 0;
  padding: 0;
  border: 0;
}
.roll { padding-top: var(--space-3); }
.roll-head {
  display: flex;
  align-items: baseline;
  gap: 14px;
  padding: 18px 0;
  border-top: 1px solid var(--border);
}
.roll-place {
  font-family: var(--display);
  font-weight: 400;
  font-synthesis: none;
  font-size: clamp(20px, 2.2vw, 28px);
  line-height: 1.1;
  color: var(--ink-85);
  transition: color .15s ease;
}
.roll-meta {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-40);
  white-space: nowrap;
}
.roll-go {
  margin-left: auto;
  color: var(--ink-55);
  transition: transform .25s var(--ease-out-quart), color .15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .roll-head:hover .roll-place { color: #E5484D; }
  .roll-head:hover .roll-go { transform: translateX(4px); color: var(--ink-85); }
}
/* The way back, the reference's: a quiet return link where the
   page begins */
.back-link {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-size: var(--type-xs);
  font-weight: 450;
  color: var(--ink-55);
  transition: color .15s ease;
}
.back-link .arrow { transition: transform .25s var(--ease-out-quart); }
.back-link:hover .arrow { transform: translateX(-3px); }
/* ============================================================
   THE FEED — what I'm up to. One card a post, newest first, the
   photographs tiled inside it rather than running the page.
   ============================================================ */
.feed-list { display: flex; flex-direction: column; gap: var(--space-4); padding-bottom: 60px; }
.card {
  padding: 18px 20px 20px;
  border: 1px solid var(--border);
  border-radius: 16px;
  background: var(--surface);
  transition: border-color .15s ease;
}
@media (hover: hover) and (pointer: fine) {
  .card:hover { border-color: var(--ink-30); }
}
/* The tiles: one full width in its own orientation, two abreast as
   an upright pair, three as a tall beside a stacked pair, four
   square. Fixed ratios so a card never stretches to whatever shape
   the photographs happen to be; the wall's gaps and corners. */
.tiles {
  display: grid;
  gap: clamp(4px, 0.4vw, 5px);
  margin-top: var(--space-3);
  border-radius: 20px;
  overflow: hidden;
}
.tiles--3 .tile:first-child { grid-row: span 2; }
.tiles .tile { position: relative; margin: 0; min-height: 0; overflow: hidden; }
/* scoped under .tiles to outrank the .shot-btn rules further down the
   file, which otherwise win on source order and give every tile its own
   rounded corners and an auto height */
.tiles .tile-btn {
  display: block;
  width: 100%;
  height: 100%;
  padding: 0;
  border: 0;
  border-radius: 0;
  background: var(--surface-hover);
  cursor: zoom-in;
  overflow: hidden;
}
.tiles .tile-btn::after { content: none; }
.tiles .tile img,
.tiles .tile video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .5s var(--ease-out-quart);
}
@media (hover: hover) and (pointer: fine) {
  .tiles .tile-btn:hover img { transform: scale(1.03); }
}
/* the clip's length, burned quietly into the corner */
.tile-dur {
  position: absolute;
  left: 10px;
  bottom: 10px;
  padding: 4px 9px;
  border-radius: 6px;
  background: rgba(10, 10, 10, .6);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  font-family: var(--mono);
  font-size: 11px;
  color: #fff;
  pointer-events: none;
}
/* the rest of the set as a quiet pill, not a veil over the frame */
.tile-more {
  position: absolute;
  right: 10px;
  bottom: 10px;
  padding: 5px 11px;
  border-radius: 999px;
  background: rgba(10, 10, 10, .6);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  font-family: var(--mono);
  font-size: var(--type-2xs);
  font-weight: 500;
  pointer-events: none;
}

/* A clip holds still under a play button until it's asked for, the
   duration burned into the corner — the reference's treatment, and
   the reason a six-second video reads as a moment and not a file. */
.vid {
  position: relative;
  border-radius: 14px;
  overflow: hidden;
  background: #000;
  box-shadow: var(--shadow-flush);
}
.vid video { display: block; width: 100%; }
/* A clip's whole surface opens the viewer; the speaker sits above
   it so a press for sound never enlarges the frame. */
.vid-open {
  position: absolute;
  inset: 0;
  z-index: 1;
  padding: 0;
  border: 0;
  background: none;
  cursor: zoom-in;
}
/* The speaker button lived here — a clip in the journal used to run
   itself, muted and looping, and the speaker was the only control it
   needed. A post's clip now carries ordinary controls instead, so
   nothing autoplays anywhere and nothing needs muting by hand.

   The clip's length, in the corner. It has to
   be taken out of flow or its line box props the container open
   below the video and shows a band of the black backing. */
.vid-dur {
  position: absolute;
  left: 10px;
  bottom: 10px;
  z-index: 2;
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(10, 10, 10, .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.4;
  color: #fff;
  pointer-events: none;
}
.shot-cap {
  display: block;
  margin-top: 8px;
  font-family: var(--serif);
  font-size: var(--type-sm);
  line-height: 1.45;
  color: var(--ink-55);
}
.shot-credit { margin-left: 8px; font-size: var(--type-2xs); color: var(--ink-55); }
/* `.roll` used to carry `container-type: inline-size` so the wall
   could step its columns by container width. Nothing queries it any
   more — the wall steps by viewport (see below) and no rule in the
   sheet uses a cq unit — so the containment came out with it. It went
   briefly back on for a left-hand nav rail, which would have made the
   viewport a lie; that rail was reverted and so was this. */
/* ---------- The wall, and every collection's gallery ----------
   Square corners and a lot of air, after franzgruenewald.com. Every
   frame fills its whole column in one of two boxes — 4:5 upright,
   5:4 landscape — and is centre-cropped to fill it. The shape is read
   off what was shot, in js/hobby.js, so nothing is tagged by hand.

   Two rules do the work. The two heights are what keep the wall from
   reading as a rank of identical stamps; and the cells centre on
   their row, which turns that difference into a stagger rather than a
   ragged top edge. A landscape frame sits inset among uprights —
   shorter, starting lower and ending higher — and that is the look.

   This REPLACED a scheme that kept every frame at the proportions it
   was shot at and sized it by area (`--fw`, since deleted). The
   frames are cropped now. That is the trade the reference makes and
   it was taken deliberately — don't "restore" the old behaviour on
   the strength of the no-crop principle alone.

   The column count steps by VIEWPORT, not by container. It used to
   step by container because the wall once sat beside a sidebar; both
   walls now run the full width with only the nav's gutters beside
   them, so the viewport is the honest ruler and these are the
   reference's own breakpoints.

   No titles under the frames. The picture is the whole message. */
.stream-grid {
  --wall-cols: 1;
  display: grid;
  grid-template-columns: repeat(var(--wall-cols), minmax(0, 1fr));
  align-items: center;
  /* The gap GROWS rather than steps. It used to be 32px, then jump to
     80px at 1024 on the same pixel the columns went 2 → 4 and the page
     gutter went 24 → 80: three discontinuities at once, which took the
     photographs from 465px wide at 1023 down to 153px at 1024. A
     1024px laptop showed smaller pictures than an 884px folding phone.
     Interpolating the gap removes one of the three, and the column
     ladder below removes the second. */
  gap: clamp(32px, 4.2vw, 80px);
  padding-bottom: clamp(52px, 6vw, 104px);
}
/* THE COLUMN LADDER climbs one step at a time — 1, 2, 3, 4, 5 — and
   never 2 → 4. Adding a column costs roughly n/(n+1) of the frame's
   width, so a single step is a 33% dip at worst and the frames grow
   back before the next one. Skipping a step doubles that in one pixel.
   Measured widths at the sizes that matter: 360 → 312, 884 (Z Fold
   open) → 402, 1024 → 416, 1440 → 275, 1920 → 288, 2560 → 416. The
   frames stay in a 260–480 band from tablet up instead of collapsing
   to thumbnails in the middle of the range. Check that band again if
   you move a breakpoint or the gutter. */
@media (min-width: 768px)  { .stream-grid { --wall-cols: 2; } }
@media (min-width: 1152px) { .stream-grid { --wall-cols: 3; } }
@media (min-width: 1440px) { .stream-grid { --wall-cols: 4; } }
@media (min-width: 1920px) { .stream-grid { --wall-cols: 5; } }
.stream-grid .shot {
  margin: 0;
  width: 100%;
}
.stream-grid .shot-btn,
.stream-grid .vid {
  width: 100%;
  height: auto;
  border-radius: 0;
  corner-shape: normal;
  background: none;
}
/* EVERY FRAME KEEPS THE SHAPE IT WAS SHOT IN. There were two boxes
   here — 4:5 for uprights, 5:4 for landscapes — with the picture
   inside them on object-fit: cover, which bought a tidy grid rhythm by
   cutting off whatever didn't fit. It cut a lot: 63 of San Diego's 78
   frames are the same 3:2 and each lost 16.6% off its sides, the
   side-profile rhino lost 21.3% — which is the horn — and the average
   across that collection was 14.8%.

   Nothing was added to undo it. shotHtml() already writes
   style="aspect-ratio:W/H" on every img and video from the recorded
   dimensions, and the note that stood here explained why that had no
   effect: giving the box a definite width AND a definite height is
   precisely what makes an inline ratio go inert. Releasing the height
   hands the frame back to it, so the box is now whatever the
   photograph is. object-fit goes with it — once the box matches the
   picture there is nothing left to fit.

   Rows are ragged as a result, and that is the trade. `align-items:
   center` was already on the grid, so a short frame centres against a
   taller neighbour rather than stretching to meet it. Every frame in
   js/photos.js carries w and h, so each one still reserves its exact
   height before the picture loads and nothing shifts; a frame added
   without dimensions WOULD shift the page, which is one more reason
   the poster records them. `.shot--wide` / `.shot--tall` were read by
   the four deleted rules and by nothing else, so they are gone from
   the renderer too. */
/* the flush inset ring belonged to the rounded tile; a square frame
   sits on the page unaided */
.stream-grid .shot-btn::after { display: none; }
.stream-grid .shot-btn img,
.stream-grid .vid video {
  width: 100%;
  height: auto;
}
.stream-grid .shot-cap { display: none; }
/* THE LEAD AND ITS PLATE. The frame is capped by HEIGHT and keeps its
   own proportions, so its width is whatever its shape makes it — it
   can never line up with a grid that runs gutter to gutter. Rather
   than fight that, the title and the date sit under it and take that
   same width: `fit-content` shrinks the figure to the picture, so the
   plate's left edge IS the picture's left edge, and the pair centres
   on the page as one object. A print with its label.

   Every part of the chain has to be fit-content or the button's
   `width: 100%` blows it back out to the full measure.

   The height is the definite dimension and the ratio supplies the
   width. Don't set both to auto: the renderer's inline aspect-ratio
   then has nothing to resolve against and the frame collapses to zero
   before the picture loads. */
.xp-lead {
  width: fit-content;
  max-width: 100%;
  margin: 0 auto;
}
.xp-lead .shot,
.xp-lead .shot-btn {
  width: fit-content;
  max-width: 100%;
  margin: 0;
  border-radius: 0;
  background: none;
}
.xp-lead .shot-btn::after { display: none; }
.xp-lead .shot-btn img {
  display: block;
  width: auto;
  /* Was min(64vh, 640px). The bar, #leadSec's padding-top and the
     plate carrying the name and date take ~274px above the frame, so
     74vh puts its foot just past the fold — 6px over on a 1030px
     screen, ~66px on an 800px one. That is deliberate and is the whole
     margin available: the title stays on screen (it is what makes the
     frame a print with a label rather than a hero image) while the
     foot of the picture hangs below the fold and pulls you into the
     wall under it. Going higher starts pushing the title off, which is
     the thing this layout exists to prevent. The px half stops a tall
     desktop monitor rendering it enormous. */
  height: min(74vh, 860px);
  max-width: 100%;
  object-fit: contain;
}
.xp-lead .shot-cap { display: none; }
/* The plate sits ABOVE the frame and takes its width, so the name and
   the date hang off the picture's own left edge rather than the
   page's. It is set small on purpose: it introduces the frame under
   it and must not out-shout it. `figcaption` is allowed to be either
   the first or the last child of a figure — first, here. */
/* Three classes on purpose. `.page-head.xp-head` carries the page's
   gutter padding and sits later in this file, so a two-class rule
   here loses to it and the plate hangs 80px off the picture's edge.
   Out-weigh it rather than depend on order. */
.xp-lead__plate .page-head.xp-head {
  padding: 0 0 12px;
  max-width: none;
  margin: 0;
  /* centred ON the picture, not flush to its left edge — the figure
     is already the picture's width, so centring here centres against
     the frame itself. `.page-head.wall` sets left, and this rule
     out-weighs it. */
  text-align: center;
}
/* The plate zeroed the padding `.page-head` normally brings, which is
   what used to hold the masthead off the bar — so the room above the
   lead is set here instead. */
/* HELD-OPEN SLOTS. 23 of the 27 collections are logged but not yet
   edited, so most of these pages have no frames at all. They keep the
   page's shape rather than collapsing to a title on white — a blank
   where the lead goes and a wall of blanks under it.

   Flat and inert, deliberately. The collections index made the same
   call in the same words: nothing is loading here and nothing is
   about to, so a shimmering skeleton would be a lie about the state
   of the page. These are just quiet grey rectangles, and the note
   under the plate says in words that the pictures aren't up yet. */
.xp-blank {
  display: block;
  background: var(--surface);
}
/* Held open at the lead's own size, or the page visibly changes shape
   when the pictures land. Tracks .xp-lead .shot-btn img above — if you
   move one, move both. */
.xp-blank--lead {
  width: min(66vw, 1048px);
  max-width: 100%;
  height: min(74vh, 860px);
}
.stream-grid .shot.xp-blank { width: 100%; margin: 0; }
.stream-grid .shot.xp-blank.xp-blank--tall { aspect-ratio: 4 / 5; }
.stream-grid .shot.xp-blank.xp-blank--wide { aspect-ratio: 5 / 4; }

#leadSec {
  padding-top: clamp(34px, 5.5vw, 84px);
  margin-bottom: clamp(30px, 4vw, 56px);
}
.shot { margin: 0 0 clamp(12px, 1.2vw, 18px); break-inside: avoid; }
.shot-btn {
  display: block;
  width: 100%;
  padding: 0;
  border-radius: 14px;
  overflow: hidden;
  position: relative;
  cursor: zoom-in;
  background: var(--surface);
}
.shot-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 14px;
  box-shadow: var(--shadow-flush);
  pointer-events: none;
}
.shot-btn img {
  width: 100%;
  height: auto;
  display: block;
  transition: transform .6s var(--ease-out-quart);
}
@media (hover: hover) and (pointer: fine) {
  .shot-btn:hover img { transform: scale(1.03); }
}
/* `.shot-pill` was here — a translucent lozenge of caption laid over
   the bottom-left of a frame on the front wall, faded in on hover.
   Deleted along with the markup that made it (see `shotHtml` in
   js/hobby.js). It could only be revealed by hovering, which a phone
   cannot do, so it carried a `@media (hover: none)` rule that left it
   permanently open on every frame — turning the wall into rows of
   photographs each stuck behind a grey label. Hiding it under a
   breakpoint would have left the desktop half of the same bad idea,
   so the whole treatment went. The collection page's `.shot-cap`
   figcaption remains: it sits under the frame and covers nothing. */
/* The theme control lives with the other theme rules — see
   COLOUR THEME above. The old override lived here and leaked a
   `margin-bottom: 8px` from the retired track-and-knob switch it
   was overriding: flexbox centres the margin box, so the glyph sat
   4px above every other item in the bar. */
/* The photos page opens on the wall itself — no masthead, frames
   straight under the bar. It keeps the same gutter as an
   collection's wall so the two pages line up edge for edge. */
.wall-bleed { padding: 0 var(--gutter); }
.wall-bleed .roll { padding-top: clamp(5px, 0.55vw, 8px); }
.wall-bleed .stream-grid { padding-bottom: 0; }
/* The masthead is a column down the middle of the page with its
   type ragged right — a headline measure, not a full page width.
   The wall below it still runs edge to edge. */
.page-head.wall {
  max-width: 640px;
  padding-left: var(--space-5);
  padding-right: var(--space-5);
  margin-left: auto;
  margin-right: auto;
  text-align: left;
}
/* A collection page's masthead is NOT the centred 640px column above.
   It sits on the page's own left edge — the same `--gutter` the nav's
   wordmark and the wall's frames stand on — so the title, the bar
   above it and the grid below all share one line down the page.
   It has to live HERE, after `.page-head.wall`: both selectors carry
   the same weight, so source order is the only thing deciding it. */
.page-head.xp-head {
  max-width: none;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
  margin-left: 0;
  margin-right: 0;
}
/* When and where, under the title — the same line the nav menu
   shows, set in Gooper upright so the slab answers the italic. */
.xp-byline {
  font-family: var(--sans);
  font-style: normal;
  font-weight: 500;
  font-synthesis: none;
  font-size: clamp(15px, 1.5vw, 18px);
  line-height: 1.35;
  letter-spacing: -.005em;
  color: var(--ink-55);
  /* No negative compensator. This carried -6px to claw back against a
     16px margin on the title above it; that margin is now --space-2, so
     the -6 collapsed the pair to a 0px gap and left the line below it
     closer to the essay than to its own masthead. */
  margin: 0 0 var(--space-2);
}
/* The frames used to carry a "Highlights" label and its hairline.
   The page is one collection and the pictures are the first thing on
   it — nothing needed announcing. All the label left behind is the
   air it held between the masthead and the first frame. */
#framesSec .roll { padding-top: clamp(20px, 2.4vw, 34px); }
/* a clip that isn't playing wears a play badge; the whole frame
   still opens it, so the badge is decoration */
.vid-badge {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  background: rgba(10, 10, 10, .55);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  z-index: 2;
}
.vid-badge svg { width: 22px; height: 22px; margin-left: 3px; display: block; }
/* CLIPS — a row of upright players, wide enough to watch and few
   enough that they never become a wall of movement. */
.clip-row {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(var(--space-3), 1.2vw, var(--space-4));
}
.clip-row .shot { margin: 0; }
.clip-row .vid { width: min(320px, 72vw); }
.clip-row .vid video { width: 100%; height: auto; }
@media (max-width: 520px) {
  .clip-row .vid { width: 100%; }
}
#clipsSec .sec-label { margin-top: clamp(30px, 4vw, 48px); margin-bottom: 14px; }
/* The footer is one line now: a copyright and nothing else. */
.footer__inner { justify-content: center; }
.footer__left {
  display: block;
  font-size: var(--type-2xs);
  color: var(--ink-55);
}
/* ABOUT — the masthead column the rest of the site uses, and the
   essay running under it at reading width. */
.about-read { max-width: 640px; }
.about-read h2 {
  margin: clamp(34px, 4vw, 52px) 0 var(--space-2);
  /* Recoleta, the same face and weight as every .page-head h1, so the
     section titles sit in the site's title register rather than being
     bold body text. Only 400–700 are declared — do not go past 700 here
     or the browser synthesises the weight. */
  font-family: var(--title-face);
  font-style: normal;
  font-weight: 600;
  font-synthesis: none;
  font-size: clamp(21px, 2.2vw, 27px);
  line-height: 1.15;
  letter-spacing: -.015em;
  color: var(--ink-85);
}
.about-read p {
  margin: 0 0 var(--space-4);
  font-size: 16px;
  line-height: 1.62;
  color: var(--ink-78);
}
.about-read .lede { font-size: var(--type-md); color: var(--ink-85); }
.about-read a { text-decoration: underline; text-decoration-color: var(--border); }
.about-read a:hover { text-decoration-color: currentColor; }
/* The essay's photographs. Three placements — the masthead frame, a
   two-up, and a full-column frame between sections — but one set of
   rules, so a change to the treatment cannot land on some of them and
   miss the others. */
.about-portrait { margin: 0 0 var(--space-2); }
.about-figure { margin: clamp(26px, 3vw, 38px) 0; }
.about-pair figure { margin: 0; }
.about-portrait img,
.about-figure img,
.about-pair img {
  /* height:auto or the browser lays the frame out from the width and
     height attributes, and a wrong pair there stretches the picture */
  width: 100%;
  height: auto;
  /* Square, like the wall. A radius crops the corners of a frame that
     was composed to its edges, and it reads the picture as a card. */
  border-radius: 0;
  display: block;
  background: var(--surface);
}
.about-portrait figcaption,
.about-figure figcaption,
.about-pair figcaption {
  margin-top: 9px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-55);
}
/* Two frames side by side. Stacked until the column is wide enough
   that halving it still leaves a photograph rather than a thumbnail —
   at 600px the column is 552 and each frame lands at 262, just inside
   the 260–480 band the wall is held to. */
.about-pair {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-4);
  margin: clamp(26px, 3vw, 38px) 0;
}
@media (min-width: 600px) {
  .about-pair { grid-template-columns: 1fr 1fr; }
  /* Side by side the two frames have to agree on a shape, or their
     bottoms and their captions land on different lines. 3:4 is what
     three of the four photographs on this page already are, so only
     the odd one gets touched. Unlike a corner radius — which crops
     every frame whether or not you meant it — this is one deliberate
     ratio for one row, and the single frames above and below keep
     whatever shape they were shot at. */
  .about-pair img { aspect-ratio: 3 / 4; object-fit: cover; }
}
.about-read .pull-quote {
  /* the library-era plate is gone; a rule down the side is enough */
  margin: clamp(26px, 3vw, 38px) 0;
  padding: 0 0 0 18px;
  border: 0;
  border-left: 2px solid var(--ink-30);
  background: none;
  box-shadow: none;
  border-radius: 0;
}
.about-read .pull-quote blockquote {
  margin: 0;
  font-family: var(--sans);
  font-style: italic;
  font-weight: 500;
  font-size: clamp(18px, 1.9vw, 22px);
  line-height: 1.4;
  color: var(--ink-85);
}
.about-read .pull-quote figcaption {
  margin-top: 8px;
  font-size: var(--type-xs);
  color: var(--ink-55);
}
.about-read .signoff { margin-top: 22px; color: var(--ink-55); }
.about-reach { padding-bottom: clamp(40px, 6vw, 72px); }
/* ---------- The collections index ----------
   Every collection is a band: its name across the page in Gooper, the
   byline under it, an arrow at the far edge, and its own roll running
   out past the right margin beneath — as much of it as the page will
   hold, the rest a scroll away.

   The frames are in colour from the start — they're the reason
   anyone came. It's the name that changes: grey until you arrive on
   its band, then its own ink.

   All the way back, not just the current year: 2019 gets the same
   band as this August. Only the year heading marks which one is still
   being lived. */

/* the head sits at the gutter with the bands, not centred over them
   the way a gallery's does — the type below starts at the page edge
   and the title has to start there too */
.xp-head {
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}
.xp-head > * { max-width: 46rem; }
/* every band brings its own rule on top, so a second one under the
   year heading would just double it. Scoped to the index so the log's
   year headings keep theirs. */
/* ============================================================
   THE LEDGER — the index of every collection.

   NOT a grid. The front wall already is one, and repeating it here
   would make this page a second homepage wearing a different hat.
   This is a dated list instead: the year carries the structure, each
   collection is one ruled row, and a single frame rides along as
   evidence rather than a whole roll of them.

   Every row wears the collection page's own plate — the name in
   Cheltenham over a line of facts in the nav's sans — so the index
   and the page it opens read as one thing. Two faces on the whole
   page and no third.

   WHAT THIS REPLACED. Bands: a name in heavy Gooper italic set at
   ink-30, a horizontal roll of every frame running off the right
   edge, and a rotating five-colour ink per row. The roll was the
   problem — 23 of the 27 collections have no frames yet, so the page
   was mostly grey rectangles, and the ones that did have frames
   showed the same pictures the collection page shows. One frame per
   row says the same thing and leaves the page quiet.
   ============================================================ */
/* THE YEAR IS THE PAGE'S SPINE — the thing you scroll past, because
   the whole argument of the site is that these pictures are how a
   place was on a day, and the day is what makes them worth keeping.
   `js/hobby.js` emits one <section class="xp-era"> per year, newest
   first, with the year as the h2 below.

   It is a plain heading above its rows at every width. An earlier
   version put it in a column of its own down the left, set large and
   pinned, so it rode along while its rows scrolled and handed over to
   the next year at the end of its own era — see the note below the
   rule for why that came out. */
.xp-era { display: block; }
.xp-era__year {
  font-family: var(--title-face);
  font-style: normal;
  font-weight: 600;
  /* a step below the page title, which it matched exactly at every
     width under 1083px — same face, same weight, same size, so the
     page had one level where it needed two */
  font-size: var(--type-lg);
  line-height: 1;
  letter-spacing: -.01em;
  /* 21px is under the 24px large-text threshold, so this needs the full
     4.5:1; --ink-45 measured 3.35:1. Still reads as secondary. */
  color: var(--ink-55);
  margin: clamp(30px, 3.4vw, 48px) 0 6px;
}
/* The year sits ABOVE its collections, not beside them. It spent a
   version in a column of its own down the left — set large and pinned,
   riding along while its rows scrolled — which cost the rows about
   230px of measure and gave it back nothing: a year is a divider, and a
   divider only has to say where the section starts. Above, small, and
   the frames get the width instead. */

.xp-row {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.4vw, 40px);
  /* the air sits inside the row, not between rows — house rule */
  padding: clamp(14px, 1.5vw, 22px) 0;
  border-top: 1px solid var(--border);
  text-decoration: none;
  color: inherit;
}
/* the words take a fixed narrow column so every row's frames start on
   the same line down the page */
.xp-row__text {
  flex: none;
  width: clamp(150px, 19vw, 250px);
  min-width: 0;
}
.xp-row__name {
  display: block;
  font-family: var(--title-face);
  font-style: normal;
  font-weight: 500;
  font-size: var(--type-md);
  line-height: 1.25;
  color: var(--ink-85);
  transition: color .2s ease;
}
.xp-row__meta {
  display: block;
  margin-top: var(--space-2);
  font-family: var(--sans);
  font-size: var(--type-xs);
  font-weight: 450;
  color: var(--ink-55);
}
/* HEIGHT is fixed and width follows each frame's own shape, so a run of
   mixed uprights and landscapes still lines up top and bottom. Anything
   past the edge of the measure simply clips — the row is a glimpse of
   the set, not the set. */
.xp-row__frames {
  flex: 1;
  min-width: 0;
  display: flex;
  gap: clamp(var(--space-2), 0.7vw, var(--space-3));
  overflow: hidden;
}
.xp-row__frame {
  flex: none;
  height: clamp(88px, 9vw, 130px);
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--surface);
}
.xp-row__frame--wide { aspect-ratio: 5 / 4; }
.xp-row__frame img,
.xp-row__frame .xp-blank {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* on a phone the words go above the run rather than beside it */
@media (max-width: 700px) {
  .xp-row { display: block; }
  .xp-row__text { width: auto; margin-bottom: var(--space-3); }
  .xp-row__frames { overflow-x: auto; scrollbar-width: none; }
  .xp-row__frames::-webkit-scrollbar { display: none; }
}
@media (hover: hover) and (pointer: fine) {
  .xp-row:hover .xp-row__name { color: var(--heart); }
}

/* ============================================================
   THE JOURNAL — short posts, newest first.

   WHAT THIS REPLACED. The journal used to be a THREAD: every
   note was its own row, and a run of rows from one outing was
   drawn as a "chain" hanging off a connector rail with a node
   per post, each row re-introducing the same collection, venue
   and date the reader had just read. A note is still one post —
   posting cheaply and often is the point — but the chain, the
   rail and the per-row header line are gone, and a same-day run
   now simply clusters under one dateline. See `sameRun` in
   js/hobby.js.

   THE SHAPE, measured off threads.com at a 1275px viewport
   rather than remembered:

     page ground   a grey — NOT the page white
     the column    one WHITE surface holding every post as a row,
                   divided by hairlines, radius 16
     the archive   a second card in the right margin, slightly
                   DARKER than the ground, with a hairline

   That is three grounds, not one: the page recedes, the column
   is the brightest thing on it, and the rail sits just behind
   the page. A stack of separate post-cards was tried first and
   read as a feed of objects rather than as one publication —
   which is the wrong claim for a journal. Dark keeps the same
   relationships with the page already near-black.
   ============================================================ */
/* INVERTED. The page is the white ground and each post carries its own
   light grey — the opposite of the column-on-grey it started as. A post
   is a block you can see the edges of, and the PHOTOGRAPHS SIT OUTSIDE
   IT, on the page, tied to it by proximity rather than by being inside
   its box. That is how an attachment behaves in a message thread: the
   bubble holds the words, the picture is its own thing underneath. */
body.feed-page { background: var(--background); }
html.dark body.feed-page { background: var(--background); }

/* THE LAYOUT. Three tracks: an empty one, the column, and the
   archive. Because the side tracks are both 1fr the column is
   centred on the page no matter how wide the rail is — the rail
   lives in the margin and can never push the reading measure off
   centre. Below 1100px there is no margin to live in and the whole
   thing collapses to one column, which is why the archive comes
   first in the source: on a phone it belongs under the masthead. */
/* One centred column. There was a second track here holding a sticky
   month rail; the archive is the pill row under the masthead at every
   width now, so the posts have the page to themselves. */
.journal {
  max-width: 648px;
  margin: 0 auto;
  padding: 0 var(--space-5);
}
.journal .page-head { padding-bottom: var(--space-6); }
/* The masthead wears the face; the deck says what a post is before
   the reader has scrolled to one. */
/* The masthead stacks and centres: the face on top, the title under
   it, the deck under that. It used to run the portrait and the title
   side by side on the left, which pulled the whole head off-axis from
   the column of posts below it. */
.journal .page-head { text-align: center; }
.journal-head {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-5);
}
.journal-head h1 { margin: 0; }
.journal-deck {
  margin: var(--space-2) auto 0;
  max-width: 46ch;
  font-size: var(--type-base);
  line-height: 1.6;
  color: var(--ink-55);
}
/* the month pills centre with it rather than hanging left */
.journal .archive__in { justify-content: center; }
.journal-avatar {
  flex: none;
  /* the face leads the masthead now that it sits above the title
     rather than beside it, so it carries more weight */
  width: 92px;
  height: 92px;
  border-radius: 50%;
  object-fit: cover;
  display: block;
  box-shadow: 0 0 0 1px var(--border);
}

/* ---------- THE COLUMN ----------
   ONE surface holding every post. `overflow: hidden` is load-bearing
   twice over: it keeps the corners honest, and it is what a strip
   bleeding past the last post's padding runs into — so the bleed can
   never turn into a page that scrolls sideways. */
/* The column is no longer a card. It was one surface holding every
   post; now the posts are the surfaces and the page shows between
   them. */
.posts {
  margin-bottom: clamp(64px, 8vw, 120px);
  background: none;
  border: 0;
  overflow: visible;
}
/* On a phone the cards keep a small margin off the screen edge rather
   than running out to it. Edge to edge was right when the column was
   ONE surface holding every post — then the page and the column were
   the same object. Now each post is its own card, and a card touching
   both edges of the screen stops reading as a card. */
@media (max-width: 640px) {
  .journal { padding-left: 12px; padding-right: 12px; }
}
.feed-sentinel { height: 1px; }
.noscript-note + .feed-sentinel { display: none; }

/* ---------- A POST ----------
   A row of the publication, not an object of its own: told from the
   one above it by a hairline running the column's full width, never
   by a gap and never by a border of its own. */
.post {
  --post-pad: 18px;
  /* the bubble's own inset. It used to share --post-pad with the photo
     strip's bleed, which meant loosening the writing also pushed the
     frames further past the column edge. */
  --bubble-pad: 24px;
  padding: 0;
  border: 0;
  margin-bottom: 26px;
}
.post:last-child { margin-bottom: 0; }
/* The padding is symmetric, but the last thing inside a post used to
   add its own bottom margin on top of it — a paragraph carries 18px,
   so a text-only post sat 26px from the top rule and 44px from the
   bottom one. Collapsing the final margin is what actually makes the
   inset equal on all four sides. */
.post > :last-child { margin-bottom: 0; }
@media (max-width: 560px) { .post { --post-pad: 18px; --bubble-pad: 18px; } }

/* ---------- A RUN OF POSTS ON ONE DAY ----------
   Posting more than once a day is the point, and each note is its own
   post. But three posts in a row repeating "June 20, 2026 · San Diego,
   CA" is the reader being told something they were just told.

   So a same-day, same-place run clusters the way a burst of iOS
   messages does: the first post carries the dateline, the rest sit
   tight under it with no rule and no repeated header. A new day — or
   the same day somewhere else — breaks the run and starts a fresh one.
   Threads' column, iMessage's grouping.

   (An earlier pass grouped an outing into ONE post with its notes on a
   connector rail. Rejected: the relationship was real but it made a day
   read as a single obligation rather than several cheap ones.) */
/* The boundary still has to be VISIBLE, or a run collapses into one
   flowing post — which is the thing the rail version got wrong. In
   iMessage the bubble's own shape does that job; there are no bubbles
   here, so an inset rule does it instead. Full-width rule = a new day.
   Short inset rule = same day, next post. Air alone wasn't enough. */

/* THE HEADER IS THE DATE, in full, and the place after it in the
   quiet weight. There is no title: a post is two sentences long and a
   headline over two sentences is a title on a note to yourself. The
   whole line is the permalink, so the thing you would copy is the
   thing you click. The scroll margin keeps the sticky bar off it when
   an anchor is followed. */
.post__head {
  display: block;
  /* Tight to the words under it: the date is this post's header, not a
     separate line of furniture sitting above one. The margin alone
     wasn't the whole gap — the line inherited the body's 1.45rem
     leading, which put ~4px of air under a 15px baseline before the
     margin even started, and the paragraph adds its own above. Pulling
     the leading in is what actually closes it. */
  /* Place left, date pinned right, sharing the first baseline. Inline
     with a dot reads better when every place is one word, but these
     get long — "Universal Studios Hollywood" and a date run together
     stop being one label and become a run-on. */
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 18px;
  line-height: 1.3;
  /* 18px tall, which misses even the 24px floor of WCAG 2.5.8. The
     padding buys the height and the negative margin gives it back, so
     nothing moves. Deliberately 24 and NOT 44: this is a permalink to
     the post's own anchor, and a 44px band would hang 13px down over
     the post's words and take taps meant for them — the head is first
     in the card, so the body paints on top and wins the overlap, but
     the top edge would still swallow them. A self-link is not worth
     that trade. */
  padding-block: 3px;
  margin: -3px 0 1px;
  /* the bar was sticky when this was written and is static now, so a
     permalink was landing 92px of empty space above the post */
  scroll-margin-top: var(--space-4);
}
/* A KICKER, NOT A TITLE. The dateline ran at the prose size in heavy
   ink, which made it the loudest thing in the post — a headline over
   two sentences, and a date is not what the post is about. It is
   furniture: it says when, gets out of the way, and gives every card
   the same thing in the same place so none of them look structureless.
   Small, quiet, and identical on every post. */
/* WHERE, then WHEN. The place carries the post — it is the thing that
   differs between one entry and the next — and the date sits under it
   as the stamp. A date in bold at the head of every card was furniture
   pretending to be a title. */
/* It WRAPS rather than truncating: with a long title you would rather
   read the whole thing on two lines than "Universal Studios Holl…".
   The date keeps the first line's baseline while the title runs on
   under it. */
.post__title {
  min-width: 0;
  font-size: var(--type-sm);
  font-weight: 650;
  letter-spacing: -.01em;
  color: var(--foreground);
  transition: color .15s ease;
}
/* The date sits a step below the title's size. It was matched at 16px
   and read larger, because the numerals in it have no x-height to lose
   against a lowercase word. 14px is where the two look level. */
.post__date {
  /* never shrinks — the title gives way first */
  flex: none;
  font-size: var(--type-2xs);
  font-weight: 500;
  letter-spacing: .005em;
  color: var(--ink-55);
}
.post__head:hover .post__title { color: var(--heart); }

/* THE CARD CARRIES THE GREY and holds the whole post — dateline,
   words and frames. A words-only card is only as wide as it needs to
   be, so a two-line note doesn't drag a full-width slab across the
   column; a card with frames takes the full width, because a strip
   needs a width to overflow and a photograph shouldn't be sized by
   the length of the sentence above it. */
.bubble {
  display: inline-block;
  max-width: 100%;
  padding: 18px var(--bubble-pad) 20px;
  border-radius: 16px;
  background: var(--bubble);
}
.bubble--media { display: block; }
/* air between the writing and what was shot while it was written */
.post__say + .media { margin-top: 14px; }
.post__say {
  font-size: var(--type-base);
  line-height: 1.55;
  color: var(--ink-78);
  letter-spacing: -.003em;
}
.post__say + .post__say { margin-top: var(--space-3); }

/* ---------- MEDIA, wherever prose is the point ----------
   ONE item sits in the column, capped by HEIGHT so a portrait can't
   own the screen. TWO OR MORE become a strip that runs past the
   right edge of the card and snaps. That overflow is the point: it
   says "there's more here" without a count badge, and it is the one
   place the column is allowed to break. */
.media { --frame-cap: min(460px, 58vh); margin: 0 0 20px; }
.media:last-child { margin-bottom: 0; }

/* The cap is a height, and it is set so a portrait costs roughly the
   same vertical space as a landscape rather than half again as much:
   at 660px wide a 3:2 landscape is 440px tall, so a 3:4 portrait
   capped at 460 lands at 345×460 and the two read as equals down the
   page. Capping at 600 made every portrait the tallest thing on
   screen, which was the complaint. The width left over beside an
   upright frame is not waste — it is what stops a column of mixed
   orientations from lurching. */
figure.one { margin: 0; display: flex; flex-direction: column; align-items: flex-start; }
figure.one .frame-btn { max-width: 100%; }
figure.one .frame {
  width: auto;
  max-width: 100%;
  max-height: var(--frame-cap);
  height: auto;
}
figure.one figcaption {
  margin-top: 9px;
  font-size: var(--type-xs);
  line-height: 1.5;
  color: var(--ink-55);
  max-width: 60ch;
}

.strip {
  display: flex;
  gap: var(--space-3);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding-bottom: 2px;
  scroll-snap-type: x proximity;
}
.strip::-webkit-scrollbar { display: none; }
/* the bleed: out to the card's own edge, and padded back so the last
   frame can come to rest instead of sitting under the corner */
.post .strip {
  margin-right: calc(-1 * var(--bubble-pad));
  padding-right: var(--bubble-pad);
}
.strip figure { margin: 0; flex: none; scroll-snap-align: start; max-width: 78vw; }
/* A definite height — not a max — because intrinsic sizing reads a
   definite height through the aspect ratio and a max-height doesn't;
   this is what lets the button shrink-wrap the picture. */
.strip .frame { height: clamp(220px, 30vw, 320px); width: auto; max-width: none; }
.strip figcaption {
  margin-top: 8px;
  font-size: 12.5px;
  line-height: 1.45;
  color: var(--ink-55);
  max-width: 34ch;
}

/* the frame itself: square-ish corners, the surface showing through
   while the bytes are still in the air */
/* Photographs and clips take the same corner as the bubble beside
   them, a hair tighter. They sit on the page rather than inside the
   post, so a square corner read as a crop mark against the rounded
   block above it. */
.frame { display: block; border-radius: 14px; background: var(--surface); }
/* in dark the column is already the surface, so a frame still in the
   air takes the next step up rather than vanishing into it */
html.dark .frame { background: var(--surface-hover); }
.frame[data-kind="video"] { background: #000; }
.frame-btn {
  display: block;
  width: fit-content;
  padding: 0;
  border: 0;
  /* the button clips the frame it holds, so both carry the radius */
  border-radius: 14px;
  overflow: hidden;
  background: none;
  line-height: 0;
  cursor: zoom-in;
}
.strip .frame-btn { height: auto; }

/* YouTube. An iframe, not a script — nothing is fetched from a CDN at
   parse time, and nocookie sets nothing until it is played. */
.tube {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  /* same corner as a photograph — an embed is a frame like any other */
  border-radius: 14px;
  overflow: hidden;
  background: #000;
}
.tube iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.strip .tube { width: clamp(320px, 50vw, 500px); height: auto; }

/* ---------- THE ARCHIVE ----------
   A month index beside the column, grouped by year and newest first,
   with a count per month. Clicking one filters the journal to it and
   writes ?month= into the URL, so a month is a place you can send
   somebody.

   THIS IS A DELIBERATE EXCEPTION to the house rule that filters
   belong inside the list rather than in a control beside it (see
   HANDOFF.md). Cody asked for it by name, for the journal only: the
   record runs back to 2019, and there is no way to put "September
   2021" inside a stream that renders eight posts at a time. Don't
   copy the pattern to any other page.

   It is the second card: slightly DARKER than the page ground and
   drawn with a hairline, exactly like the panel the reference keeps
   in that slot. It recedes rather than competing — a sibling of the
   column, not nav floating in the margin.

   RIGHT, not left: the masthead is set hard against the left edge of
   the column and a left rail would argue with it, and right is where
   a blog has always put its archive. */
.archive { margin: 0 0 14px; }
.archive__in {
  display: flex;
  align-items: center;
  gap: 7px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding-bottom: 2px;
}
.archive__in::-webkit-scrollbar { display: none; }
.archive__label { display: none; }
.archive__year { display: flex; align-items: center; gap: 7px; }
.archive__y {
  flex: none;
  padding-left: var(--space-1);
  font-family: var(--mono);
  font-size: 10.5px;
  font-weight: 500;
  letter-spacing: .09em;
  color: var(--ink-40);
}
.archive__row {
  flex: none;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 6px 13px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--background);
  font-size: 13.5px;
  font-weight: 450;
  color: var(--ink-55);
  white-space: nowrap;
  text-align: left;
  transition: color .14s ease, background-color .14s var(--ease-out-quart);
}
html.dark .archive__row { background: var(--surface); }
.archive__row:hover { color: var(--foreground); background: var(--surface-hover); }
.archive__n { font-family: var(--mono); font-size: 10.5px; color: var(--ink-55); }
/* the live month is named by weight and by the count going red — the
   same red the wordmark keeps, and the only colour on the page */
.archive__row.is-active { color: var(--foreground); font-weight: 600; }
.archive__row.is-active .archive__n { color: var(--heart); }

/* The archive is the pill row, at every width — a horizontal scroll under the masthead, which on a
   phone is the only way back to 2019. It is chrome there, not a
   sibling column, so it takes no card of its own. */
@media (max-width: 640px) {
  /* out to the gutter, so the row can be scrolled edge to edge */
  .archive { margin-inline: -var(--space-5); }
  .archive__in { padding-inline: var(--space-5); }
}
/* The sidebar's quiet rows sit a breath below the main four */
.side-gap { height: 18px; }
/* Home: the summary column — posts read at feed width, the shelf
   is a short row of covers */
.home-sec { margin-top: clamp(30px, 4vw, 48px); }
.link-row--more { margin-top: var(--space-4); }
.photo-stats {
  padding: 20px 0 8px;
  margin-top: 40px;
  border-top: 1px solid var(--border);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--ink-55);
}

/* THE VIEWER — franzgruenewald.com's, in its light state: a plain
   white field with one bar across the top and nothing else on it.
   The collection's name and the frame's number sit at the left of
   that bar, the controls at the right, all of it bare text and a
   bare glyph — no pills, no round buttons, no arrows down the sides.
   The picture itself is the next button, and the set loops.

   The margin is thin on purpose (48px over, 16px either side, the
   reference's own figures): the frame is meant to be as large as the
   screen allows, and the white round it is a mat, not a border. The
   caption and the download are ours and have no counterpart there —
   they stay because the frames here are meant to be read and taken,
   but they keep the same quiet register as everything else. */
.lightbox {
  margin: 0;
  padding: 0;
  border: 0;
  /* 100vw counts the scrollbar, so the mat used to hang a dozen-odd
     pixels off the right of the client area and take that much of the
     picture with it. Percentages resolve against the viewport proper
     for a fixed box, which is what the reference's inset-0 does. */
  width: 100%;
  max-width: none;
  height: 100vh;
  height: 100dvh;
  max-height: none;
  /* The mat follows the theme. It used to be #fff in both, on the
     argument that opening a frame is stepping up to a print on a wall
     — but a white flash is the wrong answer at night, and every viewer
     people actually use (Photos, Lightroom, Lightbox) goes dark with
     the room. Light keeps the reference's white wall; dark drops to
     the page's own ground so the frame is the only lit thing.
     Both values live in --lb-mat, set once per theme. */
  background: var(--lb-mat);
  /* the reference's own inset — enough over the top to clear the bar,
     barely any at the sides, so the picture gets the screen */
  padding: 48px 16px;
  box-sizing: border-box;
  cursor: zoom-out;
  overflow: hidden;
}
/* The two mat values are repeated here rather than read from --lb-mat.
   ::backdrop only began inheriting from its originating element in
   Chrome 122 / Safari 17.4; on anything older var(--lb-mat) is invalid
   at computed-value time and the backdrop falls back to transparent,
   which flashes the page through during the open. Keep these two in
   step with --lb-mat above. */
.lightbox::backdrop { background: #fff; }
html.dark .lightbox::backdrop { background: #111110; }
.lightbox figure {
  margin: 0;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* the photograph — or the clip — takes the whole height and keeps
   its own shape */
.lb-video {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  background: none;
}
.lb-video[hidden] { display: none; }
/* The picture takes the whole padded box and is contained inside it,
   so the white still runs all the way round whatever shape the frame
   is — but the ELEMENT reaches the edges, which is what lets a click
   anywhere on the mat move to the next frame rather than only a click
   that lands on the picture itself. */
.lightbox img {
  display: block;
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: 0;
  cursor: pointer;
}
.lightbox:not(.has-set) img { cursor: default; }
/* THE BAR. One row across the top, transparent, no rule under it:
   the name at the left with the count beside it on the same baseline,
   the things you can do at the right. The reference carries the
   project's name and a zero-padded count here and nothing else. */
/* THE BAR HAS TO FIT INSIDE THE MAT'S TOP INSET — 48px — or it sits
   on the picture. It is 12 + 20 + 12 = 44px: the padding either side
   of the tallest thing in it, which is the close glyph's 20px box.
   The bar's own line-height is set here because the page's inherited
   one is nearly 2, which alone made the row 62px and put 14px of
   chrome over the top of every frame. Change any of the three and
   check the sum still clears 48. */
.lb-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 3;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 12px 20px;
  font-size: var(--type-xs);
  line-height: 1.25;
  cursor: default;
}
.lb-bar__side {
  display: flex;
  align-items: baseline;
  gap: var(--space-4);
  min-width: 0;
}
.lb-bar__side--end { align-items: center; gap: 18px; flex: none; }
.lb-title {
  color: var(--ink-80);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* zero-padded and tabular, so the number doesn't shuffle its
   neighbours as it counts up */
.lb-count {
  flex: none;
  font-size: var(--type-2xs);
  color: var(--ink-55);
  font-variant-numeric: tabular-nums;
}
.lightbox:not(.has-set) .lb-count { display: none; }
.lb-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* 44px of hit area, pulled back with negative margin so the bar's
     height is unchanged — this is the only way out of a full-screen
     dialog whose entire picture surface advances instead of closing. */
  width: 44px;
  height: 44px;
  margin: -12px -12px -12px 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink-80);
  cursor: pointer;
  transition: opacity .15s ease;
}
.lb-close svg { width: 14px; height: 14px; display: block; }
@media (hover: hover) and (pointer: fine) {
  .lb-close:hover { opacity: .55; }
}
@media (max-width: 620px) {
  .lb-bar { padding: 10px 14px; font-size: var(--type-2xs); }
  .lb-bar__side { gap: var(--space-3); }
  .lb-bar__side--end { gap: 12px; }
}
/* Nothing is set under the picture. There was a `.lightbox-cap` here
   carrying the frame's words; the viewer holds the photograph and the
   bar and nothing else now, and the figcaption came out of the three
   pages that had it. */
/* the download sits in the bar now; nothing carries a filled pill in
   this viewer, so it reads as the plain word it is */
.lightbox-dl {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 0;
  background: none;
  color: var(--ink-80);
  font-size: var(--type-2xs);
  font-weight: 500;
  cursor: pointer;
  white-space: nowrap;
  transition: opacity .15s ease;
}
.lightbox-dl:hover { background: none; opacity: .55; }
.lightbox-dl small { font-weight: 450; color: var(--ink-55); font-size: var(--type-2xs); }
/* on a phone the terms would crowd the close — the rule is stated on
   the pages themselves too */
@media (max-width: 620px) {
  .lightbox-dl small { display: none; }
}

/* The house rule, stated wherever photos can be downloaded */
.dl-note {
  margin-top: 14px;
  font-size: var(--type-xs);
  line-height: 1.6;
  color: var(--ink-40);
  max-width: 560px;
}

/* ============================================================
   PINS — the pinboard rows (the bookstore-row register)
   ============================================================ */
.pins { padding-top: 8px; margin-top: var(--space-5); }
.pin { padding: 18px 0; border-top: 1px solid var(--border); }
.pin-title {
  font-family: var(--serif);
  font-size: var(--type-md);
  font-weight: 500;
  letter-spacing: -.005em;
  color: var(--ink-85);
  transition: color .15s ease;
}
.pin-title:hover { color: var(--accent); }
.pin-title .arrow {
  font-size: .7em;
  color: var(--ink-55);
  display: inline-block;
  transition: transform .2s var(--ease-out-quart), color .15s ease;
}
.pin-title:hover .arrow { transform: translate(2px, -2px); color: var(--accent); }
.pin-meta {
  margin-left: var(--space-3);
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-55);
  white-space: nowrap;
}
.pin-note { margin-top: var(--space-2); font-size: var(--type-sm); line-height: 1.5; color: var(--ink-65); max-width: 620px; }

/* ============================================================
   GAMES — now playing / finished / shelved
   ============================================================ */
.game-group { padding-top: 40px; }
.game { padding: var(--space-4) 0; border-top: 1px solid var(--border); }
.game-head { display: flex; align-items: baseline; gap: var(--space-3); flex-wrap: wrap; }
.game-title {
  font-family: var(--serif);
  font-size: var(--type-md);
  font-weight: 500;
  letter-spacing: -.005em;
  color: var(--ink-85);
}
.platpill {
  display: inline-flex;
  align-items: center;
  height: 20px;
  padding: 0 8px;
  border-radius: 999px;
  font-family: var(--mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: .04em;
  text-transform: uppercase;
  background: var(--surface);
  color: var(--ink-55);
  box-shadow: var(--shadow-flush);
}
.game-when {
  margin-left: auto;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-55);
  white-space: nowrap;
}
.game-note { margin-top: var(--space-2); font-size: var(--type-sm); line-height: 1.5; color: var(--ink-65); max-width: 620px; }
.game-group .rubric-label .live-pill { vertical-align: 1px; margin-left: 8px; }

/* ============================================================
   WRITING — pieces on one page
   ============================================================ */
.pieces { padding-top: 8px; }
.piece { padding: 40px 0 8px; }
.piece + .piece { border-top: 1px solid var(--border); }
.piece .kicker { margin-bottom: var(--space-2); }
.piece h2 {
  font-size: 24px;
  font-weight: 500;
  letter-spacing: -.01em;
  color: var(--ink-85);
  margin-bottom: 8px;
}
.piece-deck { font-size: var(--type-sm); line-height: 1.5; color: var(--ink-55); margin-bottom: 14px; max-width: 620px; }
.piece-body {
  font-size: var(--type-sm);
  font-weight: 450;
  line-height: 1.45rem;
  letter-spacing: -.005em;
  color: var(--ink-80);
  max-width: 620px;
  margin-bottom: 14px;
}

/* ============================================================
   THE DARKROOM — park.photos' register for the photography
   wing (home, galleries, archive): near-black ground and a
   white ink scale. html.dark flips the token scale for a page.

   Two things this comment used to claim are gone. The
   film-grain wash was deleted 2026-08-16 (see below). The
   display face is not Instrument Serif — that was one of the
   nine faces dropped in the type rebuild; titles are Recoleta
   and everything else is Geist. See the type table in
   CLAUDE.md.
   ============================================================ */
html.dark { background: #0A0A0A; scrollbar-color: rgba(255, 255, 255, .28) transparent; }
html.dark::-webkit-scrollbar-thumb { background-color: rgba(255, 255, 255, .22); }
html.dark::-webkit-scrollbar-thumb:hover { background-color: rgba(255, 255, 255, .34); }
html.dark body {
  --background: #0A0A0A;
  --foreground: #F5F5F3;
  /* The frame should be the only lit thing in the room. Slightly above
     --background so a photograph with true blacks still has an edge
     rather than bleeding into the mat. */
  --lb-mat: #111110;
  --surface: #161615;
  --surface-hover: #1F1F1D;
  /* iOS lifts the incoming bubble off the black rather than sinking it,
     so this goes lighter than --surface, not darker */
  --bubble: #2A2A2C;
  --border: rgba(255, 255, 255, .1);
  /* the light accent is #4c74ff, which goes muddy on near-black and
     reads as off-palette; lift it rather than leaving links dim */
  --accent: #8AA4FF;
  /* .18 alpha over #0A0A0A is close enough to nothing that selecting
     text looks like it failed */
  --selection: rgba(138, 164, 255, .32);
  --ink-85: rgba(255, 255, 255, .92);
  --ink-80: rgba(255, 255, 255, .84);
  --ink-78: rgba(255, 255, 255, .8);
  --ink-65: rgba(255, 255, 255, .66);
  --ink-55: rgba(255, 255, 255, .56);
  --ink-50: rgba(255, 255, 255, .5);
  --ink-45: rgba(255, 255, 255, .46);
  --ink-40: rgba(255, 255, 255, .4);
  --ink-30: rgba(255, 255, 255, .3);
  --shadow-flush: inset 0 0 0 1px rgba(255, 255, 255, .08);
  --shadow-raised: 0 1px 2px rgba(0, 0, 0, .5), inset 0 0 0 1px rgba(255, 255, 255, .09);
  --shadow-floating: 0 8px 32px rgba(0, 0, 0, .55), inset 0 0 0 1px rgba(255, 255, 255, .12);
}
/* THERE IS NO FILM GRAIN. `html.dark body::after` held one — an SVG
   feTurbulence wash at opacity .16 on mix-blend-mode: overlay, fixed
   over the whole viewport at z-index 9999, borrowed from park.photos
   along with the rest of this register. It was deleted on 2026-08-16
   at Cody's word, in two steps: first the projector-static animation
   that jumped it to a new offset every .6s, then the wash itself.

   Nothing replaces it. The dark ground is #0A0A0A and the photographs
   sit on it unmediated, which is the point — a texture laid over
   every frame is a texture the photographer did not put there.

   Don't reinstate it from the park.photos reference: that site is
   still the source for the near-black ground and the ink scale, and
   the grain was the one part of the borrow that did not survive
   contact with a wall of actual photographs. This was the only
   body::after rule in the file. */
/* Literal-color components that the tokens don't reach */
/* the bar takes the page's own ground in either theme now that it no
   longer floats over anything — the dark translucency went with the
   sticky positioning that needed it */
html.dark .menu-item__tile { background: #1F1F1D; }
html.dark .rubric::after { background: rgba(255, 255, 255, .12); }
html.dark .link-row a {
  background: rgba(255, 255, 255, .07);
  color: var(--ink-65);
}
html.dark .link-row a:hover { color: var(--ink-85); }
html.dark .deck a, html.dark .colophon-note a, html.dark .dash-note a {
  text-decoration-color: rgba(255, 255, 255, .3);
}
/* The library pages auto-darken after sunset, so their one-off light
   colors need dark counterparts or they vanish into the ground. */
html.dark .live-pill {
  background: rgba(48, 164, 108, .16);
  box-shadow: inset 0 0 0 1px rgba(48, 164, 108, .34);
  color: #5BD99A;
}
html.dark .gcard::before { box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .1); }
html.dark .w-pop { box-shadow: 0 4px 24px rgba(0, 0, 0, .6); }
/* Gallery-page titles take the display face on dark pages */
/* (dark page titles take the same Gooper italic as light — the
   old override that changed face and size here has been removed) */

/* --- The dark hero: the lead photograph edge to edge, a full
   viewport tall, the site voice bottom-anchored over a scrim that
   keeps the type readable without burying the animal. The nav
   floats over it (body.home unsticks it from the flow). --- */
/* Studio pages: the vertical nav carries everything on desktop;
   the top bar + burger only exist on small screens */
@media (min-width: 900px) {
  body.studio-page .nav { display: none; }
}
/* Hero photo meta, park's corners: the year bottom-left, where it
   was taken bottom-right with the state flag */
.hero-year {
  position: absolute;
  bottom: 26px;
  left: clamp(20px, 3vw, 40px);
  z-index: 2;
  font-size: var(--type-sm);
  font-weight: 550;
  color: rgba(255, 255, 255, .92);
  text-shadow: 0 1px 12px rgba(0, 0, 0, .5);
}

/* ============================================================
   MOTION HELPERS — CSS-only. The page head rises once on
   arrival; the style engine finishes it on wall-clock time, so
   nothing can strand content invisible if a script never runs.
   ============================================================ */
@keyframes head-rise {
  from { opacity: 0; transform: translateY(10px); filter: blur(5px); }
  to   { opacity: 1; transform: none; filter: blur(0); }
}
.page-head > *:not(.hero-main),
.page-head .hero-main > * {
  animation: head-rise .5s var(--ease-out-quart) backwards;
}
.page-head > *:nth-child(2), .page-head .hero-main > *:nth-child(2) { animation-delay: .07s; }
.page-head > *:nth-child(3), .page-head .hero-main > *:nth-child(3) { animation-delay: .14s; }
.page-head > *:nth-child(4), .page-head .hero-main > *:nth-child(4) { animation-delay: .21s; }
.page-head > *:nth-child(n+5), .page-head .hero-main > *:nth-child(n+5) { animation-delay: .28s; }
[hidden] { display: none !important; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .wordmark:hover .wordmark__dot { animation: none; }
  html.theme-anim body,
  html.theme-anim .nav,
  html.theme-anim .footer,
  html.theme-anim .mobile-menu__inner { transition: none; }
}
