/* ---------------------------------------------------------------------------
   Shared styles for the public pages.

   Art direction v2, from the five reference frames: a mid-grey ground with
   white type, a chartreuse rail carrying only "Hello!" and the + that opens
   the menu, a charcoal panel for project write-ups, and a chartreuse bar
   pinned bottom-right.

   Everything reads from the tokens below — change those first.
--------------------------------------------------------------------------- */

:root {
  /* --- ground ------------------------------------------------------------
     The list pages: a light off-white with dark type. */
  --ground:     #efefef;

  /* One grey for every piece of text on the site — the same one the menu and
     the bottom bar use on the chartreuse. 5.8:1 on this ground, 5.3:1 on the
     chartreuse, so it clears WCAG AA on both. */
  --ink:        #5c5c5c;
  --ink-soft:   #5c5c5c;

  /* A step lighter, for the labels over the meta rules. About as light as the
     grey can go and still clear WCAG AA on the ground — 4.57:1. One value
     lighter lands at 4.4996:1, which misses. */
  --ink-light:  #6c6c6c;

  --chartreuse: #d6f21d;
  --on-yellow:  var(--ink);   /* the same grey, on the chartreuse */

  /* The pills along the foot: a heavy frost under a near-solid wash of the
     page's own ground, carrying the same grey as everything else. Quiet in
     tone rather than see-through — let down much further and the grey stops
     reading wherever a pill crosses something dark, which on these pages it
     does. At 0.88 it holds 4.5:1 even over black. */
  --bar-bg:     rgba(246, 246, 246, 0.88);
  --bar-solid:  #f1f1f1;                  /* where there is no blur to be had */
  --on-bar:     var(--ink);

  /* A project write-up now sits on the same ground as the projects index, in
     the same ink — these stay as their own tokens only so the write-up can be
     given a surface of its own again without hunting through the sheet. */
  --panel:      var(--ground);
  --panel-ink:  var(--ink);
  --panel-mid:  var(--ink);

  --img-empty:  #0b0b0b;   /* an empty picture reads as a black block */
  --line:       rgba(31, 31, 31, 0.18);
  --wash:       rgba(31, 31, 31, 0.05);   /* hover and note grounds */

  /* --- measure ----------------------------------------------------------- */
  /* the write-up panel's own inset — the stage matches it exactly, so the
     pictures begin at the top of the title and end at the foot of the
     previous/next links */
  --panel-top:    clamp(48px, 9vh, 120px);
  --panel-bottom: clamp(60px, 8vh, 96px);

  /* the space between one picture or clip and the next, whether they are
     separate gallery entries or several inside one interactive block */
  --media-gap: clamp(14px, 1.6vw, 28px);

  --rail:   clamp(64px, 5.6vw, 112px); /* the chartreuse column, left      */
  --drawer: clamp(258px, 26vw, 340px); /* the menu that slides out of it   */
  --bar:  46px;                        /* the row of pills along the foot  */
  --pad:  clamp(20px, 3vw, 46px);

  --font: Inter, "Inter Placeholder", ui-sans-serif, system-ui, -apple-system, sans-serif;

  /* --- type -------------------------------------------------------------
     A tight neo-grotesque, set light at display sizes and regular below, with
     tracking that closes up as the type grows. */
  --type-hero:    clamp(15px, 2.5vw, 40px);   /* 30% of what it was */
  --type-display: clamp(34px, 4.4vw, 76px);
  --type-title:   clamp(20px, 1.6vw, 26px);
  --type-lead:    clamp(16px, 1.15vw, 19px);
  --type-body:    16px;
  --type-meta:    14px;

  --lh-hero: 0.94;  --lh-display: 1.0;  --lh-title: 1.12;  --lh-body: 1.5;
  --track-hero: -0.045em;  --track-display: -0.035em;  --track-body: -0.006em;

  /* --- motion ------------------------------------------------------------ */
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --dur:  680ms;
}

* { box-sizing: border-box; }

html { background: var(--ground); scroll-behavior: smooth; }
html, body { margin: 0; padding: 0; }

body {
  min-height: 100vh;
  padding-left: var(--rail);
  /* --bar-h is the row's real height, measured once it is on the page; --bar
     is what to leave for it before that, and if the pills wrap it grows. */
  padding-bottom: calc(var(--bar-h, var(--bar)) + 12px);
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--type-body);
  font-weight: 400;
  line-height: var(--lh-body);
  letter-spacing: var(--track-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, figure, ul, ol { margin: 0; padding: 0; }
ul, ol { list-style: none; }
img { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }

/* ===========================================================================
   Motion
   ---------------------------------------------------------------------------
   Reveals are opt-in per element and only ever hidden once JS has confirmed it
   is running — with scripting off, `.js` never lands and every [data-reveal]
   stays visible.
=========================================================================== */

.js [data-reveal] {
  opacity: 0;
  transform: translateY(26px);
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
  transition-delay: var(--d, 0ms);
}

.js [data-reveal].is-in { opacity: 1; transform: none; }

/* A line of type that wipes up from behind its own edge. The span is the thing
   that moves; the element clips it.

   The clip box is the line box, and a tight line-height makes that shorter than
   the font's ink — the tail of a "j" was losing its last pixel. The padding
   gives the clip somewhere to go; the negative margin puts the layout back
   exactly where it was. */
.wipe {
  display: block;
  overflow: hidden;
  /* Top and bottom, and generously: a tight line-height makes the line box
     shorter than the typeface's ink, and how much shorter depends on the
     font's own metrics. Inter overshoots further than the system fallback, so
     a value tuned against the fallback clipped on the real thing. The negative
     margin keeps the layout exactly where it was. */
  padding: 0.22em 0;
  margin: -0.22em 0;
}

.js .wipe > span {
  display: block;
  /* past the extra padding as well as its own height, or the top of the parked
     line would show through the bottom of the clip */
  transform: translateY(155%);
  transition: transform 900ms var(--ease);
  transition-delay: var(--d, 0ms);
}

.js .wipe.is-in > span { transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .js [data-reveal],
  .js .wipe > span { opacity: 1; transform: none; transition: none; }
}

/* ===========================================================================
   The rail — chartreuse, fixed left. It carries the + and nothing else but
   "Hello!"; the links live in the menu the + opens.
=========================================================================== */

/* The whole strip opens the drawer — on a pointer by being hovered, on a
   touch by being tapped — so the + is the sign rather than the only way in. */
.rail {
  cursor: pointer;
  position: fixed;
  inset: 0 auto 0 0;
  z-index: 60;
  width: var(--rail);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 26px;
  padding: 16px 0 22px;
  background: var(--chartreuse);
  color: var(--on-yellow);
}

/* how far down the page you are, drawn on the rail's inner edge */
.rail__progress {
  position: absolute;
  top: 0;
  right: 0;
  width: 3px;
  height: var(--p, 0%);
  /* the ink, not the panel: the panel is the page ground now, which on the
     chartreuse would be a near-white line nobody can see */
  background: var(--ink);
  transition: height 120ms linear;
}

/* --- the + ---------------------------------------------------------------
   Two bars; the upright one rotates away to leave an x when the menu is open.
------------------------------------------------------------------------- */
.plus {
  flex: none;
  width: 46px;
  height: 46px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.plus:focus-visible { outline: 2px solid var(--on-yellow); outline-offset: 2px; }

.plus__cross { position: relative; display: block; width: 22px; height: 22px; }

.plus__bar {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 1.5px;
  margin-top: -0.75px;
  background: currentColor;
  transition: transform 460ms var(--ease);
}

.plus__bar--v { transform: rotate(90deg); }
.plus[aria-expanded="true"] .plus__bar--v { transform: rotate(135deg); }
.plus[aria-expanded="true"] .plus__bar--h { transform: rotate(45deg); }

/* --- Hello! --------------------------------------------------------------
   Set down the column, reading top to bottom, the way it has always run on
   the homepage.
------------------------------------------------------------------------- */
.rail__hello {
  transform: rotate(90deg);
  transform-origin: 50% 50%;
  white-space: nowrap;
  margin-top: 30px;
  font-size: clamp(24px, 2.2vw, 38px);
  font-weight: 300;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--on-yellow);
}

@media (prefers-reduced-motion: reduce) {
  .rail__progress, .plus__bar { transition: none; }
}

/* ===========================================================================
   The menu the + opens — a chartreuse drawer that slides out from behind the
   rail, the way it always has on the homepage. It sits below the rail in the
   stack, so it appears to come out of it.
=========================================================================== */

.menu {
  position: fixed;
  top: 0;
  left: var(--rail);
  z-index: 50;              /* over the bar, under the rail */
  width: var(--drawer);
  max-width: calc(100vw - var(--rail));
  height: 100vh;
  display: flex;
  flex-direction: column;
  gap: clamp(22px, 4vh, 34px);
  padding: clamp(70px, 11vh, 112px) clamp(22px, 2vw, 34px) clamp(26px, 4vh, 40px);
  background: var(--chartreuse);
  color: var(--on-yellow);
  overflow-y: auto;
  overscroll-behavior: contain;
  /* hidden rather than merely slid away: a drawer parked off-screen still
     holds focusable links */
  visibility: hidden;
  transform: translateX(-100%);
  transition: transform 460ms var(--ease), visibility 0s linear 460ms;
}

.menu[data-open="true"] {
  visibility: visible;
  transform: none;
  transition: transform 460ms var(--ease), visibility 0s;
}

.menu__bio p { font-size: var(--type-meta); line-height: 1.55; }
.menu__bio p + p { margin-top: 1.2em; }

.menu__nav { display: flex; flex-direction: column; gap: 13px; margin-top: auto; }

.menu__nav a {
  display: block;
  overflow: hidden;   /* clips the wipe */
  width: max-content;
  max-width: 100%;
  padding: 0.22em 0;
  margin: -0.22em 0;
  font-size: var(--type-lead);
  line-height: 1.25;
}

.menu__nav a span {
  display: block;
  text-decoration: underline;
  text-underline-offset: 4px;
  transform: translateY(165%);
  transition: transform 620ms var(--ease);
  transition-delay: var(--d, 0ms);
}

.menu[data-open="true"] .menu__nav a span { transform: none; }

.menu__nav a:hover span,
.menu__nav a:focus-visible span { text-decoration-thickness: 2px; }

/* every link is underlined, so the open page is marked by weight */
.menu__nav a[aria-current="page"] span { font-weight: 500; text-decoration-thickness: 2px; }

/* the monogram, bottom left, arriving last */
.menu__mark {
  display: block;
  width: clamp(34px, 3.4vw, 46px);
  color: #1c1c1c;
  opacity: 0;
  transition: opacity 420ms var(--ease);
}

.menu__mark svg { display: block; width: 100%; height: auto; }
.menu[data-open="true"] .menu__mark { opacity: 1; transition-delay: 430ms; }

/* the copy fades up behind the links, a beat later */
.menu__bio {
  opacity: 0;
  transform: translateY(10px);
  transition: opacity 380ms var(--ease), transform 380ms var(--ease);
}

.menu[data-open="true"] .menu__bio { opacity: 1; transform: none; transition-delay: 140ms; }

@media (prefers-reduced-motion: reduce) {
  .menu, .menu__nav a span, .menu__bio, .menu__mark { transition: none; }
  .menu__nav a span, .menu__bio { transform: none; }
  .menu__bio, .menu__mark { opacity: 1; }
}

/* ===========================================================================
   Page furniture
=========================================================================== */

.main { padding: clamp(44px, 8vh, 110px) var(--pad) clamp(60px, 10vh, 130px); }

.lede { max-width: 34ch; }

/* Sized to fit the column it sits in, not the viewport. The wipe clips its own
   box on every side, so a word wider than the column — "Transmissions" — loses
   its tail rather than overflowing. */
.lede__title {
  font-size: clamp(28px, 2.8vw, 46px);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: var(--track-display);
}

.lede__note { margin-top: clamp(18px, 4vh, 46px); font-size: var(--type-lead); }

/* two-column spread: the lede holds still on the left while the work scrolls */
.spread {
  display: grid;
  grid-template-columns: minmax(0, 360px) minmax(0, 1fr);
  gap: clamp(28px, 5vw, 96px);
  align-items: start;
}

.spread__side { position: sticky; top: clamp(44px, 8vh, 110px); }

/* The projects page is the work and nothing else: no heading over it and no
   paragraph beside it, so the grid has the whole width. The heading is still
   said aloud — a page with no heading at all is one a screen reader cannot get
   its bearings in — and the paragraph is still the editor's to write, waiting
   for the day it is wanted back. */
.spread--bare { grid-template-columns: minmax(0, 1fr); gap: 0; }

.spread--bare .spread__side {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.spread--bare .lede__note { display: none; }

.note {
  max-width: 60ch;
  padding: 20px 22px;
  background: var(--wash);
  font-size: var(--type-meta);
}

.note a { text-decoration: underline; text-underline-offset: 3px; }

/* ===========================================================================
   Projects — a staggered grid, then the full index underneath
=========================================================================== */

/* Columns sit close together so the covers read as one field; the rows keep
   their air, or a card's writing would crowd the picture under it. */
.grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  column-gap: 5px;
  row-gap: clamp(34px, 3.6vw, 58px);
}

/* Every card starts at the top of its row and every cover is cropped the same,
   so titles and captions line up straight across. */
.grid > * { align-self: start; }

.card { display: block; }

/* The frame clips the hover zoom and gives every card the same 4:5 box,
   whatever shape the picture inside it is. Being a fixed ratio rather than one
   taken from the picture, it also holds the card open while the cover is still
   on its way, and keeps it open if the cover never arrives — which is what a
   failed cover used to do silently, leaving the grid looking empty. */
.card__frame {
  position: relative;        /* the frost is laid over the picture */
  display: block;
  overflow: hidden;
  background: var(--img-empty);
  aspect-ratio: 4 / 5;
}

/* The frame is one shape for all of them, so the cover fills it and takes the
   crop. Centred, so what is cut comes off both edges rather than one. */
.card__img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  background: var(--img-empty);
  transition: transform 900ms var(--ease);
}

.card:hover .card__img { transform: scale(1.045); }

/* The frost: a wash of the page's own ground over a blur of the cover, with
   the title on it. The wash is nearly solid because the title has to read over
   whatever the picture happens to be; the blur is what makes it glass. */
.card__veil {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  padding: clamp(12px, 1.1vw, 20px);
  background: rgba(246, 246, 246, 0.88);
  -webkit-backdrop-filter: blur(18px) saturate(140%);
  backdrop-filter: blur(18px) saturate(140%);
  opacity: 0;
  transition: opacity 420ms var(--ease);
}

.card:hover .card__veil,
.card:focus-visible .card__veil { opacity: 1; }

/* Nothing hovers on a touch screen, so the title sits in a band at the foot of
   the card rather than waiting for a pointer that is never coming. */
@media (hover: none) {
  .card__veil {
    opacity: 1;
    inset: auto 0 0 0;
    background: rgba(246, 246, 246, 0.92);
  }
}

/* No picture, so nothing to take a shape from — this one keeps a set ratio so
   the card still reads as a card. */
/* No picture to take a shape from: the frame's placeholder ratio stands. */
.card__img--empty {
  display: grid;
  place-items: center;
  padding: 10px;
  color: rgba(255, 255, 255, 0.55);
  font-size: var(--type-meta);
  text-align: center;
}

/* The card says two things: what it is, and when. Both are set as body copy —
   same size, same measure — and the year is told apart by stepping back in
   colour alone. */
.card__title {
  margin: 0;
  overflow-wrap: anywhere;
  font-size: var(--type-body);
  font-weight: 400;
  line-height: 1.3;
  letter-spacing: var(--track-body);
  color: var(--ink);
}

/* The same mark the menu uses, at label size: two hairlines for more, one for
   less. No pill, no border — the sign carries it. */
.showmore {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(18px, 2.6vw, 40px);
  /* the padding below buys a thumb-sized target; take it back off the top so
     the row still sits where the eye expects it */
  margin-top: calc(clamp(40px, 7vh, 84px) - 11px);
}

.showmore button {
  display: inline-flex;
  align-items: center;
  gap: 11px;
  padding: 11px 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font: inherit;
  font-size: var(--type-body);
  letter-spacing: var(--track-body);
  cursor: pointer;
}

.showmore .plus__cross { width: 15px; height: 15px; flex: none; }

.showmore button:hover span:last-child {
  text-decoration: underline;
  text-underline-offset: 4px;
}

.showmore button:focus-visible { outline: 2px solid var(--ink); outline-offset: 4px; }

/* --- the index ----------------------------------------------------------- */
.index { display: block; margin-top: clamp(56px, 10vh, 120px); border-top: 1px solid var(--line); }

.index__row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr) 70px;
  gap: 20px;
  align-items: start;
  padding: 16px 0;
  border-bottom: 1px solid var(--line);
  transition: padding-left 420ms var(--ease), background 420ms var(--ease);
}

.index__row:hover { padding-left: 16px; background: var(--wash); }
.index__title { font-size: var(--type-lead); }
.index__disc, .index__year { font-size: var(--type-meta); color: var(--ink-soft); }
.index__year { text-align: right; }

/* The picture that follows the pointer down the written index. Fixed, so it is
   placed in viewport coordinates and nothing in the page can clip it; it is
   moved by transform alone, which keeps it off the layout entirely. */
.peek {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 40;
  width: clamp(132px, 11vw, 176px);
  aspect-ratio: 4 / 5;
  overflow: hidden;
  background: var(--img-empty);
  pointer-events: none;
  opacity: 0;
  transform: translate3d(-9999px, -9999px, 0);
  transition: opacity 260ms var(--ease);
}

.peek.is-on { opacity: 1; }

.peek img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@media (prefers-reduced-motion: reduce) {
  .peek { transition: none; }
}

/* ===========================================================================
   Project write-up — charcoal panel left, pictures right, both full bleed
=========================================================================== */

/* The write-up is the whole page: the panel and the pictures run edge to edge
   and floor to ceiling, with no page ground showing anywhere around them. The
   body class does the work so the light pages keep their padding. */
body.work-page { padding-bottom: 0; }
body.work-page .main { padding: 0; }
body.work-page .note { margin: var(--pad); }

.work {
  display: grid;
  grid-template-columns: minmax(0, 0.62fr) minmax(0, 1fr);
  /* fills the window even when the copy is short and there are no pictures,
     which is what stops a band of page ground appearing under it */
  min-height: 100vh;
}

/* Holds still while the pictures go past.

   The height cap and the inner scroll are what make that safe: these write-ups
   run longer than a screen, and a sticky block taller than the viewport pins
   its top and puts everything past the fold out of reach. Held to exactly one
   windowful, a long write-up scrolls inside the panel and then carries on
   scrolling the page, so nothing is unreachable. */
.work__panel {
  position: sticky;
  top: 0;
  align-self: start;
  height: 100vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  padding: var(--panel-top) clamp(26px, 2.6vw, 52px) var(--panel-bottom);
  background: var(--panel);
  color: var(--panel-ink);
  /* the inner scrollbar sits right beside the chartreuse rail, so it is toned
     down rather than left at the browser default */
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

/* A step below the page displays: this one sits in a narrow column and her
   titles run long, so the reference's 2.5vw is the size that holds. */
.work__title {
  font-size: clamp(30px, 2.6vw, 50px);
  font-weight: 300;
  line-height: 1.04;
  letter-spacing: var(--track-display);
}

/* --- the meta grid -------------------------------------------------------
   Client / type / year / location as small labelled cells, each a tiny
   bracketed caption over a hairline with the value under it. auto-fit means
   the same markup gives three columns in a wide panel and one on a phone,
   with no breakpoint to keep in step.
------------------------------------------------------------------------- */
.work__meta {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: clamp(18px, 3vh, 26px) clamp(16px, 2vw, 30px);
  margin-top: clamp(28px, 5vh, 56px);
}

.meta__cell { display: flex; flex-direction: column; min-width: 0; }

/* the role list runs long, so it takes the whole width on its own row */
.meta__cell--wide { grid-column: 1 / -1; }

/* Set like the body copy — same size, same tracking, no caps — and told apart
   from the value under it by colour alone. */
.meta__label {
  padding-bottom: 6px;
  border-bottom: 1px solid var(--line);
  font-size: var(--type-body);
  line-height: var(--lh-body);
  letter-spacing: var(--track-body);
  color: var(--ink-light);
}

.meta__value {
  margin-top: 11px;
  font-size: var(--type-body);
  line-height: 1.35;
  color: var(--panel-ink);
  overflow-wrap: anywhere;
}

.meta__value > span { display: block; }
.meta__value > span + span { margin-top: 4px; }

/* flex: 1 lets the copy take up whatever height is going, so the pager below
   it ends up on the foot of the panel. On a phone there is no slack to take
   and it simply follows the copy. */
/* Nothing in the panel may be squeezed. It is a fixed-height flex column now
   that it sticks, so by default every child shrinks to make the content fit —
   which crushed the title to the height of its own padding and let .wipe clip
   the text off. The panel scrolls instead; that is what the inner scroll is
   for. */
.work__panel > * { flex: 0 0 auto; }

.work__body {
  flex: 1 0 auto;
  margin-top: clamp(30px, 5vh, 62px);
  max-width: 46ch;
  color: var(--panel-mid);
}
.work__body p + p, .work__body ul + p, .work__body p + ul { margin-top: 1.15em; }
.work__body li { position: relative; padding-left: 16px; }
.work__body li::before { content: "—"; position: absolute; left: 0; }
/* Not the chartreuse: on this ground it is 1.1:1 and effectively invisible.
   Every link is the one grey, told apart by its underline. */
.work__body a { text-decoration: underline; text-underline-offset: 3px; }

.work__awards { margin-top: 1.4em; padding-top: 1.2em; border-top: 1px solid rgba(255, 255, 255, 0.16); }
.work__awards li { position: relative; padding-left: 16px; line-height: 1.5; }
.work__awards li::before { content: "—"; position: absolute; left: 0; }

/* --- the gallery ----------------------------------------------------------
   A plain stack: one picture under the next, each the full width of its
   column and as tall as its own proportions make it. Nothing cropped, nothing
   pinned, nothing capped to the window.

   The top and bottom insets are the panel's, so the first picture starts level
   with the title and the last one stops level with the foot of the write-up —
   which also keeps it clear of the fixed bottom bar.
------------------------------------------------------------------------- */
.work__gallery {
  display: flex;
  flex-direction: column;
  gap: var(--media-gap);
  padding: var(--panel-top) clamp(20px, 2.4vw, 40px) var(--panel-bottom);
}

.work__gallery > img {
  width: 100%;
  height: auto;
  background: none;
}

/* A picture or a video that would not load. A broken <img> collapses to
   nothing and a video the browser cannot decode sits at the default 150px —
   either way the page looks broken with no sign of why, so each becomes a
   labelled block of real size. */
.work__gallery > .is-missing,
.work__embed .is-missing {
  display: grid;
  place-items: center;
  min-height: 34vh;
  padding: 24px;
  background: var(--img-empty);
  color: rgba(255, 255, 255, 0.55);
  font-size: var(--type-meta);
  line-height: 1.5;
  text-align: center;
}

/* --- interactive blocks ---------------------------------------------------
   Author-supplied markup from the portal. It sits in the picture column, full
   width, after the gallery. The rules below are defaults an embed can override
   with its own inline styles — they exist so a bare <iframe> or <canvas> lands
   sensibly instead of at its intrinsic 300x150.
------------------------------------------------------------------------- */
/* A block often holds several clips — her Digital Loom block is five videos
   one after another. As plain blocks they stacked flush; this spaces them the
   same as two separate gallery entries. */
.work__embed {
  width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--media-gap);
}

/* iframe, video and canvas are matched at any depth, since a pasted embed is
   often wrapped in a div of its own. img and svg stay direct children only —
   a complex embed may use small ones decoratively, and those should keep
   whatever size the author gave them. */
.work__embed iframe {
  display: block;
  width: 100%;
  border: 0;
  aspect-ratio: 16 / 9;
}

/* A whole document is not a 16:9 picture — it was written expecting a window
   and usually sizes itself with vh, so it gets a tall box of its own. */
.work__embed--doc iframe {
  aspect-ratio: auto;
  height: calc(100vh - var(--panel-top) - var(--panel-bottom));
  height: calc(100dvh - var(--panel-top) - var(--panel-bottom));
}

/* Same rule, but the box stays tight to the video: a <video> draws its
   controls at the foot of its box, and a full-height box would strand them a
   long way under a landscape clip. */
.work__embed video,
.work__embed canvas,
.work__embed > img,
.work__embed > svg {
  display: block;
  width: 100%;
  height: auto;
  object-fit: contain;
  margin: 0 auto;
}

/* A video the browser cannot decode has no size of its own, so without the
   swap below it sits at the default 150px: a wide empty slab with a play bar. */

.work__gallery--empty {
  display: grid;
  place-items: center;
  min-height: calc(100vh - var(--panel-top) - var(--panel-bottom));
  background: var(--img-empty);
  color: rgba(255, 255, 255, 0.55);
  font-size: var(--type-meta);
}

/* At the foot of the charcoal panel. Stacked rather than one row across:
   the column is narrow and her titles run long. */
.pager {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
  margin-top: clamp(34px, 6vh, 72px);
  padding-top: 20px;
  border-top: 1px solid var(--line);
  font-size: var(--type-meta);
  color: var(--panel-ink);
}

/* A grey, but a light one — this is on charcoal now, where the ink-soft grey
   used on the light pages would all but vanish. */
.pager a:hover { text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 2px; }
.pager > span { color: var(--ink); }

/* ===========================================================================
   Transmissions — an index whose rows open in place
=========================================================================== */

.t-list { margin-top: clamp(40px, 7vh, 84px); border-top: 1px solid var(--line); }
.t-item { border-bottom: 1px solid var(--line); }

.t-head {
  display: flex;
  gap: 18px;
  align-items: flex-start;
  width: 100%;
  padding: 16px 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: padding-left 420ms var(--ease);
}

.t-head:hover { padding-left: 14px; }

.t-thumb {
  flex: none;
  width: 62px;
  height: 44px;
  /* contain, like everywhere else — a thumbnail is small enough to lose its
     subject entirely to a crop */
  object-fit: contain;
  background: var(--img-empty);
}

.t-meta { display: flex; align-items: center; gap: 8px; font-size: var(--type-meta); color: var(--ink-soft); }
.t-meta svg { width: 15px; height: 15px; flex: none; }
.t-title { display: block; margin-top: 3px; font-size: var(--type-lead); }

/* the + on the right end of a row, turning to an x when the row is open */
.t-head::after {
  content: "";
  margin-left: auto;
  flex: none;
  width: 13px;
  height: 13px;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1.5px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1.5px 100% no-repeat;
  transition: transform 460ms var(--ease);
}

.t-head[aria-expanded="true"]::after { transform: rotate(135deg); }

/* grid-template-rows animates where height: auto cannot */
.t-panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows 560ms var(--ease);
}

/* Hidden rather than merely clipped: a zero-height row still holds focusable
   links, so without this you can tab into a closed transmission. */
.t-panel > div { overflow: hidden; visibility: hidden; transition: visibility 0s linear 560ms; }
.t-item.is-open .t-panel { grid-template-rows: 1fr; }
.t-item.is-open .t-panel > div { visibility: visible; transition: visibility 0s; }

.t-panel__inner { padding: 4px 0 30px 80px; max-width: 68ch; }
.t-panel img { width: 100%; margin-bottom: 18px; background: var(--img-empty); }
.t-ref { margin-bottom: 8px; font-size: var(--type-meta); color: var(--ink-soft); }
.t-panel p + p { margin-top: 1em; }
.t-link { display: inline-block; margin-top: 14px; font-size: var(--type-meta); text-decoration: underline; text-underline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .t-panel, .t-head, .t-head::after, .index__row, .card__img { transition: none; }
}

/* ===========================================================================
   About
=========================================================================== */

/* The writing on the left, her face on the right, and the lists underneath
   both. The portrait column is the narrower of the two: it is a picture of a
   person, not the subject of the page. */
.about__top {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr);
  gap: clamp(28px, 5vw, 86px);
  align-items: start;
  margin-bottom: clamp(44px, 8vh, 110px);
}

.about__intro { max-width: 58ch; }
.about__bio { margin-top: clamp(20px, 4vh, 44px); font-size: var(--type-lead); }
.about__bio p + p { margin-top: 1.2em; }

.about__figure { margin: 0; }

.about__figure img {
  display: block;
  width: 100%;
  background: var(--img-empty);
}

/* a square, so a portrait that never arrives holds the shape of one */
.about__placeholder {
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  background: var(--img-empty);
  color: rgba(255, 255, 255, 0.55);
  font-size: var(--type-meta);
}

.about__cols {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: clamp(24px, 3vw, 56px);
  padding-top: clamp(28px, 5vh, 56px);
  border-top: 1px solid var(--line);
}

.about__cols h2 { margin-bottom: 16px; font-size: var(--type-meta); font-weight: 400; color: var(--ink-soft); }
.about__list li { line-height: 1.7; }
.about__loading { margin-top: 16px; font-size: var(--type-meta); color: var(--ink-soft); }
.about__label { font-size: var(--type-meta); color: var(--ink-soft); }
.about__contact { margin-top: 6px; }
.about__contact a { text-decoration: underline; text-underline-offset: 3px; }

/* ===========================================================================
   Drafts — only ever visible with ?preview=1
=========================================================================== */

.draft { position: relative; }

.draft::before {
  content: "draft";
  position: absolute;
  z-index: 2;
  margin: 10px;
  padding: 3px 9px;
  border-radius: 30px;
  background: var(--chartreuse);
  color: var(--on-yellow);
  font-size: 11px;
  line-height: 1.5;
}

.t-item.draft::before { position: static; display: inline-block; margin: 14px 0 0; }

/* ===========================================================================
   The bar, bottom right
=========================================================================== */

/* A row of frosted pills rather than one bar: the line about who owns the
   place, then one for each way of reaching her, side by side across the foot
   of the page. Each carries its own ground, so the gaps between them show
   whatever the page has there. */
.foot {
  position: fixed;
  left: var(--rail);
  right: 0;
  bottom: 0;
  z-index: 45;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;   /* a cluster at the end, not a span across */
  gap: 6px;
  padding: 0 clamp(8px, 0.9vw, 14px) clamp(8px, 0.9vw, 14px);
  font-size: 12px;
  color: var(--on-bar);
}

/* The links are laid out by the row, not by their own box: display:contents
   hands each one straight to the flex line, so a pill is a pill whether it is
   the copyright or a link. */
.foot__links { display: contents; }

.foot > span:first-child,
.foot__links a {
  display: flex;
  align-items: center;
  min-height: 30px;
  padding: 5px 13px;
  border-radius: 999px;
  white-space: nowrap;
  background: var(--bar-bg);
  -webkit-backdrop-filter: blur(30px) saturate(150%);
  backdrop-filter: blur(30px) saturate(150%);
}

/* Each one is only as wide as what it carries. */
.foot > span:first-child,
.foot__links a { flex: 0 0 auto; }

.foot__links a { gap: 9px; }

/* The same mark the rail wears, at the size of a full stop: two hairlines
   crossed, drawn rather than typed so it needs no markup of its own — the
   footer's is written by the editor and cannot carry extra spans. */
.foot__links a::after {
  content: "";
  flex: none;
  width: 9px;
  height: 9px;
  background:
    linear-gradient(currentColor, currentColor) center / 100% 1px no-repeat,
    linear-gradient(currentColor, currentColor) center / 1px 100% no-repeat;
  opacity: 0.5;
  transition: transform 420ms var(--ease), opacity 320ms var(--ease);
}

.foot__links a:hover::after { transform: rotate(90deg); opacity: 1; }

/* Without the blur a pill is just a washed-out grey over whatever it covers,
   and the type has nothing dependable to sit on. Then they go solid. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .foot > span:first-child,
  .foot__links a { background: var(--bar-solid); }
}

.foot > span:last-child { display: none; }   /* the generator's spacer */

/* ===========================================================================
   Narrow
=========================================================================== */

@media (max-width: 1080px) {
  .spread { grid-template-columns: 1fr; }
  .spread__side { position: static; }
  .grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .about__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* stacked, the portrait follows the writing and need not be full width */
  .about__top { grid-template-columns: 1fr; }
  .about__figure { max-width: 440px; }
}

@media (max-width: 900px) {
  /* pictures first, then the panel — the reference's phone layout */
  .work { grid-template-columns: 1fr; }
  .work__panel {
    position: static;
    height: auto;
    min-height: 0;
    overflow: visible;
    grid-row: 2;
  }
  .work__gallery { grid-row: 1; padding: 0 0 var(--pad); }
  .work__gallery--empty { min-height: 46vh; padding: 0; }
  .index__row { grid-template-columns: minmax(0, 1fr) 62px; }
  .index__disc { display: none; }
}

/* --- the rail lies down across the top -----------------------------------
   Below this width a fixed column takes a fifth of the screen.
------------------------------------------------------------------------- */
@media (max-width: 768px) {
  .rail {
    inset: 0 0 auto 0;
    width: 100%;
    height: auto;
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 6px clamp(10px, 3vw, 20px);
  }

  .rail__progress { top: auto; bottom: 0; right: auto; left: 0; width: var(--p, 0%); height: 3px; }

  /* upright again, and first in the row, as in the reference */
  .rail__hello { order: -1; transform: none; margin: 0 0 0 6px; font-size: 30px; }

  body { padding-left: 0; padding-top: 58px; }

  .menu {
    top: 58px;
    left: 0;
    width: 100%;
    max-width: none;
    height: auto;
    max-height: calc(100vh - 58px);
    gap: 30px;
    padding: 26px var(--pad) 32px;
    /* out of the bar rather than out of the rail, since that is where the
       chartreuse is on a phone */
    transform: translateY(-110%);
  }

  .menu[data-open="true"] { transform: none; }

  /* links first: they are what the + was pressed for. The drawer is only as
     tall as its contents here, so the auto margin has nothing to push against
     and has to come off. */
  .menu__nav { order: -1; margin-top: 0; }
  .menu__mark { order: 1; }

  .main { padding-top: clamp(26px, 5vh, 44px); }
  /* the page already clears the fixed top bar, so the full window height would
     overshoot by exactly that much */
  .work { min-height: calc(100vh - 58px); }

  .foot { left: 0; justify-content: space-between; gap: 12px; }
  .t-panel__inner { padding-left: 0; }
}

@media (max-width: 560px) {
  .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .about__cols { grid-template-columns: 1fr; }
  .foot { font-size: 12px; }
  .foot__links { gap: 10px; }
}

/* ===========================================================================
   Home — the artwork full bleed, the name across the foot of it
=========================================================================== */

.home { min-height: 100vh; padding: 0; }

.hero {
  position: fixed;
  inset: 0 0 0 var(--rail);
  z-index: 0;
  overflow: hidden;
  background: var(--img-empty);
}

/* Held slightly larger than the frame so the pointer parallax has somewhere to
   travel without ever showing an edge. */
/* The character grid, drawn by ascii.js, sits in the artwork's own box and
   drifts with it. The picture underneath is still there at zero opacity. */
.hero__ascii {
  position: absolute;
  inset: -3.5%;
  width: 107%;
  height: 107%;
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0) scale(var(--z, 1));
  transition: transform 900ms var(--ease);
}

.hero__img {
  position: absolute;
  inset: -3.5%;
  /* the base `img { max-width: 100% }` would clamp this back to the frame,
     which leaves a gap on the right as soon as the parallax moves it */
  max-width: none;
  width: 107%;
  height: 107%;
  object-fit: cover;
  transform: translate3d(var(--mx, 0px), var(--my, 0px), 0) scale(var(--z, 1));
  transition: transform 900ms var(--ease);
}

.home__name {
  position: fixed;
  z-index: 5;
  left: calc(var(--rail) + var(--pad));
  right: var(--pad);
  bottom: clamp(18px, 4vh, 54px);      /* no footer here to clear */
  font-family: Geist, var(--font);
  font-size: var(--type-hero);
  font-weight: 300;
  line-height: var(--lh-hero);
  letter-spacing: -0.06em;
  color: #fff;
  pointer-events: none;
}

/* each word rises on its own beat */
.home__name span { display: inline-block; }

.js .home__name span {
  transform: translateY(1.4em);
  opacity: 0;
  transition: transform 1100ms var(--ease), opacity 900ms var(--ease);
  transition-delay: var(--d, 0ms);
}

.js .home__name.is-in span { transform: none; opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  .hero__img, .hero__ascii { transition: none; transform: none; }
  .js .home__name span { transform: none; opacity: 1; transition: none; }
}

@media (max-width: 768px) {
  .hero { inset: 58px 0 0 0; }

  /* the rail is a bar across the top here, so the row starts at the edge —
     and four pills will not sit on one line, so the long one takes its own */
  /* No room to cluster at the end here — they fill the line and wrap, so they
     start at the edge and stay level with each other. */
  .foot { left: 0; gap: 5px; justify-content: flex-start; }
  .home__name { left: var(--pad); }
}
