/* Beneyam Hassen — one stylesheet, hand written, no framework.
   Rules that are not negotiable:
     · images are contained, never cropped to fill
     · images are large
     · nothing competes with a photograph on screen          */

/* ── EB Garamond, self-hosted ─────────────────────────────────────────────
   Downloaded from the google/fonts GitHub repository (SIL Open Font
   License — free to self-host) and built locally into these three static
   woff2 files: the upstream family is a variable font, and Regular 400 and
   Medium 500 were instanced out of EBGaramond[wght].ttf, Italic 400 out of
   EBGaramond-Italic[wght].ttf, then each subset to Latin + the punctuation
   the site actually uses (fonttools varLib.instancer + fonttools subset).
   No CDN request: these three files are the only source, served from this
   same origin. The filenames below are placeholders — main() rewrites them
   to the fingerprinted names when it fingerprints this file itself, the
   same way it rewrites nothing else in this CSS. Don't hand-edit a hashed
   filename here; edit the plain one and rebuild.
   font-display: swap so the fallback stack renders immediately and text is
   never invisible while these load. */
@font-face {
  font-family: "EB Garamond";
  src: url("/EBGaramond-Regular.4ddcf7b6b9.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "EB Garamond";
  src: url("/EBGaramond-Medium.19db7f68dc.woff2") format("woff2");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "EB Garamond";
  src: url("/EBGaramond-Italic.30944633b7.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
}

:root {
  /* ── Beneyam's palette ───────────────────────────────────────────────────
     Brown is the main colour, and the way to make a colour "main" without it
     shouting is to make it the INK — it is then on every page, in every
     sentence, and never announces itself. #2B1700 measures 14.8:1 on the cream,
     so it carries body text comfortably.

     Rust is the single accent: links, plate numbers, the cursor. 6.9:1, and it
     separates cleanly from the brown so a link never looks like an accident.

     Teal is deliberately unused. It measures 8.7:1 — a fine colour — but rust
     and teal are within 1.26:1 of each other, meaning they read as the same
     weight. Two accents of equal weight look like a flag, not a decision.
     Held in reserve; see the note at the bottom of this block.               */

  --paper:     #F7F4EC;   /* warm white. the old #F9ECE5 had red 13 points above
                             green, which is what read as pink. here they are 3
                             apart and the warmth comes from dropping blue, so it
                             leans yellow — paper, not blush. */
  --ink:       #160F07;   /* your brown, darker — 17.3:1 */
  --ink-soft:  #625D55;   /* captions — 5.9:1 */
  --ink-faint: #767168;   /* labels, dates — 4.6:1. the natural tint landed at
                             3.5:1 and failed, so this is darkened on purpose. */
  --rule:      #DEDBD3;
  --accent:    #9C2113;   /* rust — 7.2:1 */
  --accent-bright: #B8402F;
  --lightbox:  #0E0A06;
  color-scheme: light;

  /* ── one column for the whole site ───────────────────────────────────────
     --col is the page column. Every structural block on every page — heading,
     dateline, list, plate list, further reading, onward, quote — begins and
     ends on these two lines. Before this there were three different widths in
     play, so a heading started in one place and the list beneath it started in
     another, and nothing on the page agreed with anything else.

     --measure is narrower, and is used ONLY to stop lines of body text getting
     too long to read. It constrains the paragraph, not the block, so prose
     still starts on the column's left edge — a shared edge, a shorter line.  */
  --col:     46em;
  --measure: 34em;
  --sheet-w: 1620px;   /* the contact sheet is wider; heading and grid share it */
  --serif: "EB Garamond", "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  --sans:  ui-sans-serif, -apple-system, "Helvetica Neue", Arial, sans-serif;

  /* ── leading, for anything that isn't a paragraph ────────────────────────
     body's line-height: 1.62 is prose leading — right for paragraphs, wrong
     for a one-line uppercase label or a short caption, which just inherited
     it anyway because nothing said otherwise. Unitless on purpose: inherited
     as the number, not a fixed size, so a descendant with its own font-size
     (a link inside .masthead, say) still gets leading proportioned to ITS
     OWN size rather than the ancestor's. */
  --lh-label: 1.3;    /* uppercase letter-spaced labels, small metadata, single-line UI text */
  --lh-tight: 1.45;   /* short prose that isn't a paragraph — a caption, a note, a status line */
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration-color: var(--accent); text-underline-offset: 3px; }
a:hover { color: var(--accent); }

/* masthead — quiet. the work is the site, not the nav */
.masthead {
  display: flex; justify-content: space-between; align-items: baseline; gap: 2rem;
  padding: 1.6rem 6vw;
  font-family: var(--sans); font-size: .74rem; line-height: var(--lh-label);
  letter-spacing: .11em; text-transform: uppercase;
}
.masthead a { text-decoration: none; }
.masthead nav { display: flex; gap: 1.5rem; color: var(--ink-soft); }
.masthead nav a.on { color: var(--accent); }
.masthead .name { font-weight: 600; letter-spacing: .14em; }

/* opening frame — arrives before the title */
.opening { padding: 2vh 6vw 0; display: flex; justify-content: center; }
.opening img { display: block; max-height: 84vh; max-width: 100%; width: auto; height: auto; cursor: zoom-in; }

/* The Work page opens larger than any other page — near enough the full height
   of the screen, so the first thing a stranger meets is a photograph and not a
   layout. It has to be scrolled past, which is the point. */
.home-opening img { max-height: 92vh; }

/* ── the rotating opening ────────────────────────────────────────────────────
   Every frame is in the page; only opacity changes. The rotation gets one box,
   and every frame fits inside it.

   The first version left frame one in normal flow and stacked the rest on top
   of it, which meant the box was whatever shape the first photograph happened
   to be — a landscape lead made a short box that squashed every portrait after
   it, and on a phone, where the opening runs full-bleed with no height cap, the
   box was whatever that one file dictated. Now the height is the page's
   decision, not the first photograph's, and `contain` means nothing is ever
   cropped to fill it. */
.rotator {
  position: relative;
  height: 92vh; width: 100%;
}
.rotator .frame {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  transition: opacity 1.1s ease;
}
.rotator .frame:not(.on) { opacity: 0; pointer-events: none; }
.rotator .frame picture { display: block; width: 100%; height: 100%; }
.rotator img {
  width: 100%; height: 100%;
  max-width: 100%; max-height: 100%;
  object-fit: contain; object-position: center;
  cursor: pointer;                       /* click advances, it does not zoom */
}

/* A full-bleed invisible button over the photograph, so a click anywhere moves
   on and a keyboard can reach it. It carries no visual weight at all. */
.rot-next {
  position: absolute; inset: 0; width: 100%; height: 100%;
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin: 0;
  cursor: pointer; z-index: 2;
}
.rot-next:focus-visible { outline: 2px solid var(--accent); outline-offset: -4px; }

/* The caption sits under the box, never over a photograph. Quiet enough that
   the picture is still the thing you look at — it says where you are, not what
   to think. Empty captions collapse to nothing rather than leaving a gap. */
.rot-cap {
  /* Hard right, on the same 6vw margin the photograph uses, so it sits under
     the frame's right edge rather than floating in the middle of the page.
     Full width rather than the reading column — the rotation is wider than the
     text, and a caption centred to the text would not line up with anything. */
  max-width: none; margin: 1rem 0 0; padding: 0 6vw;
  text-align: right;
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--ink-faint);
  min-height: 1.1em;
  transition: opacity .26s ease;
}
@media (max-width: 700px) {
  /* the opening runs full-bleed on a phone, so the caption comes back in */
  .rot-cap { padding: 0 4vw; }
}
.rot-cap:empty { min-height: 0; margin-top: 0; }
.rot-cap.fading { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .rot-cap { transition: none; } }

/* Motion off: no rotation, no fade. The first photograph stays and the button
   still works, so the other frames remain reachable by choice. */
@media (prefers-reduced-motion: reduce) {
  .rotator .frame { transition: none; }
}

/* title, after the photograph */
.title-block { max-width: var(--col); margin: 5.5rem auto 0; padding: 0 6vw; }
.title-block h1 {
  font-size: clamp(1.9rem, 4.4vw, 2.9rem); line-height: 1.16;
  font-weight: 500; margin: 0 0 .6rem; letter-spacing: -.012em;
}
.dateline {
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .13em; text-transform: uppercase; color: var(--ink-faint);
}

/* prose — chunks, never walls */
.prose { max-width: var(--col); margin: 3.2rem auto; padding: 0 6vw; }
.prose p { margin: 0 0 1.35em; max-width: var(--measure); }
.prose p:last-child { margin-bottom: 0; }
.lede { font-size: 1.14em; line-height: 1.55; }

.section-mark {
  max-width: var(--col); margin: 6.5rem auto 2.4rem; padding: 0 6vw;
  font-family: var(--sans); font-size: 1.15rem; line-height: var(--lh-label);
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink);
  display: flex; align-items: center; gap: 1.1rem;
}
.section-mark::after { content: ""; flex: 1; height: 1px; background: var(--rule); }

/* plates — variable scale is the rhythm */
figure { margin: 0 auto; padding: 0 6vw; display: flex; flex-direction: column; align-items: flex-start; }
figure + figure { margin-top: 5.5rem; }
figure img { display: block; max-width: 100%; width: auto; height: auto; align-self: center; cursor: zoom-in; }

.plate-full   img { max-height: 88vh; }
.plate-large  img { max-height: 72vh; }
.plate-medium img { max-height: 52vh; }
.plate-small  img { max-height: 34vh; }

/* film — for the performance work, where the video is the piece and the
   photographs are documentation of it. sits where a plate would sit. */
figure.film { margin-top: 0; }
figure.film video {
  display: block; width: 100%; max-width: 1400px; max-height: 84vh;
  align-self: center; background: #0d0d0f;
}

figcaption {
  margin-top: .85rem; font-family: var(--sans); font-size: .76rem;
  line-height: 1.5; color: var(--ink-faint); max-width: 30em;
}
figcaption .n { color: var(--accent); margin-right: .5em; font-variant-numeric: tabular-nums; }

/* a footnote, not a caption of its own — same size as the text above it,
   just set apart by italics and a quieter colour, so "written about this"
   reads as an aside rather than competing with the caption for attention */
figcaption .plate-note { display: block; margin-top: .3rem; font-style: italic; }
figcaption .plate-note a { color: inherit; }
figcaption .plate-note a:hover { color: var(--accent); }

/* home index — a list that leads, not a grid that offers */
.home-index { max-width: var(--col); margin: 6rem auto 0; padding: 0 6vw; }
.home-index .entry {
  display: flex; justify-content: space-between; align-items: baseline; gap: 2rem;
  padding: 1.15rem 0; border-bottom: 1px solid var(--rule); text-decoration: none;
}
.home-index .entry:first-child { border-top: 1px solid var(--rule); }
/* line-height: 1.25, not the inherited body default of 1.62 — that's prose
   leading, sized for paragraphs, not a heading-sized title that is usually
   one line and sometimes two. The unused half-leading it left below the
   title was bigger than .sub's own margin-top, so the sub-line (the medium
   here, an archive note on .home-index rows generally — see build line
   ~1141) read as belonging to the row below rather than its own title.
   Retune any spacing near .sub against THIS line-height, not the old one. */
.home-index .t { flex: 1; min-width: 0; font-size: 1.15rem; line-height: 1.25; }
.home-index .d {
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint);
  white-space: nowrap;
}
.home-index .entry:hover .t { color: var(--accent); }
.home-index .entry.static { cursor: default; }
.home-index .sub {
  display: block; font-family: var(--sans); font-size: .74rem;
  color: var(--ink-faint); margin-top: .1rem; line-height: 1.45;
}

/* plate list — full information, out of the pictures' way */
.plate-list {
  max-width: var(--col); margin: 8rem auto 0; padding: 3rem 6vw 0;
  border-top: 1px solid var(--rule); font-family: var(--sans);
}
.plate-list h2 {
  font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint); font-weight: 500; margin: 0 0 2rem;
}
.plate-list ol { list-style: none; margin: 0; padding: 0; }
.plate-list li {
  display: grid; grid-template-columns: 2.6rem 1fr; gap: 1rem;
  padding: .85rem 0; border-bottom: 1px solid var(--rule);
  font-size: .82rem; line-height: 1.55; color: var(--ink-soft);
}
.plate-list .num { color: var(--accent); font-variant-numeric: tabular-nums; }
.plate-list .who { color: var(--ink); }
.plate-list .meta { color: var(--ink-faint); }
.note {
  margin-top: 2rem; font-size: .78rem; line-height: var(--lh-tight);
  color: var(--ink-faint); font-style: italic;
}

/* clicking the email copies it too, and says so once, quietly.
   Small enough that it reads as a receipt, not an announcement. */
.copied {
  margin-left: .55rem;
  font-family: var(--sans); font-size: .66rem; line-height: var(--lh-label);
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink-faint);
  opacity: 0; transition: opacity .3s ease;
}
.copied.show { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .copied { transition: none; } }

/* ── the letter ──────────────────────────────────────────────────────────────
   One button, and the address written out underneath it. The button is a link,
   not a form control, because there is no form — nothing to submit, nothing to
   go wrong, nothing to maintain. Sized generously for a thumb. */
.join { margin-top: 2.6rem; }
.join-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .7rem 1rem;
}
.join-lbl {
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .13em; text-transform: uppercase; color: var(--ink-faint);
}
.join-why {
  font-family: var(--sans); font-size: .88rem; line-height: var(--lh-label);
  color: var(--ink); background: transparent;
  border: 0; border-bottom: 1px solid var(--ink-soft);
  padding: .45rem 1.6rem .45rem .1rem;
  border-radius: 0; cursor: pointer;
  appearance: none; -webkit-appearance: none;
  /* the chevron, drawn rather than imported, so it follows the theme */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 8px center, right 3px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}
.join-why:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.join-why option { color: var(--ink); background: var(--paper); }

.join-btn {
  display: inline-block;
  font-family: var(--sans); font-size: .78rem; line-height: var(--lh-label);
  letter-spacing: .14em; text-transform: uppercase;
  color: var(--paper); background: var(--ink);
  padding: .95rem 1.9rem; border-radius: 2px;
  text-decoration: none;
  transition: background .25s ease;
}
.join-btn:hover, .join-btn:focus-visible { background: var(--accent); }
.join-form { display: block; max-width: var(--measure); }
.join-form .join-lbl { display: block; margin: 1.5rem 0 .4rem; }
.join-form .join-row .join-lbl { margin: 0; }

.join-form input[type=email],
.join-form textarea {
  display: block; width: 100%;
  font-family: var(--serif); font-size: .95rem; line-height: 1.55;
  color: var(--ink); background: transparent;
  border: 1px solid var(--rule); border-radius: 2px;
  padding: .7rem .8rem;
}
.join-form textarea { resize: vertical; min-height: 7em; }
.join-form input[type=email]::placeholder { color: var(--ink-faint); }
.join-form input[type=email]:focus-visible,
.join-form textarea:focus-visible,
.join-why:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-color: transparent;
}
/* min-width so the button does not visibly resize as the label changes with
   the menu — "Send" and "Ask to be removed" are very different lengths. */
.join-form .join-btn {
  margin-top: 1.5rem; border: 0; cursor: pointer;
  font-family: var(--sans); min-width: 15em; text-align: center;
}
.join-form .join-btn[disabled] { opacity: .5; cursor: default; }

/* Off-screen rather than display:none — a bot that skips hidden fields still
   fills this one, and no person ever sees or tabs into it. */
.join-pot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.join-said {
  font-family: var(--sans); font-size: .82rem; line-height: var(--lh-tight);
  margin: 1rem 0 0; min-height: 1.2em;
}
.join-said.good { color: var(--ink); }
.join-said.bad  { color: var(--accent); }

.join-addr {
  font-family: var(--sans); font-size: .78rem; line-height: var(--lh-tight);
  color: var(--ink-faint); margin-top: 2rem;
}
.join-addr a { color: var(--ink-soft); }

/* phone: the menu and the button each take the full width, stacked */
@media (max-width: 560px) {
  .join-row { flex-direction: column; align-items: flex-start; gap: .5rem; }
  .join-why { width: 100%; }
  .join-btn { width: 100%; text-align: center; }
}

/* ── contact sheet ───────────────────────────────────────────────────────────
   The record layer, shown plainly. Every cell is the same size and every
   photograph sits inside its cell at its own proportions — nothing is cropped
   to make a tidy grid, because cropping to fit would be an edit, and an edit is
   exactly what this layer is not. No captions, no varying scale, no argument.
   A project persuades; a contact sheet only testifies.                       */
/* One column for the whole page. The heading, the dateline, the note and the
   grid all begin and end on the same two lines — previously the text sat in the
   narrow reading measure while the grid ran wider, so the page had two competing
   left edges and nothing lined up with anything. */
/* The sheet runs wider than the rest of the site and sits closer to the edges
   of the screen — 3vw of margin rather than 6 — because a contact sheet is the
   one page where quantity is the point and the photographs should be large
   enough to actually read. The heading follows it, so both edges still line up. */
.sheet-page .title-block { max-width: var(--sheet-w); padding: 0 3vw; }
.sheet {
  display: grid;
  /* A fixed number of columns, not auto-fill. Auto-fill picks a count from the
     available width, so the same sheet is four across on one laptop and five on
     another and the rows never feel deliberate. Fixed counts make every row the
     same, which is the whole point of a contact sheet.
     Never more than four: past that the frames get too small to be looked at,
     which defeats the page. */
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px;
  max-width: var(--sheet-w);
  margin: 3.5rem auto 0;
  padding: 0 3vw;
}
@media (max-width: 1400px) { .sheet { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Square cells, filled. The first version held each photograph inside its cell
   at its own proportions — honest, but a portrait frame then forced its row
   taller than the rest and the grid came apart. A contact sheet's whole job is
   to be even, so the cells crop. Nothing is lost: the full frame, uncropped, is
   one click away in the lightbox. */
.sheet .cell {
  object-position: center;      /* default; order.txt can override per photo */
  aspect-ratio: 1 / 1;
  min-width: 0; min-height: 0;
  overflow: hidden;
  background: var(--rule);
}
.sheet .cell picture { display: block; width: 100%; height: 100%; }
.sheet .cell img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* inherited so a per-photograph focus set on the cell reaches the image */
  object-position: inherit;
  display: block; cursor: zoom-in;
  transition: opacity .25s ease;
}
.sheet .cell img:hover { opacity: .84; }

.sheet-note {
  font-size: .84rem; line-height: var(--lh-tight); color: var(--ink-soft);
  max-width: 34em; margin: .7rem 0 0;
}

@media (max-width: 760px) {
  .sheet {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px; margin-top: 2.2rem; padding: 0 3vw;
  }
  .sheet-page .title-block { padding: 0 3vw; }
}

/* ── archive rows that lead somewhere ────────────────────────────────────────
   Most of the archive is text, and stays text. An entry that has been worked
   into a sequence or a sheet becomes a link and shows one frame on hover.

   Two rules make it behave. It is a fixed square and the photograph is cropped
   to fill it, so a portrait and a panorama read as the same object rather than
   as two different-shaped scraps. And at 46px it is shorter than the shortest
   row (padding 1.15rem twice plus a 1.15rem line is about 65px), so centred
   vertically it cannot reach the rule above or below.

   Note: no `overflow: hidden` on the row. The accent underline that draws
   itself on hover sits at bottom:-1px, outside the box, and clipping the row
   would erase it.                                                           */
.home-index .entry.lead {
  position: relative;
  --peek-size: 46px;
}

/* The whole row is clickable, but only the title is a link. `.stretch::after`
   spreads that one link across the row, which keeps the markup legal — a note
   link can then sit inside the row without being an anchor inside an anchor.
   The note link is lifted above the overlay so it stays separately clickable. */
.home-index .entry.lead .stretch { text-decoration: none; color: inherit; }
.home-index .entry.lead .stretch::after { content: ""; position: absolute; inset: 0; }
.home-index .entry.lead .notes-on a { position: relative; z-index: 1; }
.home-index .entry.lead:hover .t,
.home-index .entry.lead:focus-within .t { color: var(--accent); }

/* A normal flex item, not an absolutely positioned one laid over .d — that
   was the actual bug: .peek sat on top of the year, not the title, since
   right: var(--peek-inset) placed it over whatever flush-right text already
   occupied that band. As a flex item its space is reserved by the row's own
   layout instead of a second, easily-mismatched reservation, and .entry's
   existing gap: 2rem spaces it from .t and .d on both sides for free.
   align-self: center overrides the row's align-items: baseline, which would
   otherwise line up a 46px square with surrounding text on its baseline. */
.home-index .entry.lead .peek {
  flex: 0 0 var(--peek-size); align-self: center;
  height: var(--peek-size);
  display: block; overflow: hidden;
  background: var(--rule);
  opacity: 0; transition: opacity .24s ease;
  pointer-events: none;
}
.home-index .entry.lead .peek img {
  width: 100%; height: 100%;
  object-fit: cover;
  /* inherited so cover-focus: in order.txt can set it on the .peek span
     itself — center, its initial value, when nothing overrides it */
  object-position: inherit;
  display: block;
}
/* Hover only, deliberately. This also matched :focus-visible, which looked
   right until you clicked a row and pressed back — the browser restores focus
   to the link you left from, so that one thumbnail stayed open for good while
   every other row behaved. Keyboard users still get the accent title and the
   underline, which is signal enough. */
.home-index .entry.lead:hover .peek { opacity: 1; }

/* A phone has no hover and no room, so the thumbnail simply does not exist.
   Nothing else to undo here now — a flex item that's display: none takes no
   space and needs no separate reservation zeroed out to match. */
@media (max-width: 760px), (hover: none) {
  .home-index .entry.lead .peek { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .home-index .entry.lead .peek { transition: none; }
}

.further {
  max-width: var(--col); margin: 3.5rem auto 0; padding: 0 6vw;
  font-family: var(--sans); font-size: .82rem; line-height: var(--lh-tight);
  color: var(--ink-soft);
}

footer {
  margin-top: 8rem; padding: 2.5rem 6vw 4rem; border-top: 1px solid var(--rule);
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint);
  display: flex; justify-content: space-between; gap: 1.5rem; flex-wrap: wrap;
}
footer a { text-decoration: none; }

/* lightbox — still contained, still never cropped */
#lightbox {
  position: fixed; inset: 0; background: #0d0d0f; display: none;
  align-items: center; justify-content: center; padding: 2vh 2vw;
  cursor: zoom-out; z-index: 100;
}
#lightbox.open { display: flex; }
#lightbox img { max-width: 100%; max-height: 96vh; width: auto; height: auto; object-fit: contain; }

@media (max-width: 700px) {
  body { font-size: 19px; }
  .masthead { padding: 1.1rem 6vw; font-size: .68rem; }
  .masthead nav { gap: 1rem; }
  figure + figure { margin-top: 3.5rem; }
  .plate-full img, .plate-large img { max-height: 70vh; }
  .home-index .entry { flex-direction: column; gap: .3rem; }
}

/* desktop-only accent cursor. absent on the phone, which is most visitors */
@media (hover: hover) and (pointer: fine) {
  #dot {
    position: fixed; top: 0; left: 0; width: 26px; height: 26px; margin: -13px 0 0 -13px;
    border-radius: 50%; background: var(--accent);
    opacity: 0;                        /* invisible until it knows where you are */
    pointer-events: none; z-index: 200;
    transition: opacity .22s ease;
  }
  #dot.ready { opacity: .34; }
  body.zooming #dot.ready { opacity: .5; }
  #dot {
  }
  body.zooming #dot { opacity: .5; width: 46px; height: 46px; margin: -23px 0 0 -23px;
                      transition: opacity .16s ease, width .22s ease, height .22s ease, margin .22s ease; }
}
@media not all and (hover: hover) { #dot { display: none; } }



/* pair — two frames at equal height, the comparison IS the argument */
figure.pair .two { display: flex; gap: 1.2rem; align-items: flex-start; justify-content: center; width: 100%; }
figure.pair .two img { max-width: calc(50% - .6rem); width: auto; }
figure.pair.plate-full  .two img { max-height: 76vh; }
figure.pair.plate-large .two img { max-height: 62vh; }
@media (max-width: 700px) {
  figure.pair .two { flex-direction: column; align-items: center; gap: .8rem; }
  figure.pair .two img { max-width: 100%; max-height: 52vh; }
}

/* the reveal — where a project withholds its year until the end */
.reveal {
  margin-top: 2rem; font-size: 1rem; line-height: var(--lh-tight); color: var(--ink);
  font-family: var(--serif); letter-spacing: 0;
}


/* ────────────────────────────────────────────────────────────────────────────
   MOTION

   One rule governs all of it: things move on ARRIVAL and DEPARTURE, never
   while you are looking. A photograph that is on screen is completely still.
   Nothing animates in a viewer's peripheral vision while they are reading a
   picture — that is the difference between motion and fidgeting.
   ──────────────────────────────────────────────────────────────────────── */

/* page-to-page cross-fade. no framework, no router — the browser does it.
   silently ignored by browsers that don't support it. */
@view-transition { navigation: auto; }

::view-transition-old(root) { animation: fade-out .28s ease both; }
::view-transition-new(root) { animation: fade-in  .42s ease both; }
@keyframes fade-out { to   { opacity: 0; } }
@keyframes fade-in  { from { opacity: 0; } }

/* the opening frame arrives, once, on load */
.opening img {
  animation: opening-in 1.1s cubic-bezier(.16,.84,.44,1) both;
}
@keyframes opening-in {
  from { opacity: 0; transform: scale(1.012); }
  to   { opacity: 1; transform: none; }
}

/* plates rise into place as you reach them.
   default state is VISIBLE — js adds .will-reveal, so a failed script or a
   blocked file leaves every photograph on the page. */
.will-reveal { opacity: 0; transform: translateY(16px); }
.will-reveal.revealed {
  opacity: 1; transform: none;
  transition: opacity .85s cubic-bezier(.16,.84,.44,1),
              transform .85s cubic-bezier(.16,.84,.44,1);
}
/* the caption follows its picture a beat later */
.will-reveal.revealed figcaption { animation: fade-in .7s .22s ease both; }

/* section marks: the rule draws itself across */
.section-mark::after { transform-origin: left; }
.will-reveal.revealed.section-mark::after { animation: rule-draw 1s .1s cubic-bezier(.16,.84,.44,1) both; }
@keyframes rule-draw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

/* index rows — the accent arrives from the left rather than blinking on */
.home-index .entry { position: relative; transition: color .3s ease; }
.home-index .entry::after {
  content: ""; position: absolute; left: 0; bottom: -1px; height: 1px; width: 100%;
  background: var(--accent); transform: scaleX(0); transform-origin: left;
  transition: transform .45s cubic-bezier(.16,.84,.44,1);
}
.home-index .entry:hover::after { transform: scaleX(1); }
.home-index .t { transition: color .3s ease, transform .45s cubic-bezier(.16,.84,.44,1); }
.home-index .entry:hover .t { transform: translateX(5px); }
.home-index .entry.static::after { display: none; }
.home-index .entry.static:hover .t { transform: none; }

/* .entry.lead's .t wraps the stretched-link anchor (`.stretch::after`, sized
   `inset: 0` against `.entry.lead`'s `position: relative`). ANY transform on
   an ancestor of an absolutely positioned element makes that ancestor its
   containing block — so putting the hover transform above on .t collapsed
   the overlay's containing block from the whole row down to the title text,
   shrinking the clickable area to just the title at exactly the moment a
   reader is hovering to click it. Cancel it on .t for lead rows and put the
   same motion on .ttl instead, a plain span with nothing absolutely
   positioned inside it. Do not move this back to .t — it looks harmless and
   silently breaks the row again. */
.home-index .entry.lead:hover .t,
.home-index .entry.lead:focus-within .t { transform: none; }
.home-index .entry.lead .ttl {
  display: inline-block;
  transition: transform .45s cubic-bezier(.16,.84,.44,1);
}
.home-index .entry.lead:hover .ttl,
.home-index .entry.lead:focus-within .ttl { transform: translateX(5px); }

/* lightbox: fades up rather than snapping */
#lightbox { opacity: 0; transition: opacity .3s ease; }
#lightbox.open { opacity: 1; }
#lightbox img { transform: scale(.985); transition: transform .42s cubic-bezier(.16,.84,.44,1); }
#lightbox.open img { transform: none; }

.masthead a, footer a, .further a, .prose a { transition: color .25s ease; }

/* honour the system setting. everything above becomes instant, nothing hides. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .will-reveal { opacity: 1 !important; transform: none !important; }
  @view-transition { navigation: none; }
}


/* full bleed on narrow screens — the opening frame and any `full` plate run
   edge to edge on a phone. Side padding is politeness the picture cannot afford
   at that size. Still contained, still never cropped. */
@media (max-width: 700px) {
  .opening { padding: 1.5vh 0 0; }
  .opening img { max-width: 100vw; max-height: none; }
  /* Shorter on a phone: 92vh of photograph with the browser chrome on top
     means the visitor has to scroll twice before reaching a single word. */
  .rotator { height: 62vh; }
  .rotator img { max-height: 100%; }
  figure.plate-full { padding: 0; }
  figure.plate-full img { max-width: 100vw; max-height: none; }
  figure.plate-full figcaption { padding: 0 6vw; }
}


/* ────────────────────────────────────────────────────────────────────────────
   DARK

   Not an inversion. Photographs read differently on a dark surround — they
   appear brighter, contrastier, shadow detail opens up. So:

   · Never pure black. #121110 is near-black with the same warmth as the paper
     tone, because white-hot photographs against #000 halate at the edges.
   · The photographs themselves are NEVER filtered, dimmed or adjusted. The
     surround changes; the work does not.
   · Text is warm off-white, not #fff — pure white on near-black is harsh to
     read at length, and there is a lot to read here.
   · The accent lifts, because ochre that works on paper goes muddy on dark.
   ──────────────────────────────────────────────────────────────────────── */

:root[data-theme="dark"] {
  /* the dark page is your brown, lifted just enough that the grain has a
     surface to sit on rather than disappearing into a void */
  --paper:     #231C15;   /* the new brown, lifted so grain has a surface */
  --ink:       #F7F4EC;
  --ink-soft:  #AFABA3;
  --ink-faint: #89847C;
  --rule:      #413A33;
  --accent:    #C58075;   /* rust lifted — at full strength it is unreadable here */
  --accent-bright: #D6968B;
  --lightbox:  #000;
  color-scheme: dark;
}

#lightbox { background: var(--lightbox); }

/* the toggle — a disc that fills. small, quiet, last in the nav. */
#theme {
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; padding: 0; margin-left: .35rem;
  cursor: pointer; line-height: 0;
  display: inline-flex; align-items: center;
}
#theme .disc {
  display: block; width: 11px; height: 11px; border-radius: 50%;
  border: 1.5px solid var(--ink-soft);
  background: linear-gradient(to right, var(--ink-soft) 0 50%, transparent 50% 100%);
  transition: transform .45s cubic-bezier(.16,.84,.44,1),
              border-color .3s ease, background .3s ease;
}
#theme:hover .disc { border-color: var(--accent); background-image: linear-gradient(to right, var(--accent) 0 50%, transparent 50% 100%); }
:root[data-theme="dark"] #theme .disc { transform: rotate(180deg); }
#theme:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 50%; }

/* colour transition on switch — the surround fades, nothing jumps */
html.theme-shifting,
html.theme-shifting body,
html.theme-shifting .masthead,
html.theme-shifting footer,
html.theme-shifting .plate-list,
html.theme-shifting .section-mark::after,
html.theme-shifting .home-index .entry {
  transition: background-color .32s ease, color .32s ease,
              border-color .32s ease, background .32s ease;
}
@media (prefers-reduced-motion: reduce) {
  html.theme-shifting, html.theme-shifting * { transition: none !important; }
  #theme .disc { transition: none; }
}

/* the portrait on About — smaller than a plate. it is a picture of the person,
   not a piece of the work, and it should not open the page louder than they do. */
.opening.portrait img { max-height: 58vh; }
@media (max-width: 700px) { .opening.portrait { padding: 1.5vh 6vw 0; }
                            .opening.portrait img { max-height: 46vh; max-width: 100%; } }

/* exhibitions & press — a record, not a brag. year in the margin, like a CV. */
.listing { max-width: var(--col); margin: 0 auto 3rem; padding: 0 6vw; }
.listing .row {
  display: grid; grid-template-columns: 5.5rem 1fr; gap: 1.2rem;
  padding: 1rem 0; border-bottom: 1px solid var(--rule);
}
.listing .yr {
  font-family: var(--sans); font-size: .74rem; line-height: var(--lh-label);
  letter-spacing: .1em; color: var(--ink-faint); padding-top: .3rem;
  font-variant-numeric: tabular-nums;
}
.listing .ttl { display: block; font-size: 1.05rem; line-height: var(--lh-label); }
.listing .where {
  display: block; font-family: var(--sans); font-size: .78rem; line-height: var(--lh-tight);
  color: var(--ink-faint); margin-top: .25rem;
}
@media (max-width: 700px) {
  .listing .row { grid-template-columns: 1fr; gap: .3rem; }
  .listing .yr { padding-top: 0; }
}

/* <picture> wrapper must not break the plate layout. display:contents makes
   the <img> the plate's grid/flex item, not the <picture>. It also promotes
   the <source> siblings to items: on desktop the first one took row 1 of the
   photograph's column, pushing every captioned photograph down to row 2,
   below its caption. The browser reads <source> for srcset whether it is
   displayed or not, so it is taken out of layout here. */
figure picture, .opening picture { display: contents; }
figure picture > source, .opening picture > source { display: none; }

/* further reading — sits under the plate list, quiet, can hold several links */
.further .lbl {
  display: block; font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-faint);
  margin-bottom: .8rem;
}
.further ul { list-style: none; margin: 0; padding: 0; }
.further li { padding: .4rem 0; }

/* quoted material — indented and marked so a reader can tell it isn't the
   author's own voice. attribution is required, not decorative. */
blockquote.pull {
  max-width: var(--col); margin: 3.2rem auto; padding: 0 6vw;
  border: 0;
}
blockquote.pull p {
  margin: 0 0 .9em; font-size: 1.08em; line-height: 1.55;
  padding-left: 1.1rem; border-left: 2px solid var(--accent);
}
blockquote.pull cite {
  display: block; padding-left: 1.1rem;
  font-family: var(--sans); font-style: normal;
  font-size: .75rem; line-height: 1.5; color: var(--ink-faint);
}

/* onward — where to go when the work runs out. named, not a carousel. */
.onward {
  max-width: var(--col); margin: 6rem auto 0; padding: 3rem 6vw 0;
  border-top: 1px solid var(--rule);
  display: flex; justify-content: space-between; gap: 2rem;
}
.onward a { text-decoration: none; max-width: 20em; }
.onward .n { text-align: right; margin-left: auto; }
.onward .dir {
  display: block; font-family: var(--sans); font-size: .7rem; line-height: var(--lh-label);
  letter-spacing: .16em; text-transform: uppercase; color: var(--ink-faint);
  margin-bottom: .45rem;
}
.onward .t { font-size: 1.05rem; line-height: var(--lh-label); transition: color .3s ease; }
.onward a:hover .t { color: var(--accent); }
@media (max-width: 700px) {
  .onward { flex-direction: column; gap: 2rem; }
  .onward .n { text-align: left; margin-left: 0; }
}

/* the lightbox counts, quietly, so you know how far through you are */
#lightbox { flex-direction: column; }
#lb-count {
  position: fixed; bottom: 1.6rem; left: 0; right: 0; text-align: center;
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .14em; color: rgba(255,255,255,.45); font-variant-numeric: tabular-nums;
  pointer-events: none;
}

/* a photograph's place, when it has one — an archive contact-sheet caption
   never shows on the grid itself, only here. Hidden entirely, not an empty
   line, when the photograph carries none.

   In flow, not position:fixed, and deliberately so: a fixed caption sat on
   top of whatever the photograph happened to show at that height — snow,
   in one test case, swallowing white-on-dark text completely. Shrinking the
   image to make real room in the black margin below it, then letting the
   caption sit there normally, means it is never painted over the picture. */
#lightbox.has-caption img { max-height: 84vh; }
#lb-caption {
  display: none;
  margin-top: 1.1rem; text-align: center;
  font-family: var(--sans); font-size: .76rem; line-height: 1.5;
  color: rgba(255,255,255,.6); max-width: 30em; padding: 0 6vw;
}
#lb-caption.has-caption { display: block; }

/* ── getting back ────────────────────────────────────────────────────────────
   The masthead hides as you move forward and returns the moment you scroll up.
   Going deeper into the work, nothing sits over the photographs. The instant you
   reverse — the only time you actually want to leave — the way out is there. */
.masthead {
  position: sticky; top: 0; z-index: 50;
  background: var(--paper);
  transition: transform .3s cubic-bezier(.16,.84,.44,1), box-shadow .3s ease;
}
body.nav-hidden .masthead { transform: translateY(-100%); }
body.nav-floating .masthead { box-shadow: 0 1px 0 var(--rule); }

/* explicit way back at the top of anything you clicked into */
.back {
  display: flex; align-items: baseline; gap: .5rem;
  max-width: var(--col); margin: 2.5rem auto -1.5rem; padding: 0 6vw;
  font-family: var(--sans); font-size: .72rem; line-height: var(--lh-label);
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--ink-faint); text-decoration: none;
  transition: color .25s ease;
}
.back .arr { transition: transform .25s cubic-bezier(.16,.84,.44,1); }
.back:hover { color: var(--accent); }
.back:hover .arr { transform: translateX(-4px); }

@media (prefers-reduced-motion: reduce) {
  .masthead { transition: none; }
  body.nav-hidden .masthead { transform: none; }
}

/* a note naming the work it came from, and the work naming the note */
.notes-on { margin-top: .45rem; }
.notes-on a { color: var(--ink-soft); }
.notes-on a:hover { color: var(--accent); }
.home-index .entry.static:target,
.home-index .entry:target { box-shadow: inset 3px 0 0 var(--accent); padding-left: .9rem; }

/* ── lightbox controls ───────────────────────────────────────────────────────
   Visible, because a keyboard shortcut nobody can see is not a feature.
   Quiet, because the photograph is the point. They dim at the ends of a
   sequence rather than vanishing, so the frame never jumps.               */
#lightbox { position: fixed; }
#lb-prev, #lb-next, #lb-close {
  position: fixed; z-index: 2;
  appearance: none; -webkit-appearance: none;
  background: none; border: 0; cursor: pointer;
  color: rgba(255,255,255,.62);
  font-family: var(--serif); line-height: 1;
  transition: color .2s ease, opacity .2s ease, transform .2s cubic-bezier(.16,.84,.44,1);
}
#lb-prev, #lb-next {
  top: 50%; transform: translateY(-50%);
  font-size: 3.4rem; padding: 1.4rem 1.1rem;
}
#lb-prev { left: max(.5rem, 2vw); }
#lb-next { right: max(.5rem, 2vw); }
#lb-prev:hover:not(:disabled) { color: #fff; transform: translateY(-50%) translateX(-4px); }
#lb-next:hover:not(:disabled) { color: #fff; transform: translateY(-50%) translateX(4px); }
#lb-prev:disabled, #lb-next:disabled { opacity: .18; cursor: default; }

#lb-close {
  top: 1rem; right: max(.5rem, 2vw);
  font-size: 2rem; padding: .6rem .8rem;
}
#lb-close:hover { color: #fff; }

#lb-prev:focus-visible, #lb-next:focus-visible, #lb-close:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 2px;
}

/* on a phone the gesture is the swipe, so the arrows shrink out of the way */
@media (max-width: 700px) {
  #lb-prev, #lb-next { font-size: 2.2rem; padding: 1rem .5rem; }
}
@media (prefers-reduced-motion: reduce) {
  #lb-prev, #lb-next, #lb-close { transition: none; }
}

/* ── grain ───────────────────────────────────────────────────────────────────
   A tooth on the paper, so the page and the photograph share a surface instead
   of a picture floating on glass.

   Three rules:
   · It sits BEHIND everything (z-index: -1). It never overlays a photograph.
     Your images already carry your grain; adding more on top would be altering
     the work.
   · It is fixed, not scrolling. Paper does not move under the ink.
   · It is very light. Turn --grain up if you want more; past about .06 it stops
     reading as a surface and starts reading as an effect.

   The noise is a 160px SVG tile repeated — one filter pass on a small square,
   then tiled, rather than a filter running across the whole viewport.        */

:root { --grain: .065; }
/* I originally set dark LOWER on the theory that grain reads stronger against
   black. Wrong in practice: on a near-black page the noise disappears and the
   photographs float in a void. Dark needs MORE, not less. */
:root[data-theme="dark"] { --grain: .105; }

body::before {
  content: "";
  position: fixed;
  inset: -50%;                 /* overscan, so nothing shows at the edges */
  z-index: -1;
  pointer-events: none;
  opacity: var(--grain);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
  background-repeat: repeat;
}

/* anyone who has asked their system for higher contrast gets a clean surface */
@media (prefers-contrast: more) {
  body::before { display: none; }
}

/* ── the self-portraits on About ─────────────────────────────────────────────
   A row, not a single frame, because it is a series — you read them as
   variations on one setup, which is the point of the work. Smaller than a
   plate: these are pictures OF the person, and they should not open the page
   louder than the writing does.                                            */
.portraits {
  display: flex; gap: 1rem; align-items: flex-end; justify-content: center;
  max-width: 62em; margin: 2vh auto 0; padding: 0 6vw;
}
.portraits img { display: block; width: auto; height: auto; max-height: 40vh; max-width: 100%; cursor: zoom-in; }
.portraits.n1 img { max-height: 56vh; }
/* the phone rule lives in one place — see "the portraits on About, on a
   phone" below */

/* ════════════════════════════════════════════════════════════════════════════
   TWO SITES, NOT ONE SITE AT TWO SIZES

   The contexts genuinely differ, so the layouts should:

   PHONE — one hand, one column, no hover, probably late, probably dark.
     The photograph takes the whole width. Nothing sits beside it because there
     is no beside. Everything is vertical and the pictures are the interface.

   DESKTOP — 55–66% of the screen was empty beside the writing. That space is
     not "clean", it is unused. The captions move into it: set in the left
     margin, aligned to the top of the frame, the way a wall label sits beside
     a print rather than underneath it. The photograph is uninterrupted, the
     information is peripheral, and nothing is pushed down the page.
   ════════════════════════════════════════════════════════════════════════════ */

/* ── PHONE ─────────────────────────────────────────────────────────────────── */
@media (max-width: 760px) {
  figure { padding: 0 3vw; }                 /* was 6vw — give the picture the width */
  .plate-large img  { max-height: none; max-width: 100%; }
  .plate-medium img { max-height: 46vh; }
  .plate-small img  { max-height: 30vh; }

  figcaption { padding: 0 1vw; font-size: .74rem; }

  .masthead nav { gap: .85rem; font-size: .64rem; }
  .masthead .name { letter-spacing: .1em; }

  .title-block { margin-top: 3rem; }
  .section-mark { margin: 4rem auto 2.2rem; }

  /* thumb-sized targets, not mouse-sized */
  .home-index .entry { padding: 1.35rem 0; }
  .plate-list li { padding: 1rem 0; }
}

/* ── DESKTOP ───────────────────────────────────────────────────────────────── */
@media (min-width: 1100px) {
  figure {
    display: grid;
    grid-template-columns: 1fr minmax(0, 78vw) 1fr;
    align-items: start;
    padding: 0 3vw;
    column-gap: 1.6rem;
  }
  /* row 1, top-aligned: the same row as the caption, tops level. align-self
     overrides figure img's centre (line ~233), which is for the phone's flex
     column; here justify-self does the centring. */
  figure img, figure .two, figure video {
    grid-column: 2; grid-row: 1; justify-self: center; align-self: start; min-width: 0;
  }

  /* the caption lives in the left margin, level with the top of the frame.
     The negative margin lifts the half-leading and the space above the
     numerals out, so the ink of the first line meets the photograph's top
     edge rather than the line box doing so. In em, so it holds at the
     smaller size below 1320px too. Measured 23 September 2026: 0.4em. */
  figure figcaption {
    grid-column: 1; grid-row: 1;
    margin-top: -.4em;
    text-align: right;
    justify-self: end;
    max-width: 15em;
    padding: 0;
  }
  figure figcaption .n { display: block; margin: 0 0 .3rem; }

  /* if the margin is too tight to read in, fall back to underneath */
  @media (max-width: 1320px) {
    figure figcaption { max-width: 11em; font-size: .72rem; }
  }

  .title-block h1 { font-size: clamp(2.2rem, 3.4vw, 3.4rem); }
  .plate-list { max-width: 52em; }
}

/* very wide — let the photographs breathe rather than sprawl */
@media (min-width: 1800px) {
  figure { grid-template-columns: 1fr minmax(0, 1500px) 1fr; }
  .opening img { max-height: 86vh; }
}

/* ── the phone menu ──────────────────────────────────────────────────────────
   Five links and a toggle will not sit in a phone header without becoming
   decoration. So on a phone the header holds the name and one button, and the
   links live in a panel where they can be read at a size worth tapping.    */

#menu { display: none; }
#panel { display: none; }

@media (max-width: 760px) {
  .masthead { align-items: center; }
  .masthead .name {
    white-space: nowrap;            /* it was wrapping onto two lines */
    letter-spacing: .09em;
    font-size: .72rem;
  }
  .masthead nav.wide { display: none; }

  #menu {
    display: flex; flex-direction: column; justify-content: center; gap: 5px;
    width: 34px; height: 34px; margin-right: -6px;
    appearance: none; -webkit-appearance: none;
    background: none; border: 0; padding: 0; cursor: pointer;
  }
  #menu span {
    display: block; height: 1.5px; width: 22px; margin-left: auto;
    background: var(--ink);
    transition: transform .28s cubic-bezier(.16,.84,.44,1), opacity .2s ease;
  }
  body.menu-open #menu span:first-child { transform: translateY(3.25px) rotate(45deg); }
  body.menu-open #menu span:last-child  { transform: translateY(-3.25px) rotate(-45deg); }

  #panel {
    display: flex; position: fixed; inset: 0; z-index: 40;
    flex-direction: column; justify-content: center;
    background: var(--paper);
    padding: 0 8vw;
    opacity: 0; pointer-events: none;
    transition: opacity .26s ease;
  }
  #panel[data-open] { opacity: 1; pointer-events: auto; }
  #panel nav { display: flex; flex-direction: column; gap: 0; }
  #panel nav a {
    font-family: var(--serif); text-transform: none; letter-spacing: 0;
    font-size: 1.9rem; line-height: var(--lh-label); padding: .55rem 0;
    text-decoration: none; border-bottom: 1px solid var(--rule); color: var(--ink);
  }
  #panel nav a.on { color: var(--accent); }
  #panel nav #theme { display: none; }
  .panel-foot { margin-top: 2.5rem; }
  .panel-foot #theme { display: inline-flex; }
  body.menu-open { overflow: hidden; }
}

/* ── the portraits on About, on a phone ──────────────────────────────────────
   Three stacked frames is 120vh of me before a word of writing. As a set —
   one large, two beneath — they read as a series in a single glance.

   One rule for .portraits at this breakpoint. This used to be three,
   overlapping and re-declaring each other (a flex fallback that never
   applied, a grid definition, and an overflow-guard pass that quietly
   corrected it) — hard to change safely because it was never obvious which
   one actually won. Do not add a fourth; edit this one.

   padding: 0 6vw so the photographs share an edge with the prose below
   (which is padded the same way) rather than the ~11px mismatch it used to
   have at 0 3vw. */
@media (max-width: 760px) {
  .portraits {
    display: grid; grid-template-columns: 1fr 1fr; gap: .6rem;
    align-items: start; padding: 0 6vw;
  }
  .portraits > picture:first-child { grid-column: 1 / -1; }
  .portraits img { width: 100%; height: auto; max-height: none; }
}

/* ── overflow guards ─────────────────────────────────────────────────────────
   Grid and flex children default to min-width:auto — they will not shrink
   below the intrinsic width of what is inside them. A 2600px photograph in a
   grid column therefore makes the column 2600px, the document wider than the
   phone, and everything squashes into a band on the left. min-width:0 is the
   fix, and no page should ever be able to scroll sideways.               */

html, body { max-width: 100%; overflow-x: hidden; }
img, picture, video { max-width: 100%; }

/* On About, <picture> is the grid item, not the <img> inside it. In a plate
   it is the other way round (display:contents, see "<picture> wrapper"
   above); the figure > picture rules that used to sit here assumed otherwise,
   never applied, and were removed 23 September 2026. The desktop grid rule
   for figure img carries min-width:0 instead. */
.portraits > picture { display: block; min-width: 0; }

/* ── the monogram ────────────────────────────────────────────────────────────
   Used as a CSS mask rather than a picture, so it takes --ink and follows the
   theme from one file — no second copy for dark mode, no black square on a
   dark page. The name stays as real text beside it: searchable, selectable,
   readable by a screen reader. The mark is decorative, so it is hidden from
   assistive tech rather than announced twice.                             */
.masthead .name { display: inline-flex; align-items: center; gap: .55rem; }
.masthead .name .mark {
  display: block; width: .95rem; height: 1.32rem; flex: none;
  background: var(--ink);
  -webkit-mask: url("/mark.png") center / contain no-repeat;
          mask: url("/mark.png") center / contain no-repeat;
  transition: background .3s ease;
}
.masthead .name:hover .mark { background: var(--accent); }

/* On the inner pages the monogram stands alone, so it carries a little more
   weight than it did sitting next to the name — otherwise it reads as a
   leftover rather than a mark. The accessible name lives on the link. */
.masthead .name.mono { gap: 0; }
.masthead .name.mono .mark { width: 1.14rem; height: 1.58rem; }

@media (max-width: 760px) {
  .masthead .name { gap: .45rem; }
  .masthead .name .mark { width: .82rem; height: 1.14rem; }
  .masthead .name.mono .mark { width: .95rem; height: 1.32rem; }
}

/* ════════════════════════════════════════════════════════════════════════
   WHAT THE EYES — ALTERNATE LAYOUT, /what-the-eyes-columns

   Two parallel columns instead of two stacked sections, so the imbalance
   between them is spatial rather than something a reader has to count to
   find. See Guides/What-the-Eyes-layout-concept.md. Reuses figure/figcaption
   as everywhere else on the site — plate sizes, proportions, all of it —
   this only touches how the columns and the ledger around them are laid
   out.
   ════════════════════════════════════════════════════════════════════════ */

.eye-cols {
  max-width: var(--sheet-w); margin: 5.5rem auto 0; padding: 0 3vw;
  display: grid; grid-template-columns: 1fr; row-gap: 4rem;
}
/* figure's own base rule adds 6vw of its own padding, meant for a
   full-width single column — redundant and cramping inside one of these,
   which already sets its own edges. */
.eye-col figure { padding: 0; }
.eye-col figure + figure { margin-top: 3.5rem; }
.eye-col figcaption { max-width: none; }
/* The site's desktop figure treatment floats the caption into a left
   margin outside a wide (78vw) image column — built for a plate running
   most of the page width, not one of two columns roughly a third that
   wide. Squeezed in here it still "worked" (grid tracks just clamp to
   whatever's available) but left captions cramped into a narrow strip and
   gave up real image width to an empty margin for nothing. Reset to a
   plain stacked figure — image, then caption below — the same shape every
   plate already takes below 1100px anyway. */
@media (min-width: 1100px) {
  .eye-col figure { display: block; }
  .eye-col figcaption { text-align: left; margin-top: .85rem; }
}

.eye-heading {
  font-family: var(--sans); font-size: .8rem; letter-spacing: .16em;
  text-transform: uppercase; color: var(--ink);
  margin: 0 0 2rem; background: var(--paper);
}

/* Hidden on desktop — the gap there is spatial (the seen column simply runs
   out), not a block of its own. Given a height only on the phone, below. */
.eye-gap { display: none; }

@media (min-width: 900px) {
  .eye-cols {
    grid-template-columns: 1fr 1fr; column-gap: 3rem; row-gap: 0;
    align-items: stretch;              /* the seen column stretches to match
                                           the unseen one's height, which is
                                           what leaves the hairline below
                                           room to run */
  }
  /* The empty column's only content: a hairline down its left edge, in
     --rule, nothing else. `align-items: stretch` above makes .eye-seen as
     tall as .eye-unseen even though its own eight photographs end well
     before that — the border runs the full stretched height, so it reaches
     exactly as far down as the unseen column does, automatically, with no
     measurement of where "empty" actually starts. */
  .eye-seen { border-left: 1px solid var(--rule); padding-left: 1.6rem; }
  .eye-unseen { padding-left: 1.6rem; }

  /* Stays legible past frame fourteen. top clears the sticky masthead
     (measured, not guessed — see CLAUDE.md if this ever needs retuning
     against a masthead height change). */
  .eye-heading {
    position: sticky; top: 4.5rem; z-index: 5;
    padding: .8rem 0;
  }
}

@media (max-width: 899px) {
  /* The desktop absence is spatial; here there is no "beside", so it becomes
     duration instead — a long, near-empty scroll standing in for the empty
     column. Same hairline, same meaning, rotated ninety degrees. This block
     sits in normal flow directly before .eye-heading in the markup, so the
     reader always meets the emptiness first and the heading only once
     they've scrolled through it. */
  .eye-gap {
    display: block; height: 82vh;
    border-left: 1px solid var(--rule);
  }
}

.eye-ledger { margin-top: 6rem; }
.eye-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 1.5rem;
  padding: .7rem 0; border-bottom: 1px solid var(--rule);
  font-size: .82rem; line-height: var(--lh-tight); color: var(--ink-soft);
}
.eye-row:first-child { border-top: 1px solid var(--rule); }
.eye-row .counts { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The one interpretive mark on the page. Nothing else here is in accent. */
.eye-row .zero { color: var(--accent); }
