/* ─────────────────────────────────────────────────────────────────────────
   louiejacob.com — "slate"
   Carbon paper. Two faces: Hanken Grotesk for what you look at,
   Geist Mono for what you read. Photographs are the only colour.
   Nothing here deforms a picture: contain, native ratio, no zoom, no crop.
   ───────────────────────────────────────────────────────────────────────── */

@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/hanken-grotesk-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "Hanken Grotesk";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/hanken-grotesk-latin-ext-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 100 900;
  font-display: swap;
  src: url("/fonts/geist-mono-latin-wght-normal.woff2") format("woff2-variations");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ── Tokens ─────────────────────────────────────────────────────────────── */

:root {
  /* dark paper — the default; light is set by [data-theme="light"] below */
  --paper: #0a0a0b;
  --level-1: #111113;   /* a step up from the paper: sub-header, bands, benches */
  --level-2: #18181b;   /* two steps: the footer, the deepest band */
  --control: #2c2c31;   /* the round header buttons: lifted off the paper */
  --bench: var(--level-1);
  --ink: #ebe9e4;
  --ink-2: #9b9a95;
  --ink-3: #5f5e5a;
  --line: rgba(235, 233, 228, 0.12);
  --line-2: rgba(235, 233, 228, 0.32);
  /* panels — the white boxes; the instrument's readouts and states live on them */
  --panel: #ebe9e4;
  --panel-ink: #0a0a0b;
  --panel-ink-2: #6b6b68;
  --panel-shadow: none;
  --model-ink: #c9c8c3;
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.6' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-opacity: 0.55;
  --grain-flat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.6' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 .9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");   /* white speckle for the dark paper — the overlay layer cannot show on near-black */
  --grain-flat-opacity: 0.09;

  --sans: "Hanken Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  --mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --gutter: clamp(16px, 2.2vw, 36px);
  /* the bar's height, and how far everything clears it. the safe area (the Dynamic Island) is
     not the bar's problem: html/body pad the whole document down by it, the way
     teenage.engineering does, so the bar is an ordinary thing at the top of the content and the
     lot — bar included — scrolls up under the island and Safari's bar as one piece. */
  --safe-top: env(safe-area-inset-top, 0px);
  --bar-h: 52px;

  /* the two registers — very far apart, almost nothing in between */
  --t-micro: 10px;
  --t-mono: 11px;
  --t-ui: 12px;
  --t-body: 13px;
  --t-editorial: clamp(16px, 1.25vw, 20px);   /* the one statement size: landing statement, store lede, about paragraphs */
  --t-title: clamp(44px, 7.6vw, 156px);
  /* the name fills the measure exactly: LOUIE JACOB at 600/-0.045em is 5.72em wide */
  /* the name fills the measure exactly: ink from gutter to gutter (box 5.72em, ink 5.584em).
     --lw is the layout width set by JS (viewport minus any scrollbar); 100vw until then */
  --lw: 100vw;
  --name-em: 5.584;   /* the name's ink width in em for the face in use — slate.js measures and overwrites this */
  /* the name is sized to span gutter to gutter — its ink meets both margins at every width.
     no px cap: a width cap in a width-driven formula can only leave the right margin short
     (98px short at 2560px with the old 430px cap). the ceiling is height, below. */
  --t-name: max(40px, calc((var(--lw) - 2 * var(--gutter)) / var(--name-em)));
  --t-mail: clamp(30px, 6.9vw, 148px);

  --track-micro: 0.11em;
  --track-title: -0.055em;   /* large text sits a touch tighter (Louie, 2026-08-16) */
  --track-name: -0.06em;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-io: cubic-bezier(0.83, 0, 0.17, 1);
  --dur: 0.6s;

  color-scheme: dark;
}

/* the face on trial: Helvetica Neue (Helvetica, Arial behind it), set tighter. `?face=hanken` brings Hanken back. */
[data-face="helvetica"] {
  --sans: "Helvetica Neue", Helvetica, Arial, sans-serif;
  --track-title: -0.06em;
  --track-name: -0.06em;
  --track-micro: 0.09em;
}
[data-face="helvetica"] .hero__name { font-weight: 700; }
[data-face="helvetica"] .title, [data-face="helvetica"] .item__title, [data-face="helvetica"] .tile__title, [data-face="helvetica"] .foot__mail, [data-face="helvetica"] .about__clients li { letter-spacing: -0.05em; }
[data-face="helvetica"] .bar__nav, [data-face="helvetica"] .bar__side { letter-spacing: 0; }
[data-face="helvetica"] .hero__statement, [data-face="helvetica"] .editorial { letter-spacing: -0.02em; }

[data-theme="light"] {
  --paper: #f2f2f0;
  --level-1: #e9e9e6;
  --level-2: #dfdfdc;
  --control: #e4e4e0;
  --bench: var(--level-1);
  --ink: #101012;
  --ink-2: #6b6b68;
  --ink-3: #9d9d99;
  --line: rgba(16, 16, 18, 0.12);
  --line-2: rgba(16, 16, 18, 0.32);
  --panel: #ffffff;
  --panel-ink: #101012;
  --panel-ink-2: #7a7a76;
  --panel-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 32px rgba(0, 0, 0, 0.06);
  --model-ink: #3a3a3c;
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.6' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-opacity: 0.5;
  --grain-flat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.6' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");   /* dark speckle for the light paper */
  --grain-flat-opacity: 0.07;
  color-scheme: light;
}
/* no override: the paper follows the system — decided in CSS, so a prerendered page cannot get it wrong */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
  --paper: #f2f2f0;
  --level-1: #e9e9e6;
  --level-2: #dfdfdc;
  --control: #e4e4e0;
  --bench: var(--level-1);
  --ink: #101012;
  --ink-2: #6b6b68;
  --ink-3: #9d9d99;
  --line: rgba(16, 16, 18, 0.12);
  --line-2: rgba(16, 16, 18, 0.32);
  --panel: #ffffff;
  --panel-ink: #101012;
  --panel-ink-2: #7a7a76;
  --panel-shadow: 0 1px 2px rgba(0, 0, 0, 0.04), 0 12px 32px rgba(0, 0, 0, 0.06);
  --model-ink: #3a3a3c;
  --grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.6' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  --grain-opacity: 0.5;
  --grain-flat: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.6' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 .9 0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");   /* dark speckle for the light paper */
  --grain-flat-opacity: 0.07;
  color-scheme: light;
  }
}

/* ── Reset ──────────────────────────────────────────────────────────────── */

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; scroll-behavior: smooth; background: var(--paper); }
/* the document begins below the Dynamic Island, and does not rubber-band at the top — the two
   things that make a page scroll up under the island and Safari's bar as one piece, header and
   all (teenage.engineering's own recipe; Louie, 2026-08-18) */
html { padding-top: var(--safe-top); }   /* on html only: padding both doubled it and let the one-screen reel scroll by the island's height */
/* no rubber-band at the top — but ONLY on a touch screen. on a desktop, overscroll-behavior-y:none
   on the root makes WebKit refuse to wheel-scroll the document at all: in grid and index view the
   trackpad did nothing (bisected 2026-08-18 — this one line, on its own). the rubber-band it
   suppresses is a touch thing anyway. */
@media (pointer: coarse) { html, body { overscroll-behavior-y: none; } }
/* the reel page on a phone: vertical pans are ours (lab.js turns them into sideways travel), the
   horizontal ones and pinch stay the browser's */
@media (pointer: coarse) { html.lab:not([data-view]) body { touch-action: pan-x pinch-zoom; } }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 var(--t-body) / 1.45 var(--sans);
  font-feature-settings: "ss01" 0, "kern" 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  min-height: 100svh;
  overflow-x: clip;
  display: flex;
  flex-direction: column;
}
main { flex: 1 0 auto; }
.foot { flex: 0 0 auto; }
img, picture, video, canvas, iframe { display: block; max-width: 100%; }
img { height: auto; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
h1, h2, h3, p, dl, dd, figure, ol, ul { margin: 0; }
ol, ul { padding: 0; list-style: none; }
::selection { background: var(--ink); color: var(--paper); }
:focus-visible { outline: 1px solid var(--ink); outline-offset: 4px; }
[hidden] { display: none !important; }

/* film grain — static, no blend, no WebGL, cannot fail */
/* no grain — removed site-wide (Louie, 2026-08-16) */

/* ── Type registers ─────────────────────────────────────────────────────── */

/* every mono value on the site shares one voice: 11px, weight 450, +0.02em, tabular figures */
.mono, .readout, .slate, .facts, .ledger, .bar, .foot__ledger, .btn, .part, .cart,
.tile__price, .bag__summary .row, .about__sign, .about__sheet-list, .about__sheet-facts > div, .variants__value, .credits dt, .credits h3 {
  font-family: var(--mono);
  font-size: var(--t-mono);
  font-weight: 450;
  letter-spacing: 0.02em;
  font-variant-numeric: tabular-nums;
}
/* every mono label: 10px caps, +0.11em */
.readout, .k, .bar, .btn, .slate__n, .slate__meta, .part__fits, .part__state, .cart__k, .foot__ledger, .facts dt, .credits h3 {
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-2);
}

.title {
  font-family: var(--sans);
  font-size: var(--t-title);
  font-weight: 500;
  line-height: 0.92;
  letter-spacing: var(--track-title);
  text-wrap: balance;
  color: var(--ink);
  margin-left: -0.07em;   /* optical: the cap's side-bearing, so the huge sits on the small text's edge */
}

/* the artist rides just after the title at a fraction of its size: the work is the title, the
   artist is whose it is. Sized in em so it tracks the title at every width, with a floor so it
   never shrinks past reading size on a phone. */
.title__artist {
  display: block;            /* narrow: its own line, flush with the facts below it */
  /* the title is nudged left by 0.07em of its own size so the huge cap lines up optically;
     small text should not be, so that nudge is given back — in the title's units, not this
     element's, or the correction would be a fifth of the size it needs to be */
  margin: 0.5em 0 0 calc(0.07 * var(--t-title));
  font-size: max(14px, 0.2em);
  font-weight: 500;
  line-height: 1.15;
  letter-spacing: 0;
  color: var(--ink-2);
  text-wrap: balance;
}
@media (min-width: 720px) {
  /* wide: it rides on the title's own line, a fraction of its size */
  .title__artist { display: inline-block; margin: 0 0 0 0.95em; }
}
.editorial {
  font-size: var(--t-editorial);
  line-height: 1.42;
  letter-spacing: -0.005em;
  font-weight: 400;
  text-wrap: pretty;
}
.readout { display: flex; gap: 1.6em; flex-wrap: wrap; }
.readout b { font-weight: 450; color: var(--ink); }
.dim { color: var(--ink-3); }

/* links that are words: no motion on hover, just the ink coming up */
.swap { display: inline-block; vertical-align: top; }
.swap > span { display: block; }

/* ── Top bar ────────────────────────────────────────────────────────────── */

.bar {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 100;
  height: var(--bar-h);
  padding: 0 var(--gutter);
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  color: var(--ink);
  /* no view-transition-name: a frosted (backdrop-filter) element captured as its own
     snapshot has no backdrop to sample and can flash opaque. It rides in the root
     snapshot instead — it looks the same on both pages, so the cross-fade is invisible. */
}
/* paper ground that dissolves below the bar so content slides under, never collides */
/* the bar is a see-through box: a hard-edged, frosted band the page runs under.
   The blur sits on the bar itself, not a pseudo-element — some engines drop a
   backdrop-filter on a negative-z pseudo inside a fixed, transitioned element. */
/* the bar is the paper's colour, with the page blurred through it — a hard edge, no gradient */
.bar {
  /* glass, not paper: what scrolls under the bar shows through it — the point of a frosted bar.
     it was 68% paper and read as a solid strip over the pictures (Louie, 2026-08-18) */
  background: color-mix(in srgb, var(--paper) 45%, transparent);
  -webkit-backdrop-filter: blur(16px) saturate(1.2);
  backdrop-filter: blur(16px) saturate(1.2);
}
@supports not (backdrop-filter: blur(1px)) { .bar { background: var(--paper); } }
/* on a page that scrolls, the bar sits at the top of the document and goes with it — absolute,
   not fixed — so once you scroll the pictures own the whole screen, up behind the Dynamic Island
   and Safari's bar (viewport-fit=cover lets them). only the landing reel, which does not scroll,
   keeps the bar fixed. out of flow either way, so everything that clears it by --bar-h still does.
   (Louie, 2026-08-18) */
body:not(.lab) .bar, :root[data-view="grid"] body.lab .bar { position: absolute; top: var(--safe-top); }
/* the reel's fixed bar sits below the island too — the document's padding does not move a fixed thing */
body.lab .bar { top: var(--safe-top); }
/* index view, on every page: the bar stays and the list scrolls under it — a list wants its
   controls in reach the whole way down, and the toggle back out of index view is in the bar.
   the frost is what the rows show through. (Louie, 2026-08-18) */
:root[data-view="index"] .bar { position: fixed; top: var(--safe-top); }
/* on a phone the bar is paper, not glass: with the bar staying put (reel and index views) and
   nothing of the page yet passing behind the island, a see-through bar showed a strip of rows
   between the island and itself and frost over plain paper elsewhere. solid — and where it is
   fixed it runs up from the very top, island and all, one block; the row of controls keeps its
   place under the safe area. (Louie, 2026-08-18) */
@media (max-width: 719px) {
  .bar { background: var(--paper); -webkit-backdrop-filter: none; backdrop-filter: none; }
  :root:not([data-view="grid"]) body.lab .bar, :root[data-view="index"] .bar {
    top: 0; padding-top: var(--safe-top); height: calc(var(--bar-h) + var(--safe-top));
  }
  /* (index view was tried as glass here for a moment, and grid view with the bar fixed and glass —
     both undone the same evening: paper, and in grid view the bar scrolls away. Louie, 2026-08-18) */
}


.bar .k, .bar .readout { color: var(--ink-2); }
.bar__mark {
  display: inline-flex; align-items: center;
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  justify-self: start;
  color: var(--ink);
}
.bar__nav, .bar__side {
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.01em;
  text-transform: capitalize;   /* Work · Store · About — first letter up, the rest as written */
}
.bar__nav { display: flex; gap: clamp(18px, 2.4vw, 40px); }
.bar__nav a { position: relative; padding: 6px 0; color: var(--ink-2); transition: color 0.3s var(--ease); }
.bar__nav a[aria-current="page"], .bar__nav a:hover { color: var(--ink); }
.bar__index, .bar__bag { color: var(--ink-2); }
/* the current destination is simply full ink; the others sit slightly grey */
.bar__side { justify-self: end; display: flex; gap: 12px; align-items: center; }   /* the same 12px as inside the cluster, so bag · index · paper are evenly spaced */
/* the round controls: grid (landing only) · index · paper — each its own, with air between */
.bar__cluster { display: inline-flex; gap: 12px; align-items: center; }

/* the bag: a round control like its neighbours, with the count as a small ink badge riding the
   top-right edge. hidden until something is in it (shop.js), so an empty store page shows nothing. */
.bar__bag { position: relative; overflow: visible; }
.bar__bag em {
  position: absolute; top: -3px; right: -4px;
  min-width: 15px; height: 15px; padding: 0 4px; box-sizing: border-box;
  display: inline-grid; place-items: center;
  border-radius: 999px;
  background: var(--ink); color: var(--paper);
  font-family: var(--mono); font-style: normal; font-size: 8.5px; font-weight: 500; letter-spacing: 0.02em; line-height: 1;
}
@media (hover: hover) { .bar__bag:hover { color: var(--ink); } }
/* the two round controls: index and paper */
.bar__btn {
  display: inline-grid; place-items: center;
  width: 28px; height: 28px;
  border-radius: 50%;
  background: var(--control);
  color: var(--ink-2);
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}
.bar__btn svg { width: 12px; height: 12px; display: block; }
.bar__btn svg rect:not([fill]), .bar__btn svg path:not([fill]):not([stroke]) { fill: currentColor; }
.bar__btn:hover { color: var(--ink); background: var(--line); }
.bar__btn[aria-pressed="true"] { background: var(--ink); color: var(--paper); }


/* ── Page frame ─────────────────────────────────────────────────────────── */

.page {
  padding: 0 var(--gutter);
}
main { display: block; }

/* the panel — a white box; readouts, states and controls sit on it */
.panel {
  background: var(--panel);
  color: var(--panel-ink);
  box-shadow: var(--panel-shadow);
}
.panel .k, .panel .readout, .panel dt { color: var(--panel-ink-2); }

/* ── Hero (home) ────────────────────────────────────────────────────────── */

.hero {
  min-height: 100svh;
  padding: calc(var(--bar-h) + var(--gutter) * 1.5) var(--gutter) var(--gutter);
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 8vh;
}
.hero__intro { display: grid; gap: 16px; max-width: 40ch; }
.hero__statement b { font-weight: 700; color: var(--ink); }
.hero__statement {
  font-size: var(--t-editorial);
  line-height: 1.35;
  letter-spacing: -0.012em;
  color: var(--ink);
  text-wrap: pretty;
}
.hero__time { gap: 1.2em; }
.hero__time span:last-child { color: var(--ink); }
.hero__name {
  font-family: var(--sans);
  font-size: var(--t-name);
  font-weight: 800;   /* Hanken runs to 900; the name sits two steps above the titles (500) for hierarchy */
  line-height: 0.76;
  letter-spacing: var(--track-name);
  margin: 0 -0.063em 0 -0.071em;   /* optical: the L's and B's side-bearings, so the ink sits on both gutters */
  white-space: nowrap;
  color: var(--ink);
}
.hero__name span { display: inline-block; }
body.lab .hero__name > span { pointer-events: auto; cursor: pointer; }   /* the letters take a click (back to the start); the line itself does not */

/* ── Feed (home) ────────────────────────────────────────────────────────── */

.feed {
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: clamp(88px, 16vh, 220px);
  padding: clamp(64px, 12vh, 160px) var(--gutter) 0;
}
.frame { grid-column: 1 / -1; min-width: 0; }
.frame__body {
  --cap: 82svh;
  width: min(100%, calc(var(--cap) * var(--r)));
}
.frame--right .frame__body { margin-left: auto; }
@media (min-width: 720px) {
  .frame[data-orient="landscape"]:nth-child(4n + 1) { grid-column: 1 / span 9; }
  .frame[data-orient="landscape"]:nth-child(4n + 2) { grid-column: 4 / -1; }
  .frame[data-orient="landscape"]:nth-child(4n + 3) { grid-column: 3 / span 8; }
  .frame[data-orient="landscape"]:nth-child(4n + 4) { grid-column: 1 / span 8; }
  .frame[data-orient="portrait"]:nth-child(4n + 1) { grid-column: 2 / span 5; }
  .frame[data-orient="portrait"]:nth-child(4n + 2) { grid-column: 7 / span 5; }
  .frame[data-orient="portrait"]:nth-child(4n + 3) { grid-column: 4 / span 5; }
  .frame[data-orient="portrait"]:nth-child(4n + 4) { grid-column: 8 / span 5; }
  .frame[data-orient="portrait"] .frame__body { --cap: 88svh; }
}
@media (max-width: 719px) {
  .frame__body { --cap: 72svh; }
  .frame--right .frame__body { margin-left: auto; }
}

/* the slate — clapper header above every picture */
.slate {
  display: grid;
  grid-template-columns: 3ch minmax(0, 1fr) auto;
  gap: 1.5em;
  align-items: baseline;
  padding: 0 0 12px;
  color: var(--ink-2);
}
.slate__title {
  font-family: var(--sans);
  font-size: var(--t-ui);
  font-weight: 500;
  letter-spacing: 0;
  text-transform: none;
  color: var(--ink);
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.slate__meta { text-align: right; white-space: nowrap; }
.slate__meta .arrow { display: inline-block; margin-left: 0.8em; transition: transform 0.5s var(--ease); }
.frame__body a:hover .slate__meta .arrow { transform: translateX(0.35em); }
@media (max-width: 719px) {
  .slate { grid-template-columns: 3ch minmax(0, 1fr); gap: 4px 1.5em; }
  .slate__meta { grid-column: 2; text-align: left; white-space: normal; }
  .feed { row-gap: clamp(56px, 9vh, 96px); }
}

/* the plate — the picture, whole, at its own ratio */
.plate { display: block; position: relative; aspect-ratio: var(--r); background: var(--bench); }
.plate img { width: 100%; height: 100%; object-fit: contain; }
.frame__body > a { display: block; color: inherit; position: relative; }
.plate__title {
  display: block; padding-top: 10px;
  font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; letter-spacing: 0;
  color: var(--ink-2);
  opacity: 0; transition: opacity 0.4s var(--ease);
}
@media (hover: none) { .plate__title { opacity: 1; } }
@media (hover: hover) { .frame__body > a:hover .plate__title { opacity: 1; } }

/* ── Ledger (index mode) ────────────────────────────────────────────────── */

/* the index list is its own surface: a full-width band, a step off the paper */
.ledger {
  display: none;
  padding: calc(var(--bar-h) + var(--gutter) * 2) var(--gutter) var(--gutter);
}
.about .ledger, .work .ledger { padding: var(--gutter); }
.ledger__head, .ledger li > a, .ledger li > div {
  display: grid;
  /* the type sits next to the title rather than across the page from it, so a row reads as
     one thing: what it is called, whose it is, what it is */
  grid-template-columns: 3ch minmax(0, 2.2fr) minmax(0, 1.1fr) minmax(0, 1.2fr) 11ch;
  gap: 1.5em;
  align-items: baseline;
  padding: 11px 0;
}
.ledger__head { color: var(--ink-3); font-size: var(--t-mono); padding-bottom: 14px; }
.ledger__head > span { font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro); }
.ledger li > a:hover { color: var(--ink); }
.ledger .t {
  font-family: var(--sans); font-size: var(--t-ui); font-weight: 600; letter-spacing: -0.01em; color: var(--ink);
  display: inline-flex; align-items: baseline; gap: 0.6em; min-width: 0;
}
.ledger .t__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* whose work it is, said in the title's own line: the name in full, the join left quiet */
.ledger .by { font-weight: 400; color: var(--ink-3); }
.ledger .t__artist { font-weight: 400; color: var(--ink-2); }
/* film or photographs, told apart at a glance: a play triangle, or a frame */
.kind { flex: none; width: 8px; height: 8px; align-self: center; color: var(--ink-3); transition: color 0.2s; }
.kind--film { clip-path: polygon(0 0, 100% 50%, 0 100%); background: currentColor; }
.kind--photo { border: 1.5px solid currentColor; border-radius: 1.5px; }
/* a part is solid; a file is an outline of one — the same distinction the store makes */
.kind--part { background: currentColor; border-radius: 1.5px; }
.kind--file { border: 1.5px dashed currentColor; border-radius: 1.5px; }
.ledger li > a:hover .kind { color: var(--ink-2); }
/* the kind is a shape for the eye; screen readers get the word instead */
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
.ledger .r { text-align: right; white-space: nowrap; overflow: visible; }
.ledger .m { color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (hover: hover) { .ledger li > a:hover .t { color: var(--ink-2); } }
@media (max-width: 719px) {
  .ledger__head, .ledger li > a, .ledger li > div { grid-template-columns: 3ch minmax(0, 1fr) 11ch; }
  .ledger .m:nth-child(3), .ledger .m:nth-child(4),
  .ledger__head span:nth-child(3), .ledger__head span:nth-child(4) { display: none; }
}
.ledger--plain li > div { grid-template-columns: 3ch minmax(0, 1fr); }
/* the store's list carries a control column: add to cart, right there */
/* the store's list reads like the work index: what it is called sits next to what it is */
.ledger--store .ledger__head, .ledger--store li > div { grid-template-columns: 3ch minmax(0, 1.7fr) minmax(0, 1.2fr) minmax(0, 1.1fr) 11ch minmax(120px, auto); align-items: center; }
.ledger--store li > div { padding: 8px 0; }
.ledger--store .t { color: var(--ink); }
@media (hover: hover) { .ledger--store .t:hover { color: var(--ink-2); } }
.ledger--store .c { justify-self: end; }
.ledger--store .c .btn { padding: 9px 12px 8px; }
@media (max-width: 719px) {
  .ledger--store .ledger__head, .ledger--store li > div { grid-template-columns: 3ch minmax(0, 1fr) auto; }
  .ledger--store .m { display: none; }
}

/* index mode: one control, every page */
[data-view="index"] .feed,
[data-view="index"] .hero__name,
[data-view="index"] .work__media,
[data-view="index"] .work__grid,
[data-view="index"] .work__stills,
[data-view="index"] .work__next,
[data-view="index"] .tiles,
[data-view="index"] .store__lede,
[data-view="index"] .about__editorial,
[data-view="index"] .about__sign,
[data-view="index"] .about__clients { display: none; }
[data-view="index"] .ledger { display: block; }
.js[data-view="index"] .ledger li { animation: fade-up 0.7s var(--ease) both; animation-delay: calc(0.08s + var(--i, 0) * 28ms); }
.js[data-view="index"] .ledger__head { animation: fade 0.5s var(--ease) both; }
[data-view="index"] .hero { min-height: 0; }
/* the landing in index mode: the strip and the name step aside, the page scrolls, the ledger is the page */
:root[data-view="index"] body.lab { height: auto; overflow-y: visible; }   /* same: the document scrolls; overflow-y only, so the reset's overflow-x: clip stays */
:root[data-view="index"] .labstage { height: auto; display: block; padding-bottom: 0; }
:root[data-view="index"] .reel { display: none; }
/* the band begins exactly where the pictures begin in reel view: the stage drops its
   bottom padding in index mode, so one stage gap puts the band's edge on the strip's edge */
:root[data-view="index"] body.lab .ledger {
  margin-top: clamp(16px, 2.6vh, 32px);   /* the list begins where the pictures begin */
  padding: 0 var(--gutter);
}
/* the name is fixed at the foot in index view, so the page — not the band — carries the room for
   it: as much as the fade band below is tall (its full paper through the name and its fade above),
   so the last rows can scroll clear of it — two stayed under the name (Louie, 2026-08-18) */
:root[data-view="index"] body.lab main { padding-bottom: calc(var(--t-name) * 0.76 + var(--gutter) * 2 + 88px); }
/* on the landing the name stays through index view too — anchored to the foot, the ledger runs under it */
:root[data-view="index"] body.lab .hero__name {
  display: block;
  position: fixed; left: var(--gutter); right: var(--gutter); bottom: var(--gutter);
  z-index: 5; pointer-events: none;
  color: #fff; mix-blend-mode: difference;
}
/* the rows fade away as they reach the name: a band fixed at the foot, paper to transparent, the
   name's height and a little more, under the name (which still inverts against it) and over the
   ledger. it says the list goes on, and the name floats on a fade rather than on cut-off rows.
   desktop only — on a phone the name sits below the list. (Louie, 2026-08-18) */
@media (min-width: 720px) {
  :root[data-view="index"] body.lab::after {
    content: ""; position: fixed; left: 0; right: 0; bottom: 0; z-index: 4; pointer-events: none;
    /* paper the whole height of the name and a little above it, then the fade — the rows were
       still readable behind the letters (Louie, 2026-08-18) */
    height: calc(var(--t-name) * 0.76 + var(--gutter) * 2 + 88px);
    background: linear-gradient(0deg, var(--paper) 0, var(--paper) calc(var(--gutter) + var(--t-name) * 0.76 + 12px), transparent 100%);
  }
}
/* on a phone the fixed name is a fifth of the screen and sits on top of the rows — so it comes
   off the glass and follows the list instead, in ink, the same width the list uses */
@media (max-width: 719px) {
  :root[data-view="index"] body.lab main { padding-bottom: 0; display: flex; flex-direction: column; }
  /* the stage dissolves so its children sit beside the ledger in main, and the name can be
     ordered after it — the DOM stays as it is for the desktop overlay */
  :root[data-view="index"] body.lab .labstage { display: contents; }
  :root[data-view="index"] body.lab .hero__intro { order: 0; padding: calc(var(--bar-h) + var(--gutter)) var(--gutter) 0; }
  :root[data-view="index"] body.lab .ledger { order: 1; }
  :root[data-view="index"] body.lab .hero__name {
    order: 2;
    position: static;
    /* vertical only — the horizontal is the name's own optical margins, inside a gutter box */
    margin-block: calc(var(--gutter) * 2) var(--gutter);
    padding-inline: var(--gutter);
    box-sizing: border-box;
    color: var(--ink); mix-blend-mode: normal;
    pointer-events: auto;
  }
}

/* ── Work (project page) ────────────────────────────────────────────────── */

/* back sits above the readout, at the weight of the small text around it */
.backlink {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--ink-2);
  transition: color 0.2s;
}
.backlink:hover { color: var(--ink); }
.backlink__arrow { transition: translate 0.2s; }
.backlink:hover .backlink__arrow { translate: -3px 0; }
@media (prefers-reduced-motion: reduce) { .backlink__arrow { transition: none; } }

/* a project says what it is in a sentence, in the same voice as the landing statement: the
   title carries the weight, whose it is carries the weight, the rest is said plainly */
.work__statement {
  font-family: var(--sans);
  font-size: var(--t-editorial);
  font-weight: 700;
  line-height: 1.35;
  letter-spacing: -0.012em;
  color: var(--ink);
  text-wrap: pretty;
  max-width: 46ch;
  margin: 0;
}
.work__statement .by { font-weight: 400; color: var(--ink-2); }
/* what it is, on its own line under whose it is */
.work__says { display: grid; gap: 0.35em; }
.work__tail {
  font-family: var(--sans);
  /* a step under the statement above it: enough that the eye reads two things, not one
     sentence that changed colour halfway */
  font-size: calc(var(--t-editorial) * 0.86);
  font-weight: 400;
  line-height: 1.35;
  letter-spacing: -0.012em;
  color: var(--ink-2);
  max-width: 46ch;
  margin: 0;
}

.work__head {
  /* the same top as the landing: bar + one gutter. The foot is the film's room to breathe —
     the title should not sit on top of the frame below it. */
  padding: calc(var(--bar-h) + var(--gutter)) var(--gutter) clamp(20px, 3.2vh, 44px);
  display: grid;
  gap: var(--gutter);
}
.work__head .backlink { margin-bottom: 0; }   /* one gutter to the title: pulled closer it sat on the title and the artist (Louie, 2026-08-18) */
.facts {
  display: flex; flex-wrap: wrap;
  gap: 12px clamp(28px, 4vw, 64px);
}
.facts > div { display: grid; gap: 6px; align-content: start; }
.facts dt { color: var(--ink-3); }
/* the value reads as text, not as another label: sans ui 500 — the rule the project credits
   already follow (`.credits dd`), so price / status / options match CLIENT / FORMAT / YEAR */
.facts dd { font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink); }
/* on a phone the price is the thing you came for; a step up from ui, still under the body */
@media (max-width: 719px) { .facts dd { font-size: 14px; } }

.work__media {
  padding: var(--gutter) var(--gutter) 0;
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
  row-gap: clamp(20px, 3vw, 44px);
}
.work__media > figure { grid-column: 1 / -1; min-width: 0; margin: 0; }
.work__media > figure > .plate,
.work__media > figure > .player { width: min(100%, calc(var(--cap, 90svh) * var(--r))); }
.work__media > figure.is-centre > .plate { margin-inline: auto; }
.work__media > figure.is-right > .plate { margin-left: auto; }
.work__media figcaption {
  font-family: var(--mono);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
  color: var(--ink-2);
  padding-top: 10px;
  width: min(100%, calc(var(--cap, 90svh) * var(--r)));
}
.work__media > figure.is-centre figcaption { margin-inline: auto; }
.work__media > figure.is-right figcaption { margin-left: auto; }
@media (min-width: 720px) {
  .work__media > figure.span-half { grid-column: span 6; }
  .work__media > figure.span-half > .plate, .work__media > figure.span-half figcaption { --cap: 82svh; }
  .work__media > figure.span-inset { grid-column: 3 / span 6; }
  .work__media > figure.span-inset-right { grid-column: 6 / -1; }
  .work__media > figure.span-inset > .plate, .work__media > figure.span-inset-right > .plate,
  .work__media > figure.span-inset figcaption, .work__media > figure.span-inset-right figcaption { --cap: 88svh; }
}

/* film — poster, whole; play on demand; nothing loads until asked */
.player { position: relative; aspect-ratio: var(--r); background: var(--bench); }
.player img { width: 100%; height: 100%; object-fit: contain; }
.player video, .player iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; background: #000; }
.player .play {
  position: absolute; inset: 0;
  display: grid; place-items: center;
  color: #fff;
  font-family: var(--mono);
  font-size: var(--t-micro);
  text-transform: uppercase;
  letter-spacing: var(--track-micro);
}
.player .play > span {
  display: inline-grid; grid-auto-flow: column; gap: 1.2em; align-items: center;
  padding: 16px 22px;
  background: #ebe9e4; color: #0a0a0b;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
  transition: transform 0.5s var(--ease), background 0.4s var(--ease), color 0.4s var(--ease);
}
.player .play > span::before { content: ""; width: 0; height: 0; border-style: solid; border-width: 5px 0 5px 8px; border-color: transparent transparent transparent currentColor; }
@media (hover: hover) { .player .play:hover > span { background: #fff; transform: scale(1.04); } }
.player.is-live .play { display: none; }

/* stills — a compact grid under the credits on film pages */
.work__stills {
  padding: var(--gutter) var(--gutter) 0;   /* one gutter after the credits, one gutter between the grabs */
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 340px), 1fr));
  gap: var(--gutter);
}
.work__stills figure { margin: 0; min-width: 0; }
/* every frame grab the same shape — 16:9, the masters' own — so the strip is an even grid, not
   a ragged one. the odd 4:3 or scope frame is cropped to fit here; the lightbox shows it whole,
   in its own ratio, because it reads --r from the anchor. (Louie, 2026-08-18) */
.work__stills .plate { aspect-ratio: 16 / 9 !important; }
/* the picture out of flow, so the box's ratio is the box's — in flow the img's own 4:3 height
   won the box back and aspect-ratio did nothing */
.work__stills .plate picture { position: absolute; inset: 0; display: block; }
.work__stills .plate img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.js .work__stills figure[data-reveal] .plate img { clip-path: inset(100% 0 0 0); }
.js .work__stills figure[data-reveal].in .plate img { clip-path: inset(0); transition: clip-path 1s var(--ease); }

/* photo projects: one hero, then the plates packed under the landing's rules */
.work__hero { margin: 0; padding: 0; position: relative; }   /* .work__media already carries the gutter */

/* the lightbox — the stills, over the page, as a strip you swipe. the hero stays put; the layer
   fades in over everything; every still sits in a track that scroll-snaps one to a screen, so on
   a phone you drag the picture away and it settles on the next — the way Photos does. */
/* the lightbox leaves no footprint while closed: display:none takes it out of compositing
   entirely, and the frost is applied only while open. a fixed element with backdrop-filter that
   merely sits in the DOM (hidden or not) is one of the things that stops iOS Safari collapsing
   its bars — the pages with a lightbox were the pages where the island did not work. */
/* inset 1px 0, not 0: a fixed element flush with the top and bottom of the screen is what iPhone
   Safari (iOS 26) paints its bars opaque over — the moment the box opened, a paper band stood
   behind the island and behind the bottom bar, gone at close (Louie's recording, 2026-08-18
   22:43). so the box runs two pixels PAST each edge — not flush, and nothing of the page shows
   at the top or the bottom (a pixel inside the edge left the blur short of the screen: Louie).
   and 88% paper, not 82: where Safari draws its bars it does not run our blur, so the paper has
   to carry more of the cover on its own. */
.lightbox { display: none; position: fixed; inset: -2px 0; z-index: 200; background: color-mix(in srgb, var(--paper) 88%, transparent); opacity: 0; transition: opacity 0.28s var(--ease); cursor: zoom-out; }
.lightbox:not([hidden]) { display: block; }
/* the flying copy of the picture, between the still and the lightbox's frame (stage.js) */
.lightbox__zoom { position: fixed; z-index: 210; overflow: hidden; pointer-events: none; background: var(--bench); will-change: transform, clip-path; }
.lightbox__zoom img { display: block; width: 100%; height: 100%; object-fit: cover; }   /* the box is the whole picture's shape at rest; cover == contain there */
.lightbox.is-flying .lightbox__frame img { visibility: hidden; }   /* the frame's own picture waits under the flight — one picture, not two */
.lightbox:not(.is-open) { pointer-events: none; }   /* a click during the fade-out must not land on an arrow of a box that is going */
.lightbox.is-open { opacity: 1; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
.lightbox__track {
  position: absolute; inset: 0;
  display: flex;
  overflow-x: auto; overflow-y: hidden;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
  outline: none;
  touch-action: pan-x;   /* one finger pans the strip; two fingers are ours (the pinch), and the page never zooms */
}
.lightbox__track::-webkit-scrollbar { display: none; }
/* the foot of the lightbox is spoken for: the pill (its offset, its height, a breath) and the
   caption line above it. the picture is centred in what is left ABOVE that, so it sits a little
   high and never runs under the caption — it did, on a phone on its side with Safari's bar
   showing (Louie, 2026-08-18). one measure, used by the slide's padding and the frame's cap. */
.lightbox { --lb-top: var(--gutter); --lb-foot: calc(clamp(14px, 2vh, 24px) + 36px + 14px + 40px); }   /* pill offset + pill + a breath + the caption line and air above it */
.lightbox:has(.lightbox__caption[hidden]) { --lb-foot: calc(clamp(14px, 2vh, 24px) + 36px + 14px); }   /* no caption (a project's own pictures): only the pill's room */
/* a phone on its side: as big as the picture can be and still be whole — a slim margin above, the
   pill's room below, no more (Louie, 2026-08-18) */
@media (max-height: 480px) and (orientation: landscape) {
  .lightbox { --lb-top: 10px; --lb-foot: calc(12px + 36px + 8px + 30px); }
  .lightbox:has(.lightbox__caption[hidden]) { --lb-foot: calc(12px + 36px + 8px); }
  .lightbox__bar { bottom: 12px; }
  .lightbox__caption { bottom: calc(12px + 36px + 8px); }
}
.lightbox__slide {
  flex: none; width: 100%; height: 100%; margin: 0;
  scroll-snap-align: start; scroll-snap-stop: always;
  display: grid; place-items: center;
  padding: var(--lb-top) var(--gutter) var(--lb-foot);
  box-sizing: border-box;
}
/* the frame is the picture's shape, capped to the screen on both axes, so the outside is exactly
   outside — a portrait stands tall, a landscape lies wide, neither is a slab in the other's box */
.lightbox__frame {
  margin: 0; position: relative;
  aspect-ratio: var(--r, 3 / 2);
  /* dvh, not svh: the box is open in whichever state Safari's bars are in, and does not scroll,
     so the height that is there is the height to use — with the bars away the picture is bigger */
  width: min(100%, calc((100dvh - var(--lb-top) - var(--lb-foot)) * var(--r, 1.5)));
  max-height: calc(100dvh - var(--lb-top) - var(--lb-foot));
  cursor: default;
}
.lightbox__frame.is-pinching { will-change: transform; }   /* a layer of its own only while a pinch is under way — a standing will-change on every frame is a standing compositing cost */
.lightbox__frame picture { position: absolute; inset: 0; display: block; }
.lightbox__frame img { width: 100%; height: 100%; display: block; object-fit: contain; }
.lightbox__bar {
  position: fixed; left: 50%; translate: -50% 0; bottom: clamp(14px, 2vh, 24px);
  display: flex; align-items: center; gap: 6px; padding: 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--level-1) 76%, transparent);   /* a step off the paper — a shade darker in light, a shade brighter in dark — so the pill reads on the picture behind it (Louie, 2026-08-18) */
  cursor: default;
}
.lightbox.is-open .lightbox__bar { backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
/* the bar's four things — ←, count, →, and on the landing "project →" — on one row, always: one
   font, one size, nowrap on every piece. on an iPhone the count and the link each broke onto two
   lines inside the pill (Louie, 2026-08-18). close is gone: the dark outside the picture closes. */
.lightbox__bar > * { white-space: nowrap; flex: none; }
.lightbox__step, .lightbox__goto {
  border: 0; border-radius: 999px; background: transparent;
  color: var(--ink-2);
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: var(--track-micro); text-transform: uppercase;
  cursor: pointer; text-decoration: none;
  transition: color 0.18s, background 0.18s;
}
.lightbox__step { width: 26px; height: 26px; display: grid; place-items: center; }
.lightbox__goto { padding: 0 12px; height: 26px; display: inline-grid; place-items: center; color: var(--ink); }
.lightbox__step:hover, .lightbox__goto:hover { color: var(--paper); background: var(--ink); }
.lightbox__count { font-family: var(--mono); font-size: var(--t-micro); letter-spacing: var(--track-micro); color: var(--ink-3); padding: 0 2px; }
/* the picture's name and artist, on the landing: sans, the reel's caption moved here. it may
   shrink and ellipsise so the bar stays one row on a phone */
/* the picture's name and artist, on a phone in grid view: its own line above the pill, centred,
   outside it — a title belongs with the picture, not among the controls (Louie, 2026-08-18) */
.lightbox__caption {
  display: none;
  position: fixed; left: var(--gutter); right: var(--gutter);
  bottom: calc(clamp(14px, 2vh, 24px) + 36px + 14px);   /* the pill's offset + its height + a breath */
  text-align: center;
  font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; letter-spacing: 0; text-transform: none; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  pointer-events: none;
}
.lightbox__caption .by { color: var(--ink-3); font-weight: 400; }
.lightbox__caption:not([hidden]) { display: block; }   /* on every screen — the desktop lightbox showed no title where the phone's did (Louie, 2026-08-18) */
/* the page holds still underneath: no scroll while the layer is up */
/* the page must not scroll under an open lightbox. with a mouse: html overflow hidden (beats the
   grid/index rules that open the document). on a touch screen NOT that — html overflow:hidden is
   the state iPhone Safari paints its chrome opaque over: dark bars behind the island and the
   bottom bar the moment the box opened (Louie, 2026-08-18). there the box holds the finger
   itself: touch-action none on the box (the track keeps its pan-x), and the box is its own scroll
   container with overscroll contained, so nothing chains to the document. */
@media (pointer: fine) { :root.has-lightbox, :root.has-lightbox.lab { overflow: hidden !important; } }
.lightbox { touch-action: none; overflow: hidden; overscroll-behavior: contain; }
.lightbox__track { overscroll-behavior: contain; }
/* the frame on the strip that is currently up */
#stills a[data-current] { outline: 1px solid var(--ink); outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .lightbox { transition: none; } }
.work__hero .plate { display: block; aspect-ratio: var(--r); background: var(--bench); position: relative; width: 100% !important; }
.work__hero-open { cursor: zoom-in; }   /* the hero opens the lightbox on a photo page */
.work__hero img { width: 100%; height: 100%; object-fit: contain; }
.work__grid {
  --pack-gap: var(--gutter);
  padding: var(--gutter) var(--gutter) 0;   /* the grid follows the hero (or the credits) at the same gap as between its pictures */
  display: flex; flex-wrap: wrap; align-items: flex-start; align-content: flex-start;
  gap: var(--gutter);
}
.work__grid .cell { position: relative; display: block; flex: 0 0 var(--w, 100%); aspect-ratio: var(--r); background: var(--bench); min-width: 0; cursor: zoom-in; }
.work__grid .cell.is-out { display: none; }
.work__grid .cell picture, .work__grid .cell img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.js .work__grid:not(.is-packed) { visibility: hidden; }
/* and the pictures themselves are not laid out until the pack has set --w: visibility:hidden
   still lays a cell out at 100% width, and a lazy image's sizes="auto" read that width and
   fetched the largest rung, then fetched again at the packed width — 60% of the page's picture
   bytes twice (the review, 2026-08-18). the packer measures the container, not the cells. */
.js .work__grid:not(.is-packed) .cell picture { display: none; }

/* credits */
.credits {
  background: var(--level-1);
  margin-top: var(--gutter);
  padding: clamp(18px, 2.4vh, 28px) var(--gutter);   /* slim; the band grows only with the crew */
  display: grid;
  grid-template-columns: repeat(12, minmax(0, 1fr));
  column-gap: var(--gutter);
}
.credits > * { grid-column: 1 / -1; }
/* what it is about, what it is, and who made it — side by side once there is room: half the
   width each with two columns, a third each with three */
/* Stacked on a phone, two abreast once there is room for a role and a name on one line, three
   only when the band has three things to say and the screen is wide enough that a third of it is
   still a readable measure. The count is on the section, so nothing depends on :has(). */
@media (min-width: 700px) {
  .credits > * { grid-column: span 6; }
  .credits__col + .credits__col h3:first-child { padding-top: 0; }
}
@media (min-width: 1180px) {
  .credits--3 > .credits__col { grid-column: span 4; }
}
@media (max-width: 699px) { .credits__col + .credits__col { padding-top: 18px; } }
/* a couple of sentences about the work, above its facts — the measure of a paragraph, not of a
   table, so it reads rather than scans */
/* a step under the statement size, like the line under a project's title: it is read, not
   scanned, but it is not the loudest thing in the band either */
.credits__about {
  font-size: calc(var(--t-editorial) * 0.86);
  line-height: 1.45;
  letter-spacing: -0.008em;
  max-width: 42ch;
  color: var(--ink-2);
  margin: 0;
}
.credits h3 { padding: 0 0 8px; }
.credits h3 + dl { margin-top: 2px; }
.credits h3:not(:first-child) { padding-top: 18px; }
/* Two facts share a line wherever the column can hold both at a readable width — year beside
   location rather than each taking a whole row for four characters — and fall back to one per
   line the moment it cannot. No breakpoint decides it; the column's own width does. */
/* one fact to a line: it is a list, and a list reads down */
.credits .facts { display: grid; grid-template-columns: minmax(0, 1fr); gap: 0; }
.credits dl > div {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: 1.5em;
  padding: 7px 0;
  align-items: baseline;
}
/* label → mono micro, ink-3 (like the facts and ledger heads); name → sans ui 500 (like a ledger title) */
.credits dt { font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro); color: var(--ink-3); }
.credits dd { font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; letter-spacing: 0; color: var(--ink); }

/* previous / next — small and simple */
.work__next {
  padding: clamp(64px, 12vh, 140px) var(--gutter) 0;
  display: flex; justify-content: space-between; gap: var(--gutter);
}
.work__next a {
  display: inline-flex; align-items: baseline; gap: 12px;
  color: var(--ink-2);
  font-family: var(--sans); font-size: var(--t-ui); font-weight: 500;
  min-width: 0;
}
.work__next .k { font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro); color: var(--ink-3); }
.work__next .t { color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.work__next .arrow { display: inline-block; transition: transform 0.4s var(--ease); }
.work__nextlink { text-align: right; margin-left: auto; }
@media (hover: hover) {
  .work__next a:hover .t { color: var(--ink-2); }
  .work__nextlink:hover .arrow { transform: translateX(0.35em); }
  .work__prevlink:hover .arrow { transform: translateX(-0.35em); }
}
@media (max-width: 719px) { .work__next .k { display: none; } }

/* ── Store ──────────────────────────────────────────────────────────────── */


.store { padding: calc(var(--bar-h) + var(--gutter) * 1.5) var(--gutter) 0; }   /* the landing intro's own top offset — the store lede is that intro, on the store */
.store__lede { max-width: 40ch; color: var(--ink-2); font-size: var(--t-body); display: grid; gap: 14px; }
.store__lede .hero__statement { color: var(--ink); margin-bottom: 4px; }
.store__lede a { color: var(--ink); }
@media (hover: hover) { .store__lede a:hover { color: var(--ink-2); } }

/* tiles — one row of parts, the whole store in one view */
.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 260px), 360px));   /* smaller tiles, capped, never stretched to fill */
  justify-content: start;
  gap: var(--gutter);   /* the same air as the page gutter between the parts */
  padding-top: clamp(20px, 3vh, 36px);
}
.tile {
  display: grid; grid-template-rows: auto 1fr;
  background: var(--panel); color: var(--panel-ink); box-shadow: var(--panel-shadow);
  padding: 6px 6px 0;   /* the frame around the bench lives on the tile, not as margins on the bench — WebKit drops a grid item's margin when it has an aspect ratio */
}
.tile__bench {
  position: relative; display: block;
  aspect-ratio: 1 / 1;
  width: 100%; min-width: 0;
  background: var(--bench);
}
.tile__bench lj-model { position: absolute; inset: 0; width: 100%; height: 100%; }
.tile__n {
  position: absolute; left: 10px; top: 10px;
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: var(--track-micro);
  color: var(--ink-3); pointer-events: none;
}
.tile__info { display: grid; gap: 12px; align-content: start; padding: 12px 12px 14px; }   /* 6px of tile padding + 12 = 18 from the edge */
.tile__info .readout { color: var(--panel-ink-2); }
.tile__title {
  font-family: var(--sans);
  font-size: clamp(20px, 1.5vw, 26px);
  font-weight: 500;
  line-height: 1.05;
  letter-spacing: -0.02em;
  color: var(--panel-ink);
  text-wrap: balance;
}
@media (hover: hover) { .tile__title a:hover { color: var(--panel-ink-2); } }
.tile__row { display: flex; align-items: center; justify-content: space-between; gap: 1em; flex-wrap: wrap; padding-top: 6px; }
.tile__price { font-family: var(--mono); font-size: var(--t-mono); color: var(--panel-ink); }
.tile .btn { border-color: var(--panel-ink); color: var(--panel-ink); }
.tile .btn--fill { background: var(--panel-ink); color: var(--panel); }
@media (hover: hover) { .tile .btn--fill:hover { background: transparent; color: var(--panel-ink); } }
.tile .btn--ghost { color: var(--panel-ink-2); border-color: transparent; }
.store .ledger { padding: clamp(40px, 6vh, 72px) var(--gutter) var(--gutter); }

/* variants — one row of choices per option; the chosen one is ink, the rest hairline */
.variants { display: grid; gap: 16px; }
.variants__opt { display: grid; gap: 8px; }
.variants__opt .readout span:last-child { color: var(--ink); }
.variants__values { display: flex; flex-wrap: wrap; gap: 6px; }
.variants__value {
  font-family: var(--mono); font-size: var(--t-micro); letter-spacing: var(--track-micro); text-transform: uppercase;
  padding: 9px 12px 8px; line-height: 1;
  border: 1px solid var(--line-2); color: var(--ink-2);
  transition: background 0.3s var(--ease), color 0.3s var(--ease), border-color 0.3s var(--ease);
}
.variants__value:hover { color: var(--ink); border-color: var(--ink); }
.variants__value[aria-pressed="true"] { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.variants__value[data-out] { color: var(--ink-3); border-style: dashed; }
.variants__value[data-out][aria-pressed="true"] { background: var(--ink-3); border-color: var(--ink-3); }
.btn[disabled] { pointer-events: none; }
.cart__line .v { color: var(--ink-2); }

/* the product page's own bench */
.bench {
  position: relative;
  background: var(--bench);
  aspect-ratio: 1 / 1;
  width: 100%;
  max-width: min(100%, calc(100svh - var(--bar-h) - var(--gutter) * 2));
  display: grid;
}
.bench lj-model { position: absolute; inset: 0; width: 100%; height: 100%; }
.bench__tag {
  position: absolute; left: 12px; right: 12px; top: 12px;
  display: flex; justify-content: space-between; gap: 1em;
  font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro);
  color: var(--ink-3); pointer-events: none;
}
.bench__tag > span:first-child { background: var(--panel); color: var(--panel-ink); box-shadow: var(--panel-shadow); padding: 7px 9px 6px; line-height: 1; }

.btn {
  display: inline-flex; align-items: center; gap: 0.9em;
  padding: 12px 16px;
  border: 1px solid var(--line-2);
  color: var(--ink);
  line-height: 1;
  transition: background 0.35s var(--ease), color 0.35s var(--ease), border-color 0.35s var(--ease), opacity 0.35s;
  white-space: nowrap;
}
@media (hover: hover) { .btn:hover { background: var(--ink); color: var(--paper); border-color: var(--ink); } }
.btn--fill { background: var(--ink); color: var(--paper); border-color: var(--ink); }
@media (hover: hover) { .btn--fill:hover { background: transparent; color: var(--ink); } }
.btn[disabled], .btn.is-busy { opacity: 0.45; pointer-events: none; }
.btn--ghost { border-color: transparent; color: var(--ink-3); pointer-events: none; padding-inline: 0; }

/* cart ledger — store-local, appears only with lines */
/* the bag — one column, in bands. the lines at the about page's client scale, each a big name
   with its variant, quantity and price in one mono row beneath; the summary a deeper band under
   them with checkout in it. no boxes: two full-bleed strips, a step and two steps off the paper.
   quantity and price sit in fixed columns so they stack in one line down the page. (Louie,
   2026-08-18: "boxes are not lined up") */
.bag { padding: var(--bar-h) 0 0; display: grid; gap: 0; }   /* clears the bar; the head band carries its own, equal, air */
.bag > .store__head { padding: clamp(28px, 5vh, 56px) var(--gutter) !important; display: grid; align-content: center; background: var(--paper); }   /* the title centred, on the paper — only the summary is a grey band (Louie, 2026-08-18) */
.bag__body { display: grid; gap: 0; }
.bag__band { padding: clamp(36px, 6vh, 72px) var(--gutter); display: grid; gap: 22px; }
.bag__band--lines { background: var(--level-2); }   /* the lines are the grey band */
.bag__band--summary { background: var(--paper); }    /* the summary on the paper (Louie, 2026-08-18: swapped) */
.cart__lines { display: grid; gap: clamp(18px, 2.6vh, 30px); }
.cart__line { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; }   /* one column: the name above its row. without this the two auto-placed side by side and a name could be squeezed to nothing */
/* the name at the landing intro's size — --t-editorial, the one statement size on the site
   (landing statement, store lede, about paragraphs) — so the bag speaks in the same register as
   the rest, and a name always sits on one line (Louie, 2026-08-18) */
.cart__line .t {
  font-family: var(--sans); font-size: var(--t-editorial); font-weight: 500;
  line-height: 1.2; letter-spacing: -0.012em; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.cart__line-row {
  display: grid; grid-template-columns: minmax(0, 1fr) auto 10ch; gap: 1.5em; align-items: center;
  font-family: var(--mono); font-size: var(--t-mono); color: var(--ink-2);
}
.cart__line .v { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cart__line .p { text-align: right; white-space: nowrap; color: var(--ink); }
.qty { display: inline-grid; grid-auto-flow: column; align-items: center; border: 1px solid var(--line-2); justify-self: end; }
.qty button { width: 32px; height: 32px; display: grid; place-items: center; color: var(--ink); }
.qty span { min-width: 3ch; text-align: center; color: var(--ink); }
@media (hover: hover) { .qty button:hover { background: var(--ink); color: var(--paper); } }
.bag__summary { gap: 14px; }
.bag__summary .row { display: flex; justify-content: space-between; align-items: baseline; gap: 1.5em; font-family: var(--mono); font-size: var(--t-mono); color: var(--ink-2); max-width: 44ch; }
.bag__summary .row b { font-weight: 450; color: var(--ink); }
.bag__summary .btn--fill { justify-self: start; margin-top: 6px; }
.bag__summary .cart__note { color: var(--ink-3); max-width: 44ch; }
/* on a phone the summary is the foot of the page: the footer is hidden, the band runs to the
   bottom of the window and closes with the way to ask before buying. on a desktop the footer
   stays and the band is a band. (Louie, 2026-08-18) */
.bag__contact { display: none; }
@media (max-width: 719px) {
  body:has(#bag-page[data-cart="lines"]) .foot { display: none; }
  /* what shows below the layout — behind Safari's floating bar, under 100svh — is the body's
     colour; on the bag the summary's grey must run to the very bottom, but the page above it stays
     paper (painting the whole body grey turned the header grey too). so: paper on top, the
     summary's grey pinned to the bottom of the body's own background (Louie, 2026-08-18) */
  body:has(#bag-page[data-cart="lines"]) { background: var(--paper); }
  html:has(#bag-page[data-cart="lines"]) { background: var(--paper); }   /* the sliver below the pinned summary, behind Safari's bar, matches the summary: paper */
  /* the bag is the screen: title, then the lines, then the summary pinned to the foot. the lines
     take what is left and scroll only when there are more than fit; on a tall screen the room
     between them and the summary simply stays open. the summary sits above the phone's own bar
     and the home indicator, so nothing in it is ever covered. (Louie, 2026-08-18) */
  [data-cart="lines"].bag { height: calc(100svh - var(--safe-top)); min-height: 0; grid-template-rows: auto minmax(0, 1fr); overflow: hidden; }   /* minus the html padding, or the summary sat below the fold on an island phone */
  [data-cart="lines"] .bag__body { grid-template-rows: minmax(0, 1fr) auto; min-height: 0; }
  [data-cart="lines"] .bag__band--lines { min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; align-content: start; }
  /* the summary takes what it needs and no more, so the lines get the screen: tighter gaps, the
     note and the contact line small, and its padding trimmed above the phone's own bar */
  .bag__band--summary { padding: 18px var(--gutter) calc(14px + env(safe-area-inset-bottom, 0px)); gap: 8px; align-content: start; }
  .bag__band--summary .readout { margin-bottom: 2px; }
  .bag__summary .btn--fill { margin-top: 4px; }
  .bag__summary .cart__note { font-size: var(--t-mono); padding-top: 4px; }
  .bag__contact { display: block; margin-top: 6px; font-family: var(--sans); font-size: var(--t-mono); color: var(--ink-2); max-width: 44ch; }
  .bag__contact a { color: var(--ink); }
  /* and the lines band breathes less too, so two items never need to scroll on a normal phone */
  [data-cart="lines"] .bag__band--lines { padding-top: 22px; padding-bottom: 22px; }
  .cart__lines { gap: 16px; }
}
.bag__empty { display: grid; gap: 18px; color: var(--ink-2); padding: 0 var(--gutter); }
.bag__empty a { color: var(--ink); }
[data-cart="empty"] .bag__body, [data-cart="empty"] .cart { display: none; }
[data-cart="lines"] .bag__empty { display: none; }
.cart__note { color: var(--ink-3); padding-top: 8px; }
.cart__err { color: var(--ink-2); padding: 12px 0; }
@media (max-width: 719px) {
  .cart__line-row { grid-template-columns: minmax(0, 1fr) auto; }
  .cart__line .p { grid-column: 1 / -1; text-align: left; }
}

/* product page */
/* the product is a full-width band, one step up from the paper; the bench sits a step deeper inside it */
.product {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--gutter);
  padding: calc(var(--bar-h) + var(--gutter) * 2) var(--gutter) clamp(40px, 6vh, 72px);
  align-items: start;
  background: var(--level-1);
}
.product .bench { background: var(--level-2); }
/* the part's pictures, from Shopify, under the 3D bench: a row that wraps, each in its own ratio.
   the strip is empty until shop.js fills it, and hidden when there is nothing to show. */
.product__images { display: flex; flex-wrap: wrap; gap: var(--gutter); }
.product__image { margin: 0; flex: 1 1 200px; max-width: 100%; position: relative; aspect-ratio: var(--r, 1 / 1); background: var(--level-2); }
.product__image img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.product__info .backlink { margin-bottom: 4px; }
/* on a phone (one column) the way back sits ABOVE the part, first thing on the page; in two
   columns it is the first line of the words, as before. two links, one shown (Louie, 2026-08-18) */
.product__back { display: none; }
@media (max-width: 899px) { .product__back { display: inline-flex; margin-bottom: calc(var(--gutter) * -0.4); } .product__info > .backlink { display: none; } }
@media (min-width: 900px) {
  .product { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); grid-template-rows: auto 1fr; }
  .product .bench { position: sticky; top: var(--gutter); grid-column: 1; grid-row: 1; }   /* the bar scrolls away with the page now, so nothing to clear */
  .product__images { grid-column: 1; grid-row: 2; align-self: start; }
  .product__info { grid-column: 2; grid-row: 1 / 3; }
  /* with pictures under it the bench cannot be sticky — it would ride over them as you scroll */
  .product:has(.product__images:not([hidden])) .bench { position: static; }
}
.product__info { display: grid; gap: clamp(20px, 3vh, 36px); align-content: start; }
.product__info .title { font-size: clamp(36px, 4.6vw, 88px); }
.product__desc { color: var(--ink-2); max-width: 46ch; }
.product__actions { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.product__back { color: var(--ink-2); }
.product__ships { color: var(--ink-2); }

/* the file: a digital download is its own band on the paper, below the part it belongs to,
   so buying the thing and buying the file to print it are never the same button */
.filelane {
  padding: clamp(26px, 4vh, 46px) var(--gutter);
  border-top: 1px solid var(--line);
}
.filelane[hidden] { display: none; }
.filelane__in {
  display: grid;
  gap: clamp(16px, 2.4vh, 26px);
  align-items: end;
}
@media (min-width: 700px) {
  .filelane__in { grid-template-columns: minmax(0, 1fr) auto; }
  .filelane__act { justify-self: end; text-align: right; }
}
.filelane__t { font-size: clamp(22px, 2.4vw, 34px); letter-spacing: var(--track-title); margin: 10px 0 0; }
.filelane__note { color: var(--ink-2); max-width: 52ch; margin: 8px 0 0; }
.filelane__act { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.filelane__price { font-size: clamp(18px, 1.5vw, 22px); }

/* the page nobody means to reach: the same measure as a project's header */
.notfound {
  padding: calc(var(--bar-h) + var(--gutter) * 2) var(--gutter) clamp(60px, 12vh, 140px);
  display: grid;
  gap: clamp(14px, 2vh, 22px);
  align-content: start;
  min-height: 60vh;
}
.notfound .editorial { max-width: 46ch; color: var(--ink-2); }

/* ── About ──────────────────────────────────────────────────────────────── */

.about { padding: 0 var(--gutter) 0; display: grid; gap: 0; }   /* the intro is exactly one screen; the clients band begins just below the fold */
/* the words, centred on the first screen; the clients wait below the fold */
.about__intro {
  position: relative;
  height: 100svh;
  padding: calc(var(--bar-h) + var(--gutter)) 0 calc(var(--gutter) * 2 + 40px);
  display: grid; align-content: center; justify-content: center; align-items: center;
  /* the portrait to the left, the words to the right — one measure, centred */
  grid-template-columns: minmax(0, 26vw) minmax(0, 42ch);
  column-gap: clamp(28px, 4vw, 72px);
  font-size: var(--t-editorial);
}
.about__words { display: grid; gap: clamp(28px, 5vh, 56px); min-width: 0; }
.about__portraits { min-width: 0; }
.about__portrait { margin: 0; min-width: 0; display: none; }
.about__portrait.is-on { display: block; }
.about__portrait .plate { display: block; position: relative; aspect-ratio: var(--r); background: var(--bench); max-height: min(64svh, 640px); margin-left: auto; }
.about__portrait img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.about__portrait--empty .plate { display: grid; place-items: center; width: min(100%, calc((100svh - var(--bar-h) - var(--gutter) * 3 - 40px) * 0.8)); }
.about__portrait--empty .readout { color: var(--ink-3); }
/* the arrow: there is more below — bobs gently, leaves as you scroll */
.about__more-arrow {
  position: absolute; left: 50%; bottom: var(--gutter); translate: -50% 0;
  /* on a phone the bottom centre is where Safari's floating address bar lives, and the arrow sat
     behind it. so: up past the home indicator and the bar, and over to the right, out of the
     field's way (Louie, 2026-08-18) */

  width: 28px; height: 28px; color: var(--ink-2);
  animation: bob 1.8s ease-in-out infinite;
  transition: opacity 0.4s var(--ease), color 0.3s var(--ease);
}
@media (max-width: 719px) { .about__more-arrow { display: none; } }   /* not on a phone (Louie, 2026-08-18) — top-level, not nested: iOS ≤16.4 drops a nested @media */

.about__more-arrow svg { width: 100%; height: 100%; display: block; }
.about__more-arrow:hover { color: var(--ink); }
.is-scrolled .about__more-arrow { opacity: 0; pointer-events: none; }
@keyframes bob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(6px); } }
@media (prefers-reduced-motion: reduce) { .about__more-arrow { animation: none; } }
@media (max-width: 899px) {
  .about__intro { height: auto; min-height: 100svh; grid-template-columns: minmax(0, 1fr); row-gap: 28px; align-content: start; padding-top: calc(var(--bar-h) + var(--gutter) * 2); }
  .about__portrait--empty .plate { width: min(100%, 60vw); }
  /* one column: the portrait takes the column, edge to edge, the same margins as the words under
     it. the height cap and the right-align belong to the two-column layout — on a phone they left
     a narrow picture floating right with a gap on the left. */
  .about__portrait .plate { width: 100%; max-height: none; margin-left: 0; }
}
.about__editorial { display: grid; gap: 1.1em; width: 100%; }
.about__sign { color: var(--ink-2); font-family: var(--mono); font-size: var(--t-mono); width: 100%; justify-self: start; }
.about__clients {
  display: grid; gap: 18px;
  background: var(--level-1);
  margin-inline: calc(-1 * var(--gutter));
  padding: clamp(40px, 7vh, 88px) var(--gutter) clamp(40px, 7vh, 88px);
}
/* the clients drift past in one line, huge; hover holds them */
.about__clients li {
  font-family: var(--sans);
  font-size: var(--t-title);
  font-weight: 500;
  line-height: 1;
  letter-spacing: var(--track-title);
  color: var(--ink);
  padding: 0.04em 0;
  margin-left: -0.07em;
}
.js .about__clients li[data-reveal] { transition-delay: calc(40ms + var(--i, 0) * 95ms); }   /* one run, a beat apart — begun almost at once (Louie, 2026-08-18: the lead-in was slow) */
/* five huge names moving at once, often while the page is still being scrolled: each on its own
   layer, and a shorter rise on a phone, so the run is smooth rather than a redraw a frame (Louie,
   2026-08-18) */
.js .about__clients [data-reveal] { will-change: auto; }
/* on a touch screen the names do not animate in at all: every timing tried read as slow or as a
   race with the scroll; they are simply there (Louie, 2026-08-18) */
@media (pointer: coarse) { .js .about__clients [data-reveal] { opacity: 1; transform: none; transition: none; } }
.js .about__clients .about__more[data-reveal] { transition-delay: 560ms; }   /* the line after the names waits for them */
.about__more { padding-top: clamp(16px, 2.4vh, 28px); font-size: var(--t-body); color: var(--ink-2); }
.about__cta { padding-top: clamp(20px, 3vh, 36px); font-size: var(--t-body); color: var(--ink-2); }
.about__cta a { color: var(--ink); }
.about__cta .arrow { display: inline-block; margin-left: 0.5em; transition: transform 0.4s var(--ease); }
@media (hover: hover) { .about__cta a:hover .arrow { transform: translateX(0.35em); } }
[data-view="index"] .about__cta { display: none; }

/* about, as one sheet — index view: everything on one screen, no scroll */
.about__sheet { display: none; }
[data-view="index"] .about__sheet {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(28px, 4vw, 72px);
  min-height: calc(100svh - var(--bar-h));
  padding: calc(var(--bar-h) + var(--gutter) * 2) 0 var(--gutter);
  align-content: start;
}
[data-view="index"][data-page="about"] .foot { display: none; }   /* keyed on html's own data-page, not a :has() — see the note at html.lab: a :has() judged together with :root's view attribute does not re-evaluate on toggle in iOS Safari */
[data-view="index"] .about { gap: 0; }
[data-view="index"] .about__intro, [data-view="index"] .about__clients { display: none; }
.about__sheet-col { display: grid; gap: 16px; align-content: start; }
.about__sheet .mono, .about__sheet-text { font-size: var(--t-body); color: var(--ink-2); font-family: var(--sans); text-transform: none; letter-spacing: 0; }
.about__sheet-text { display: grid; gap: 0.9em; max-width: 44ch; }
.about__sheet a { color: var(--ink); }
@media (hover: hover) { .about__sheet a:hover { color: var(--ink-2); } }
.about__sheet-list { display: grid; gap: 8px; font-family: var(--mono); font-size: var(--t-mono); }
.about__sheet-list li { display: grid; grid-template-columns: 3ch minmax(0, 1fr); gap: 1.5em; }
.about__sheet-list li span:first-child { color: var(--ink-3); }
.about__sheet-facts { display: grid; gap: 10px; }
.about__sheet-facts > div { display: grid; grid-template-columns: 9ch minmax(0, 1fr); gap: 1.5em; align-items: baseline; font-family: var(--mono); font-size: var(--t-mono); }
.about__sheet-facts dt { color: var(--ink-3); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro); }
.about__sheet-facts dd { color: var(--ink); }
@media (max-width: 900px) { [data-view="index"] .about__sheet { grid-template-columns: 1fr; min-height: 0; } }

/* ── Footer — the autograph is the email ────────────────────────────────── */

.foot {
  padding: clamp(56px, 9vh, 112px) var(--gutter) var(--gutter);
  display: grid; gap: 22px;
}
.foot__mail {
  font-family: var(--sans);
  font-size: var(--t-mail);
  font-weight: 500;
  line-height: 0.95;
  letter-spacing: var(--track-title);
  margin-left: -0.07em;
  color: var(--ink);
  transition: color 0.5s var(--ease);
  word-break: break-all;
  width: fit-content;
}
.foot__mail { display: inline-flex; align-items: baseline; gap: 0.18em; }
/* the send plane — hidden, then slides in beside the address on hover; centred on the lower-case m */
.foot__plane {
  /* 0.44em tall, resting 0.031em above the baseline so its centre sits on the x-height centre (0.502em / 2) */
  width: 0.44em; height: 0.44em; flex: none;
  margin-left: 0.06em;
  align-self: baseline;
  display: block; position: relative;
  color: var(--ink);
  opacity: 0; transform: translate(-0.25em, 0.08em) scale(0.85);
  transition: opacity 0.3s var(--ease), transform 0.6s var(--ease);
}
.foot__plane svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
@media (hover: hover) {
  .foot__mail:hover { color: var(--ink-2); }
  .foot__mail:hover .foot__plane { opacity: 1; transform: translateY(-0.031em) scale(1); }
}
.foot__mail:focus-visible .foot__plane { opacity: 1; transform: translateY(-0.031em); }
@media (prefers-reduced-motion: reduce) { .foot__plane { transition: opacity 0.2s; } }
.foot__ledger {
  display: flex; flex-wrap: wrap; gap: 10px clamp(20px, 3vw, 44px);
  padding-top: 8px; margin-top: 8px;
  color: var(--ink-3);
}
.foot__ledger a { color: var(--ink-2); }
@media (hover: hover) { .foot__ledger a:hover { color: var(--ink); } }
.foot__ledger .end { margin-left: auto; }
/* on a phone the email is the footer: it takes the column, the ledger tucks under it — smaller,
   and the three stacked spacers (grid gap, ledger padding, ledger margin) collapse to one */
@media (max-width: 719px) {
  .foot { gap: 12px; }
  .foot__mail { font-size: clamp(30px, 10.35vw, 44px); }
  .foot__ledger { font-size: 9px; gap: 8px 16px; padding-top: 0; margin-top: 0; }
}

/* ── Motion: reveals, opening, transitions ──────────────────────────────── */

/* scroll reveal — gated on .js so nothing is ever hidden without JavaScript */
.js [data-reveal] { opacity: 0; transform: translateY(26px); }
.js [data-reveal].in { opacity: 1; transform: none; transition: opacity 0.9s var(--ease), transform 1.2s var(--ease); }
.js .frame .plate img, .js .work__media figure[data-reveal] .plate img { clip-path: inset(100% 0 0 0); }
.js .frame.in .plate img, .js .work__media figure[data-reveal].in .plate img { clip-path: inset(0 0 0 0); transition: clip-path 1.25s var(--ease) 0.05s; }

/* opening — home, once per session: black, then the name rises under a mask, then the readouts */
/* the opening (once per session): the name first, then the words and the bar, then the pictures — about 2.6s in all */
/* the name starts fully below the window's bottom edge and rises into place (the landing clips at
   the viewport). the start is measured from the viewport, not the name's own height: on a phone
   the name rests higher in the window than on a desktop — intro and reel above it, the browser's
   own bars eating height — so "its own height below its rest" was still inside the window and it
   slid in from mid-screen. a full viewport below is off-screen wherever it rests. (2026-08-18) */
.opening .hero__name > span { animation: name-in 1.15s var(--ease) 0.3s both; }
/* on a phone the name fades in where it sits. the slide up from below runs through the reel
   and the intro on the way — and however far below it starts, on a small clipped stage it read
   as appearing mid-window. a fade cannot look wrong. (Louie, 2026-08-18) */
@media (max-width: 719px) { .opening .hero__name > span { animation: name-fade 1.1s var(--ease) 0.5s both; } }
.opening .hero__intro > * { animation: fade-up 0.9s var(--ease) both; animation-delay: calc(1.2s + var(--i, 0) * 0.12s); }
.opening .bar { animation: fade 0.8s ease 1.4s both; }
.opening .reel__item { animation: fade-up 1.3s var(--ease) both; animation-delay: var(--d, 1.6s); }   /* slow fades: the first picture alone at 1.6s, the rest from ~2.7s (delays set in slate.js) */
/* every later load in the session: a short, quick reveal — the name and words are simply there, the pictures fade up fast */
.opening-short .reel__item { animation: fade-up 0.5s var(--ease) both; animation-delay: var(--d, 0.1s); }
/* only the pictures that can be on screen animate; the rest of the strip is simply there (fewer layers, no hitch) */
.opening .reel__item:nth-child(n + 11), .opening-short .reel__item:nth-child(n + 11) { animation: none; }
.opening .reel__item:nth-child(-n + 10), .opening-short .reel__item:nth-child(-n + 10), .opening .hero__name > span { will-change: transform, opacity; }   /* the first picture at 1.7s, alone; the rest from ~3.2s, slowly (delays set in slate.js) */
@keyframes name-in { from { transform: translateY(calc(100svh + 12px)); } to { transform: none; } }
/* while the name rises from below the window its transformed box counts as scrollable overflow —
   the one-screen reel page was 784px taller for a second and a half, and a wheel could scroll it.
   the stage clips (clip, not hidden: no scroll container, nothing propagated to the viewport);
   below the viewport is not seen anyway. (2026-08-18) */
.opening .labstage, .opening-short .labstage { overflow: clip; }
@keyframes name-fade { from { opacity: 0; } to { opacity: 1; } }
@keyframes fade-up { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes fade { from { opacity: 0; } to { opacity: 1; } }

/* before the page reloads into grid view on a phone (see slate.js) only the PICTURES fade out; the
   bar, the intro and the name stand still, and the short opening on the other side fades only the
   pictures back in — so the words never blink (Louie, 2026-08-18) */
/* the index toggle on a phone: main out over a beat, the swap, main back (slate.js) */
.is-swapping main { opacity: 0; }
.js main { transition: opacity 0.22s var(--ease-io); }
.is-leaving .reel__item { opacity: 0; transition: opacity 0.45s var(--ease-io); }
body.lab .reel__item { transition: opacity 0.45s var(--ease-io); }   /* and back to full, as slowly, when the class comes off (grid → reel) */
@media (prefers-reduced-motion: reduce) { .is-leaving .reel__item { transition: none; } }

/* No cross-document view transitions. A page-to-page crossfade means the browser
   shows a snapshot of the old page over a document that has not painted yet, and on
   some compositors that reads as a flash. (In-page transitions — index, grid, paper — are kept:
   they are same-document and cannot flash.) Page to page is a fade by hand instead (slate.js):
   the body out over a beat as a link is followed, in as the next page paints, and in again for a
   page back from the cache — hard cuts read as a flash and a glitch on the phone (Louie's
   recording, 2026-08-18 22:55). reduced motion zeroes it with everything else, below. */
/* where the browser can cross-fade documents (Safari 18.2+, Chrome 126+) it does: the old page's
   picture fades into the new page once it has painted — no paper between them, the same
   seamlessness as the grid toggle's reload (Louie's recording, 2026-08-18 23:00). the fades by
   hand below are for browsers without it (slate.js turns the leaving fade off where the browser
   has this). back from the cache still fades in by hand: the browser does not transition that. */
@view-transition { navigation: auto; }
::view-transition-old(root) { animation-duration: 0.36s; animation-timing-function: var(--ease-io); }
::view-transition-new(root) { animation-duration: 0.36s; animation-timing-function: var(--ease-io); }
.js.is-arriving body { animation: page-in 0.32s var(--ease) both; }
.js.is-leaving-page body { opacity: 0; transition: opacity 0.18s var(--ease-io); animation: none; }
@keyframes page-in { from { opacity: 0; } to { opacity: 1; } }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.001ms !important; animation-delay: 0s !important; transition-duration: 0.001ms !important; }
  .js [data-reveal] { opacity: 1; transform: none; }
  .js .frame .plate img, .js .work__media figure[data-reveal] .plate img { clip-path: none; }
}

/* ── Small screens ──────────────────────────────────────────────────────── */

@media (max-width: 719px) {
  :root { --bar-h: 48px; }   /* the phone's shorter bar */
  .bar__nav { gap: 16px; }
  .hero { gap: 6vh; }
  .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .foot__ledger .end { margin-left: 0; }
}
@media (max-width: 420px) {
  .bar__nav { gap: 12px; }
}

/* ── Lab: alternate landings — one viewport each, the name anchored below ─── */

/* the reel page is one screen: body is exactly the viewport less the document's top padding
   (min-height:0 so body's own min-height:100svh cannot win it back), so there is nothing to
   scroll — and NOTHING is overflow:hidden on html or body. that matters on iPhone: a root that is
   overflow:hidden when the page loads is a page Safari has decided is not a scrolling document,
   and it keeps that decision — toggling to grid view made the document tall and scrollable, but
   the pictures stopped at the Dynamic Island until a reload, which loads it tall from the start
   and works (Louie's recording, 2026-08-18 21:40). the same page loaded in grid, or left in grid
   and returned to, was always fine. so the landing is an ordinary document from first paint,
   the same shape as every page that works; grid and index views simply let body grow. the
   rubber-band on a touch screen is held by overscroll-behavior at the top of the file. */
body.lab { min-height: 0; height: calc(100svh - var(--safe-top)); }
html.lab { height: 100svh; }
html.lab[data-view="grid"], html.lab[data-view="index"] { height: auto; }
/* EXPERIMENT (2026-08-18, ?tall): the reel page one pixel taller than the screen — a document that
   can scroll, by a pixel nobody sees. if iPhone Safari then treats it as a scrolling page from
   load, grid view need not reload, and a soft-navigation layer would be safe there. */
html.lab.is-tall:not([data-view]) { height: calc(100svh + 1px); }
body.lab main { min-height: 0; }
.labstage {
  --stage-gap: clamp(16px, 2.6vh, 32px);
  height: calc(100svh - var(--safe-top));   /* the html padding is outside this box */
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--stage-gap);
  padding: calc(var(--bar-h) + var(--gutter)) var(--gutter) var(--gutter);
}
.labstage .hero__intro { max-width: 40ch; }
.labstage .hero__name { align-self: end; }
.labnav {
  position: fixed; right: var(--gutter); bottom: var(--gutter); z-index: 100;
  display: flex; gap: 1.2em; align-items: center;
  font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro);
  color: var(--ink-3);
  background: var(--panel); color: var(--panel-ink-2); box-shadow: var(--panel-shadow);
  padding: 8px 12px 7px; line-height: 1;
}
.labnav a { color: var(--panel-ink-2); }
.labnav a[aria-current="page"], .labnav a:hover { color: var(--panel-ink); }
body.lab .hero__name { --t-name: max(40px, calc((var(--lw) - 2 * var(--gutter)) / var(--name-em))); }
/* it spans the width, always — the name is a ruler across the page, and a cap breaks the ruler on
   the very screens where it should be biggest. the one guard is short screens, where a name that
   tall would crowd the pictures. */
@media (max-height: 760px) { .labstage .hero__name { font-size: min(var(--t-name), 18svh); } }

/* 1 · reel */
.reel {
  display: flex; gap: var(--gutter);
  height: 100%;
  overflow-x: auto; overflow-y: hidden;
  margin-inline: calc(-1 * var(--gutter)); padding-inline: var(--gutter);
  scrollbar-width: none; overscroll-behavior-x: contain;
  cursor: grab;
}
.reel::-webkit-scrollbar { display: none; }
.reel.is-dragging { cursor: grabbing; }
.reel:focus-visible { outline: none; }
.reel__item {
  position: relative; flex: none;
  height: 100%; aspect-ratio: var(--r);
  background: var(--bench);
  display: flex; flex-direction: column; justify-content: flex-end;
}
.reel__item picture { position: absolute; inset: 0; }
.reel__item img { width: 100%; height: 100%; object-fit: contain; }
/* the title sticks to the page's left edge while its picture is passing, and
   hands over when the next picture arrives — sticky, sideways */
.reel__title {
  position: sticky; left: 0;   /* the reel's own padding puts this on the gutter */
  align-self: flex-start;
  margin-bottom: -22px;
  font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; color: var(--ink-3);   /* a step greyer than it was: the picture speaks, the caption whispers */
  white-space: nowrap; line-height: 1;
  transition: color 0.4s var(--ease);
}
@media (hover: hover) { .reel__item:hover .reel__title { color: var(--ink); } }
@media (max-width: 719px) { .reel__title { font-size: var(--t-mono); } }   /* smaller still on a phone (Louie, 2026-08-18) */
.reel__title .by { color: var(--ink-3); font-weight: 400; }   /* the artist a shade quieter than the title */
.reel__end { flex: none; width: 1px; }
.reel { padding-bottom: 26px; }   /* room for the hover title */
figure.reel__item { margin: 0; }
.reel__item--photo { cursor: default; }
:root[data-view="grid"] .reel__item--photo { cursor: zoom-in; }   /* in grid view a loose photograph opens the lightbox like the rest */
/* the order is shuffled per visit — hold the strip until that has happened (JS only) */
.js .reel:not(.is-shuffled) { visibility: hidden; }

/* grid — every picture, justified: rows share a height, each keeps its ratio,
   one gap everywhere; the name becomes the foot */
:root[data-view="grid"] body.lab { height: auto; overflow-y: visible; }   /* the DOCUMENT scrolls, not the body: with overflow:auto iOS makes the body its own scroller, the viewport never moves, the absolute bar never leaves and Safari's bar never collapses — the one page where the island did not work (Louie, 2026-08-18) */
:root[data-view="grid"] .labstage { height: auto; display: grid; grid-template-rows: auto auto auto; }
:root[data-view="grid"] .reel {
  align-items: flex-start; align-content: flex-start;   /* WebKit: never stretch — the height comes from the ratio */
  --row: clamp(200px, 34vh, 480px);
  --gap: var(--gutter);   /* one measure: the page gutter is the gap, the gap is the page gutter */
  display: flex; flex-wrap: wrap;
  gap: var(--gap);
  height: auto; overflow: visible;
  padding-bottom: 0; padding-inline: 0;
  margin-inline: 0;   /* the pictures sit on the page gutter like every other block */
  cursor: default;
}
:root[data-view="grid"] .reel__item {
  /* widths are packed by lab.js (--w): rows share a height, each picture keeps its ratio */
  flex: 0 0 var(--w, calc(var(--r) * var(--row)));
  height: auto; aspect-ratio: var(--r);
  min-width: 0;
}
:root[data-view="grid"] .reel__item.is-out { display: none; }
/* no spacer row: the grid always ends on a full-width picture, so the last row is already whole */
:root[data-view="grid"] .reel__item.is-wide { flex: 0 0 100%; }   /* the full-width beat — landscapes only */
:root[data-view="grid"] .reel__item picture { position: absolute; inset: 0; }
:root[data-view="grid"] .reel__item img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
:root[data-view="grid"] .reel__end { display: none; }
/* in grid view a picture keeps its small caption on a desktop — just under it, on hover. on a
   phone the captions made the rows ragged and truncated anyway, so there they are out of the grid
   and shown in the lightbox instead (Louie, 2026-08-18) */
:root[data-view="grid"] .reel__title {
  display: block; position: absolute; left: 0; right: 0; top: 100%;
  margin: 6px 0 0; padding: 0;
  font-size: var(--t-mono); line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--ink-3); pointer-events: none;
  opacity: 0; transition: opacity 0.25s var(--ease);
}
:root[data-view="grid"] .reel__item:hover .reel__title { opacity: 1; }
@media (max-width: 719px) { :root[data-view="grid"] .reel__title { display: none; } }
:root[data-view="grid"] .labstage .hero__name {
  /* the name stays where it lives — anchored to the foot of the viewport — and the pictures glide under it */
  position: fixed; left: var(--gutter); right: var(--gutter); bottom: var(--gutter);
  z-index: 5; pointer-events: none;
  color: #fff; mix-blend-mode: difference;   /* inverts against whatever passes beneath it */
}
:root[data-view="grid"] .reel { padding-bottom: calc(var(--t-name) * 0.76); }   /* at the end, the name sits one gutter under the last picture */
/* on a phone in grid view the name is STICKY at the foot, not fixed: it rides the bottom of the
   window while the pictures pass and settles under the last one — the same look — and the page
   then has no position:fixed element at all, which is the shape of every page that scrolls under
   the island. it keeps the difference-blend: plain ink vanished over a dark picture. (review, 2026-08-18) */
@media (max-width: 719px) {
  :root[data-view="grid"] .labstage .hero__name {
    position: sticky; bottom: var(--gutter); left: auto; right: auto;
    z-index: 5; pointer-events: none;
  }
  :root[data-view="grid"] .reel { padding-bottom: 0; }
}
/* 2 · strips */
.strips { display: grid; grid-template-rows: 1fr 1fr; gap: 6px; height: 100%; margin-inline: calc(-1 * var(--gutter)); }
.strip { position: relative; overflow: hidden; min-height: 0; }
.strip__track {
  display: flex; gap: 6px; height: 100%; width: max-content;
  animation: drift var(--drift, 90s) linear infinite;
}
.strip--right .strip__track { animation-direction: reverse; }
@media (hover: hover) { .strip:hover .strip__track { animation-play-state: paused; } }
.strip__item { flex: none; height: 100%; aspect-ratio: var(--r); background: var(--bench); display: block; }
.strip__item img { width: 100%; height: 100%; object-fit: contain; }
@keyframes drift { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) {
  .strip { overflow-x: auto; scrollbar-width: none; }
  .strip__track { animation: none; }
}

/* 3 · sheet */
.sheet { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: var(--gutter); height: 100%; min-height: 0; }
.sheet__table { position: relative; min-height: 0; }
.sheet__large {
  position: absolute; inset: 0;
  display: grid; place-items: start;
  opacity: 0; transition: opacity 0.45s var(--ease);
  pointer-events: none;
}
.sheet__large.is-on { opacity: 1; pointer-events: auto; }
.sheet__large picture { height: 100%; width: 100%; display: grid; place-items: start; }
.sheet__large img { max-height: 100%; max-width: 100%; width: auto; height: auto; object-fit: contain; }
.sheet__caption { position: absolute; left: 0; bottom: -20px; }
.sheet__caption span { color: var(--ink-2); font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; text-transform: none; letter-spacing: 0; }
.sheet__thumbs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 6px; align-content: start; overflow: auto; scrollbar-width: none; min-height: 0; }
.sheet__thumb { display: block; padding: 0; background: var(--bench); aspect-ratio: 1; position: relative; }
.sheet__thumbframe { position: absolute; inset: 6px; display: grid; place-items: center; }
.sheet__thumbframe picture, .sheet__thumbframe img { max-width: 100%; max-height: 100%; width: auto; height: auto; }
.sheet__thumbframe img { object-fit: contain; opacity: 0.55; transition: opacity 0.35s var(--ease); }
.sheet__thumb.is-on img, .sheet__thumb:hover img { opacity: 1; }
.sheet__thumb.is-on { outline: 1px solid var(--ink); outline-offset: -1px; }
@media (max-width: 719px) {
  .sheet { grid-template-columns: 1fr; grid-template-rows: minmax(0, 1fr) auto; }
  .sheet__thumbs { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}

/* 4 · deck */
.deck { position: relative; height: 100%; min-height: 0; }
.deck:focus-visible { outline: none; }
.deck__card {
  position: absolute; left: 50%; top: 50%;
  height: min(100%, 64svh); max-width: 82vw; aspect-ratio: var(--r);
  background: var(--bench);
  translate: -50% -50%;
  transform: translate(calc(var(--k) * 16px), calc(var(--k) * -12px)) rotate(calc(var(--k) * 1.3deg));
  z-index: calc(100 - var(--k));
  opacity: 1;
  transition: transform 0.85s var(--ease), opacity 0.6s var(--ease);
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.35);
}
.deck__card[data-gone] { transform: translate(-130%, 6%) rotate(-7deg); opacity: 0; pointer-events: none; }
.deck__card[data-deep] { opacity: 0; pointer-events: none; }
.deck__card img { width: 100%; height: 100%; object-fit: contain; }
.deck__caption { position: absolute; left: 0; bottom: 0; }
.deck__caption span:first-child { color: var(--ink-2); font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; text-transform: none; letter-spacing: 0; }
[data-theme="light"] .deck__card { box-shadow: 0 24px 60px rgba(0, 0, 0, 0.16); }
@media (prefers-reduced-motion: reduce) { .deck__card { transition: none; } }

/* ── a phone on its side ──────────────────────────────────────────────────────
   under 480px tall and wider than tall: an iPhone in landscape, and no tablet (an iPad mini is
   768 tall on its side). one block, last in the file, so it wins its ties. (Louie, 2026-08-18)
     · the whole page keeps clear of the camera and the island, which lie on the SIDES now: body
       pads by the safe-area insets, so store, project pages, about, footer all move in; the bar
       pads the same. (portrait insets are 0 there, so this costs nothing upright.)
     · type and chrome a step smaller — 340px is not much height
     · landing: the wordmark and the intro step out and the reel takes the stage; and the head
       script opens the landing in grid view on a phone on its side, so it scrolls down, not across
     · about: the portrait beside the words again, as on a desktop, not stacked over them */
@media (max-height: 480px) and (orientation: landscape) {
  :root {
    --bar-h: 44px;
    --t-editorial: 15px;
    --t-title: clamp(30px, 5.5vw, 60px);
    --t-mail: clamp(26px, 5vw, 60px);
  }
  /* --sx: the side inset — the gutter, or the safe area where the camera lies, whichever is more.
     the body pads by the difference, so every page's own gutter lands at --sx; the bands below
     that carry a colour pull back out to the edge of the display and pad their content by --sx */
  :root { --sx: max(var(--gutter), env(safe-area-inset-left, 0px), env(safe-area-inset-right, 0px)); }
  body { padding-left: calc(var(--sx) - var(--gutter)); padding-right: calc(var(--sx) - var(--gutter)); }
  .bar { padding-left: var(--sx); padding-right: var(--sx); }
  .about__clients { margin-inline: calc(-1 * var(--sx)); padding-inline: var(--sx); }
  .credits, .bag__band--lines, .product { margin-inline: calc(var(--gutter) - var(--sx)); padding-inline: var(--sx); }
  /* the bar stays, at the very top, glass: Safari is full-screen on a phone on its side, so the
     island model — the bar scrolling away — has nothing to give; on every page, every view */
  :root[data-page] body .bar { position: fixed !important; top: 0 !important; }
  /* landing: grid is the only picture view on a phone on its side — the reel is not offered, so
     the button that would leave grid for it is not shown; index stays */
  body.lab #grid-toggle { display: none; }
  body.lab .hero__intro, body.lab .hero__name { display: none; }
  body.lab .labstage { grid-template-rows: minmax(0, 1fr); padding-top: calc(var(--bar-h) + var(--gutter) * 0.5); }
  :root[data-view="grid"] body.lab .labstage { grid-template-rows: auto; }
  :root[data-view="grid"] body.lab .reel { padding-bottom: var(--gutter); }
  /* index view: the list runs to the foot — no wordmark, no fade */
  :root[data-view="index"] body.lab .hero__intro, :root[data-view="index"] body.lab .hero__name, :root[data-view="index"] body.lab::after { display: none; }
  :root[data-view="index"] body.lab main { padding-bottom: var(--gutter); }
  /* about: two columns, the portrait to the left of the words and as tall as they are; the words a
     step smaller; no arrow */
  /* the portrait column has a definite width, and the picture is fitted INSIDE the column × the
     row (object-fit) — as tall as the words, at its own ratio, hugging them. deriving the width from
     the height (auto column, height 100%, aspect-ratio) was circular: on the phone the column came
     out at nothing and the picture lay over the words (Louie, 2026-08-18) */
  .about__intro { --t-editorial: 14px; height: auto; min-height: calc(100svh - var(--safe-top)); grid-template-columns: minmax(0, 34vw) minmax(0, 46ch); column-gap: clamp(20px, 3vw, 48px); row-gap: 0; align-content: center; align-items: stretch; justify-content: center; padding-top: calc(var(--bar-h) + var(--gutter)); padding-bottom: var(--gutter); }
  .about__portraits { min-width: 0; }
  .about__portrait.is-on { height: 100%; }
  .about__portrait .plate { width: 100%; height: 100%; aspect-ratio: auto; max-height: none; margin: 0; background: transparent; }
  .about__portrait img { object-fit: contain; object-position: right center; }
  .about__portrait--empty .plate { width: min(100%, 30vw); }
  .about__words { gap: clamp(14px, 3vh, 24px); }
  .about__more-arrow { display: none; }
  /* product: two columns, the part to the left and sticking, the words to the right — as on a desktop */
  .product { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); grid-template-rows: auto 1fr; }
  .product__back { display: none; } .product__info > .backlink { display: inline-flex; }   /* two columns again: the words' link */
  .product .bench { position: sticky; top: calc(var(--bar-h) + var(--gutter)); grid-column: 1; grid-row: 1; }
  .product__images { grid-column: 1; grid-row: 2; align-self: start; }
  .product__info { grid-column: 2; grid-row: 1 / 3; }
  .product:has(.product__images:not([hidden])) .bench { position: static; }
}
