/* ─────────────────────────────────────────────────────────────────────────
   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 */
  /* the benches a part is shown on. Lifted well clear of the bands on carbon: a matte black part
     against #111113 is a silhouette, and lightening the plastic instead would lie about it
     (Louie, 2026-08-23: "make the background brighter?") */
  --model-bench: #2b2c31;
  /* The round header buttons. Louie's parents could not find them (2026-08-19): the chip was four
     shades off the paper and the glyph was grey. The chip is now the end of the scale — pure white
     on light paper, the lightest dark surface here — and the glyph is at full strength, with a
     hairline so the shape reads before the mark does. The paper itself is unchanged. */
  --control: #ebe9e4;
  --control-ink: #0a0a0b;
  --control-ring: rgba(10, 10, 11, 0.10);
  --control-on: #0a0a0b;          /* pressed, and under the pointer: the chip inverted */
  --control-on-ink: #ebe9e4;
  --control-on-ring: rgba(235, 233, 228, 0.38);
  --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);
  /* the frame above the controls: a shade less than the frame at the sides. A full gutter above
     read as too far from the top, 12px read as no frame at all — this is halfway between the two
     (Louie, 2026-08-19). the bar's height follows from it, so the two are one decision. */
  --bar-pad: clamp(13px, 1.5vw, 22px);
  /* the round controls: the size Safari gives its own, so they read as controls and not as marks
     printed on the paper (Louie, 2026-08-19). barely up from 28 on a phone, where the bar is
     already close to the screen's edges. the bar's height follows from it. */
  --btn: 30px;
  --bar-h: calc(var(--bar-pad) * 2 + var(--btn));

  /* 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;
  --model-bench: #e9e9e6;
  --control: #ffffff;
  --control-ink: #101012;
  --control-ring: rgba(16, 16, 18, 0.10);
  --control-on: #101012;
  --control-on-ink: #ffffff;
  --control-on-ring: transparent;
  --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;
  --model-bench: #e9e9e6;
  --control: #ffffff;
  --control-ink: #101012;
  --control-ring: rgba(16, 16, 18, 0.10);
  --control-on: #101012;
  --control-on-ink: #ffffff;
  --control-on-ring: transparent;
  --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); }

/* 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;
  /* the chip inverted, ringed in the chip's own colour: the badge sits half on the white chip and
     half on the paper, and an ink badge would have vanished into the white one (2026-08-19) */
  background: var(--control-on); color: var(--control-on-ink);
  box-shadow: 0 0 0 1.5px var(--control);
  font-family: var(--mono); font-style: normal; font-size: 8.5px; font-weight: 500; letter-spacing: 0.02em; line-height: 1;
}

/* the two round controls: index and paper */
.bar__btn {
  display: inline-grid; place-items: center;
  width: var(--btn); height: var(--btn);
  border-radius: 50%;
  background: var(--control);
  color: var(--control-ink);
  /* no ring either: white on the paper is enough of a control, and the grey edge was the one
     thing on the bar that looked drawn on (Louie, 2026-08-19) */
  transition: background 0.3s var(--ease), color 0.3s var(--ease);
}
.bar__btn svg { width: 12px; height: 12px; display: block; }
/* the index is the overview of everything — the one control worth saying out loud, because an
   icon among icons is something you have to already know to look for (Louie's mother, 2026-08-19).
   a chip that says its name where there is room; the round chip alone on a phone, where there is
   not. */
.bar__word { font-family: var(--sans); font-size: 12px; font-weight: 500; letter-spacing: 0.01em; line-height: 1; padding-bottom: 1px; }
.bar__index { width: auto; padding: 0 12px 0 10px; grid-auto-flow: column; gap: 6px; border-radius: 999px; }
@media (min-width: 720px) {
  :root { --btn: 32px; }
  .bar__btn svg { width: 13px; height: 13px; }
  .bar__index { padding: 0 13px 0 11px; gap: 7px; }
}
.bar__btn svg rect:not([fill]), .bar__btn svg path:not([fill]):not([stroke]) { fill: currentColor; }
/* while it is on, and under a real pointer: the chip inverted — so "on" is never the resting chip.
   the hover is behind (hover: hover) because a tap on a phone leaves :hover standing, and a
   control that stays inverted after a tap reads as switched on when it is not */
.bar__btn[aria-pressed="true"] {
  background: var(--control-on);
  color: var(--control-on-ink);
  box-shadow: inset 0 0 0 1px var(--control-on-ring);
}
@media (hover: hover) {
  .bar__btn:hover {
    background: var(--control-on);
    color: var(--control-on-ink);
    box-shadow: inset 0 0 0 1px var(--control-on-ring);
  }
}


/* ── 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 var(--name-mr, -0.063em) 0 var(--name-ml, -0.071em);   /* optical: the L's and B's side-bearings, so the ink sits on both gutters — measured by slate.js and remembered for the next load */
  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) calc(14ch + 14 * var(--track-micro));
  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) calc(14ch + 14 * var(--track-micro)); }
  .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 */
/* the same rhythm as the work list — number, name, type, whose, when — with the buy control after
   it, which is the one thing the work list does not have (Louie, 2026-08-19) */
.ledger--store .ledger__head, .ledger--store li > div { grid-template-columns: 3ch minmax(0, 2.2fr) minmax(0, 1.1fr) minmax(0, 1.2fr) calc(14ch + 14 * var(--track-micro)) 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"] .about__editorial,
[data-view="index"] .about__sign,
[data-view="index"] .about__clients { display: none; }
[data-view="index"] .ledger { display: block; }
/* On a project page the frame does not move and does not fade: the words come up over it. In index
   view the media is taken out of the flow — absolute with no offsets, so it keeps the exact place
   it already occupied — and the credits and the list sit across it on their own paper. The
   article clips, so a frame taller than the two of them is cut off rather than hanging below
   (Louie, 2026-08-19).

   `is-sheeting` is the same layout, held by slate.js for the length of the move in EITHER
   direction: the list still there to slide off, the frame still out of the flow, the article still
   positioned and clipping. slate.js measures the distance and animates it, so the sheet starts from
   exactly where it was and lands exactly where it will rest — a keyframe in vh started somewhere
   else, and that was the jump at each end. The article's height is held by slate.js too
   (min-height), so nothing below it — the footer — moves until the sheet has landed. */
[data-view="index"] .work, .work.is-sheeting { position: relative; overflow: hidden; }
/* the footer waits below the fold while the index is open: even when the list is short enough to
   share the screen, the email is a scroll away — cleaner, and the close-up glide never has to
   thread it through the moving sheet (Louie, 2026-08-20) */
[data-view="index"] .work { min-height: 100svh; }
/* and it earns its arrival: nothing of it while a sheet is in motion, and in index view nothing of
   it until the reader reaches the very foot of the list — then it fades in (Louie, 2026-08-20) */
/* html-prefixed: the router's page fade (.js .foot, 0.14s) sits later in this file and was
   quietly winning — the footer's own arrivals ran at 0.14s however this line read (2026-08-20) */
html.js .foot { transition: opacity 0.8s var(--ease); }
html.js.is-navigating .foot { transition: opacity 0.14s var(--ease-io); }   /* a leaving page's footer still goes with it */
html.sheeting .foot,
html[data-view="index"]:not(.at-foot) .foot { opacity: 0; pointer-events: none; }
/* at REST under the sheet the frame is gone entirely: the breathing room between the head and the
   band shows what is behind it, and a light poster's top edge flashed there the moment it decoded
   (Louie, 2026-08-20 — MATE Bike's sky). during the ride it is present and the sliver above the
   band is the poster sliding under — that part is the point. */
[data-view="index"] .work .work__media { display: none; }
.work.is-sheeting .work__media {
  display: grid;              /* not none, and not collapsed: it stays exactly as it was */
  position: absolute; left: 0; right: 0;
  z-index: 0; pointer-events: none;
}
[data-view="index"] .work .credits, .work.is-sheeting .credits,
[data-view="index"] .work .ledger, .work.is-sheeting .ledger { position: relative; z-index: 1; }
[data-view="index"] .work .ledger, .work.is-sheeting .ledger { display: block; background: var(--paper); }
/* while the sheet moves, the list's paper begins a hair under the band's foot rather than exactly
   at it: two layers moving together meet on a fractional pixel, and a seam of what is behind them
   showed through the join now and then (Louie, 2026-08-19). the band is above the list, so the
   overlap is never seen. */
.work.is-sheeting .ledger { margin-top: -2px; padding-top: calc(var(--gutter) + 2px); }
/* the sheet stops where the picture starts, not where the picture's box starts: the frame's own
   room above it is what the words were sitting in (Louie, 2026-08-19) */
[data-view="index"] .work .credits, .work.is-sheeting .credits { margin-top: calc(var(--t-editorial) * 1.35); }
/* On the way OUT, everything the index was hiding stays hidden until the sheet has landed — they
   came back the moment the view changed, appeared between the credits and the list, and pushed the
   list off the sheet. On the way IN they are not hidden but taken out of the flow where they stand
   (slate.js), under the sheet, and fade as it rises over them — hidden at once, they vanished a
   breath before the sheet reached them (Louie, 2026-08-19). */
.work.is-sheeting:not(.is-sheet-in) .work__stills,
.work.is-sheeting:not(.is-sheet-in) .work__grid,
.work.is-sheeting:not(.is-sheet-in) .work__next { display: none; }
/* no row stagger on a project page: the sheet is the motion there, and the roll on top of it read
   as a glitch on a wide screen (Louie, 2026-08-19 — tried and taken back the same night; the
   landing keeps its roll, where the list arrives on its own) */
@media (hover: hover) { .js[data-view="index"] .work .ledger li, .js[data-view="index"] .work .ledger__head { animation: none; } }
/* One band, one height, for every project: slate.js measures the tallest facts of all of them —
   in place, at this width — and the band keeps that room, so a project with a longer crew than
   the last cannot move the list under the pointer (Louie, 2026-08-19). Only where there is a
   pointer. Should one ever outgrow what the screen can give the band, it gets a way to see the
   rest instead of a taller band. */
@media (hover: hover) {
  [data-view="index"] .work .credits, .work.is-sheeting .credits { height: var(--band, auto); overflow: hidden; }
  [data-view="index"] .work .credits.is-open { height: auto; }
}
.credits__more {
  position: absolute; right: var(--gutter); bottom: 10px; z-index: 2;
  font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro);
  color: var(--ink-2); background: var(--level-1); border: 0; padding: 6px 10px; border-radius: 999px; cursor: pointer;
}
@media (hover: hover) { .credits__more:hover { color: var(--ink); } }
/* on a phone the toggle reloads into the other view, and whatever that view's big block is used to
   appear at full strength in one frame — the list on the way in, the tiles on the way out (Louie,
   2026-08-20). the reload marks the arrival (lj-vswap, worn pre-paint by the head script) and the
   arriving blocks fade in instead. */
.vswap .ledger, .vswap .tiles, .vswap .work__media, .vswap .work__stills, .vswap .work__grid,
.vswap .about__editorial, .vswap .about__clients { animation: fade 0.32s var(--ease) both; }
/* the footer's arrival fade only outside index view: fade's fill pins opacity at 1 when it ends,
   which beat the index-view hide and parked the email on the store's list (2026-08-20) */
html.vswap:not([data-view="index"]) .foot { animation: fade 0.32s var(--ease) both; }
/* On a phone the index is the list and nothing else. There is no running a pointer down it, so the
   title never changes and the band of facts is just the page you were already on — and between
   them they take most of the screen before a single row is read (Louie, 2026-08-19). */
@media (max-width: 719px) {
  [data-view="index"] .work .work__head,
  [data-view="index"] .work .credits,
  [data-view="index"] .work .work__media { display: none; }
  [data-view="index"] .work .ledger { padding-top: calc(var(--bar-h) + var(--gutter)); }
}
.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.
   at every width now: the name stays where it is on a phone too, so the rows need the same fade
   under it there (Louie, 2026-08-19). */
: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%);
}
/* the name holds its place on a phone as well. It used to be re-ordered under the list there, and
   under the list on a phone is off the screen: clicking Index made the name vanish and then turn
   up again at the end of a scroll, which reads as a fault rather than as a move (Louie,
   2026-08-19). Fixed at the foot on the glass, the rows fading out under it, is one thing that
   does not move at all. */

/* ── 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) 0;
  display: grid;
  gap: var(--gutter);
}
/* one blank line of the statement under the words, and then the frame: the leading alone was too
   tight, two gutters' worth was too much (Louie, 2026-08-19) */
.work__head + .work__media { padding-top: calc(var(--t-editorial) * 1.35); }
.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);
}
/* the same control as the ones in the bar — white, round, no ring — a size up because this one is
   the thing you came to press (Louie, 2026-08-19) */
.player .play > span {
  display: inline-grid; grid-auto-flow: column; gap: 1em; align-items: center;
  height: calc(var(--btn) + 10px);
  padding: 0 20px 0 18px;
  border-radius: 999px;
  background: var(--control); color: var(--control-ink);
  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 { transform: scale(1.04); } }
/* once the film is playing the poster is gone — not merely covered. the frame the poster was cut
   to and the film's own ratio differ by a hair, so `object-fit: contain` on the video left a sliver
   of the poster showing along one edge (Louie, 2026-08-19). the box keeps its shape from --r, and
   the video fills it on the bench. */
.player.is-live .play { display: none; }
.player.is-live picture, .player.is-live > img { display: none; }
.player video { object-fit: contain; background: #000; }

/* 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: absolute; left: 0; right: 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 */
/* the blur comes and goes with the paper — backdrop-filter animates — rather than popping on at
   full strength the moment the box opens (Louie, 2026-08-19) */
.lightbox { backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); transition: opacity 0.6s var(--ease), backdrop-filter 0.7s var(--ease), -webkit-backdrop-filter 0.7s var(--ease); }   /* slower and smoother (Louie, 2026-08-19): the paper over a long half second, the blur over most of one */
.lightbox.is-open { opacity: 1; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
.lightbox__track {
  position: absolute; inset: var(--lb-over, 0px) 0 var(--lb-under, 0px);   /* the screen's own part of the over-covering box (stage.js place()) */
  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. */
/* the caption's line is kept for as long as the box is open on the reel, whether the picture on
   show has a title or not: measured per picture, the picture resized under the reader every time
   a swipe crossed from one that has a project to one that has not (Louie, 2026-08-19) */
.lightbox {
  /* the top band holds the close, above the picture rather than on it (Louie, 2026-08-23: "x
     outside the image"). At the old gutter there were sixteen pixels up there on a phone and the
     button had nowhere to stand that was not the photograph. */
  --lb-top: max(var(--gutter), 54px);
  --lb-foot: calc(clamp(14px, 2vh, 24px) + 36px + 14px);   /* pill offset + pill + a breath */
}
.lightbox.has-caption { --lb-foot: calc(clamp(14px, 2vh, 24px) + 36px + 14px + 40px); }   /* and the caption line above it */
/* 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: 40px; --lb-foot: calc(12px + 36px + 8px); }
  .lightbox.has-caption { --lb-foot: calc(12px + 36px + 8px + 30px); }
  .lightbox__bar { bottom: calc(var(--lb-under, 0px) + 12px); }
  .lightbox__caption { bottom: calc(var(--lb-under, 0px) + 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;
}
/* The way out: in the dark just above the picture's top-right corner — outside the photograph, but
   still on its corner rather than in a screen corner the picture may be nowhere near. Round, like
   every control; the bar's white, and opaque for the bar's reason — a translucent step off the
   paper goes missing over a bright picture. */
.lightbox__close {
  position: absolute; bottom: calc(100% + 11px); right: 0; z-index: 2;
  width: 30px; height: 30px; padding: 0; border: 0; border-radius: 999px;
  display: grid; place-items: center;
  background: var(--control); color: var(--control-ink);
  cursor: pointer; -webkit-appearance: none; appearance: none;
  opacity: 0.82;
  transition: opacity 0.18s var(--ease-io), scale 0.18s var(--ease-io);
}
.lightbox__close svg { width: 15px; height: 15px; display: block; }
.lightbox__close svg path { stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; fill: none; }
@media (hover: hover) { .lightbox__close:hover { opacity: 1; scale: 1.06; } }
.lightbox__close:focus-visible { opacity: 1; }
/* a pinch scales the frame, and the button rides it — it steps aside rather than sliding about */
.lightbox__frame.is-pinching .lightbox__close { opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: reduce) { .lightbox__close { transition: none; } }

.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; }
/* the pill and the caption stand "fixed" — but a box with a backdrop-filter is the containing
   block of its fixed children, and the box runs past the screen by --lb-under (stage.js place()):
   their bottom adds that overrun, or they stand below the screen (they did, 2026-08-19) */
.lightbox__bar {
  position: fixed; left: 50%; translate: -50% 0; bottom: calc(var(--lb-under, 0px) + clamp(14px, 2vh, 24px));
  display: flex; align-items: center; gap: 6px; padding: 5px;
  border-radius: 999px;
  /* the white control the bar's buttons are made of, and opaque: a translucent step off the paper
     went missing over a bright picture (Louie, 2026-08-19). white in both papers, dark ink on it,
     so there is no blur to fade in either — nothing shows through it any more. */
  background: var(--control);
  cursor: default;
}
/* 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: color-mix(in srgb, var(--control-ink) 62%, transparent);
  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 { height: 26px; display: inline-grid; place-items: center; color: var(--control-ink); }
.lightbox__goto > span { padding: 0 12px; }   /* the padding is the text's, not the link's: on the link it was a floor the collapse could not go under */
/* the way into the project comes and goes as the swipe crosses from a picture that has one to a
   picture that has not — so the pill grows and shrinks around it rather than jumping a width
   (Louie, 2026-08-19). 0fr → 1fr animates to the link's own width, exactly, and the link fades
   with it; the negative margin takes back the pill's gap while the column is shut. */
.lightbox__gotowrap { display: grid; grid-template-columns: 0fr; margin-left: -6px; transition: grid-template-columns 0.34s var(--ease-io), margin-left 0.34s var(--ease-io); }
.lightbox__gotowrap.is-on { grid-template-columns: 1fr; margin-left: 0; }
.lightbox__gotowrap > .lightbox__goto { min-width: 0; overflow: hidden; opacity: 0; transition: opacity 0.24s var(--ease-io), color 0.18s, background 0.18s; }
.lightbox__gotowrap.is-on > .lightbox__goto { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .lightbox__gotowrap, .lightbox__gotowrap > .lightbox__goto { transition: none; } }
.lightbox.is-settling .lightbox__gotowrap, .lightbox.is-settling .lightbox__gotowrap > .lightbox__goto, .lightbox.is-settling .lightbox__caption { transition: none; }
.lightbox__step:hover, .lightbox__goto:hover { color: var(--control); background: var(--control-ink); }
.lightbox__count { font-family: var(--mono); font-size: var(--t-micro); letter-spacing: var(--track-micro); color: color-mix(in srgb, var(--control-ink) 45%, transparent); 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(var(--lb-under, 0px) + clamp(14px, 2vh, 24px) + 36px + 14px);   /* the box's overrun + 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; }
/* on every screen — the desktop lightbox showed no title where the phone's did (Louie, 2026-08-18).
   a picture without a title does not take the line away: the words fade off it and back on. */
.lightbox.has-caption .lightbox__caption { display: block; opacity: 1; transition: opacity 0.3s var(--ease-io); }
.lightbox.has-caption .lightbox__caption.is-off { opacity: 0; }
@media (prefers-reduced-motion: reduce) { .lightbox__caption { transition: none; } }
/* 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 */
/* no outline on the still the lightbox is showing: under the frost it was invisible, and in the
   frame before the frost it flashed on the picture just tapped (Louie, 2026-08-19). data-current
   stays on the element for scripts and assistive tech. */
@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; }
/* the film's frame too: it was capped to the screen's height, which on a 4:3 film at a wide screen
   stopped it well short of the right edge and read as a fault, not a choice (Louie, 2026-08-19) */
.work__hero .player { 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); }
/* people credited without a role: the name alone, set as the names beside it, one to a line */
.credits__names { list-style: none; margin: 0; padding: 0; }
.credits__names li { font-family: var(--sans); font-size: var(--t-ui); font-weight: 500; color: var(--ink); padding: 7px 0; }
.credits dl + .credits__names li:first-child { padding-top: 7px; }

/* 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(--model-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); }
/* in index view it is the page, not a band under the tiles: the same gutter above it and the same
   left edge as the work list, which the store section's own padding already sets */
[data-view="index"] .store .ledger { padding: var(--gutter) 0; }

/* 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;
  container-type: inline-size;   /* so the tag's contents can measure the bench, not the window */
}
.bench lj-model { position: absolute; inset: 0; width: 100%; height: 100%; }
/* a picture of the part shown in the bench, in the viewer's place: the pictures under the bench
   are other views of the same box, not a lightbox (Louie, 2026-08-19) */
.bench__shot { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; opacity: 0; transition: opacity 0.22s var(--ease-io); }
.bench.is-shot .bench__shot { opacity: 1; }
/* the viewer stops drawing while a picture is up — display:none and its observer reports it gone,
   so the loop skips it; visibility alone would have kept it turning behind the picture */
.bench.is-model-off lj-model { display: none; }

@media (prefers-reduced-motion: reduce) { .bench__shot { transition: none; } }
.bench__tag {
  position: absolute; left: 12px; right: 12px; top: 12px;
  display: flex; justify-content: flex-end; gap: 1em;   /* only the viewer's own controls now */
  font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro);
  color: var(--ink-3); pointer-events: none;
}

/* the right of the tag: the drag hint, and — when the part has a context body — the toggle that
   puts it on the thing it fits. the toggle is a word with a rule under it when it is on, the same
   mark the masthead uses for the page you are on; no capsule, nothing that reads as an app. */
.bench__ctl { display: inline-flex; align-items: center; gap: 1.1em; }
/* Three options in one capsule — media is square-cornered and controls are round ("buttons should
   be round or just plain text", Louie 2026-08-23). The one showing is a full inversion, so there is
   no guessing which of the three you are looking at. */
.bench__stages {
  display: inline-flex; align-items: stretch; gap: 2px; padding: 2px;
  background: var(--panel); box-shadow: var(--panel-shadow); border-radius: 999px;
  pointer-events: auto;
}
.bench__stages button {
  margin: 0; padding: 5px 11px 4px; appearance: none; -webkit-appearance: none;
  font: inherit; letter-spacing: inherit; text-transform: inherit; line-height: 1;
  background: none; color: var(--panel-ink); opacity: 0.45;
  border: 0; border-radius: 999px; cursor: pointer;
  transition: background 0.22s var(--ease-io), color 0.22s var(--ease-io), opacity 0.22s var(--ease-io);
}
.bench__stages button[aria-pressed="true"] { background: var(--ink); color: var(--paper); opacity: 1; }
@media (hover: hover) { .bench__stages button:not([aria-pressed="true"]):hover { opacity: 0.8; } }
@media (prefers-reduced-motion: reduce) { .bench__stages button { transition: none; } }
.bench.is-shot .bench__ctl { visibility: hidden; }   /* nothing to turn or to fit while a picture is up */
/* The hint goes before the control does. It has to be measured against the BENCH, not the window:
   on a product page the bench is about two fifths of a wide window, so a viewport breakpoint reads
   "plenty of room" while the pill is already sitting on top of the hint. */
@container (max-width: 460px) { .bench__hint { display: none; } }
@media (max-width: 719px) { .bench__hint { display: none; } }


.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(--model-bench); }
/* the part's pictures, from Shopify, under the bench: a row of views of the box above — a tap puts
   one in the bench in the viewer's place, and the 3D chip puts the viewer back (Louie, 2026-08-19).
   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: 8px; margin-top: calc(var(--gutter) * -0.62); }   /* they belong to the bench, so they sit under it, not a gutter away */
.product__image {
  margin: 0; padding: 0; appearance: none; -webkit-appearance: none; font: inherit; color: var(--ink-3);
  border: 1px solid transparent; flex: 0 0 84px; aspect-ratio: 1 / 1; max-width: 100%;
  position: relative; background: var(--model-bench); display: block; cursor: pointer;
  transition: border-color 0.18s var(--ease-io);
}
.product__image img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.product__image[aria-pressed="true"] { border-color: var(--ink); }
@media (hover: hover) { .product__image:not([aria-pressed="true"]):hover { border-color: var(--line-2); } }
.product__image--model > span {
  position: absolute; inset: 0; display: grid; place-items: center;
  font-family: var(--mono); font-size: var(--t-micro); text-transform: uppercase; letter-spacing: var(--track-micro);
}
.product__image[aria-pressed="true"].product__image--model > span { color: var(--ink); }
@media (max-width: 719px) { .product__image { flex: 0 0 68px; } }
@media (prefers-reduced-motion: reduce) { .product__image { transition: none; } }
.product__info .backlink { margin-bottom: 4px; }
/* the way back is the first thing on the page, above the part — where it is on a project page,
   not beside the title (Louie, 2026-08-19). one link, at every width. */
.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 auto 1fr; }
  .product__back { grid-column: 1 / -1; grid-row: 1; }   /* across the top, over the part and the words alike */
  .product .bench, .product__info { grid-row: 2; }
  .product__images { grid-row: 3; }
  .product .bench { position: sticky; top: var(--gutter); grid-column: 1; }   /* the bar scrolls away with the page now, so nothing to clear */
  .product__images { grid-column: 1; align-self: start; }
  .product__info { grid-column: 2; grid-row: 2 / 4; }
  /* with pictures under it the bench does not stick — but it stays a positioned box: static, the
     3D part inside it (absolute, inset 0) measured itself against the viewport and drew itself over
     the whole page (Louie, 2026-08-19) */
  .product:has(.product__images:not([hidden])) .bench { position: relative; top: auto; }
}
.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; }
/* the about's words, all of them, at the body size the store's own words use — one size, no
   statement line (Louie, 2026-08-20) */
.about__editorial { font-size: var(--t-body); line-height: 1.45; color: var(--ink-2); }
.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%; }
/* the sign-off reads as one more line of the words, not a stamp: the mono face at 11px carried
   more weight than the 13px body beside it (Louie, 2026-08-20) */
.about__sign { color: var(--ink-2); font-family: var(--sans); font-size: var(--t-body); 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) */
/* two things fade the page's content on the constant paper, and share one transition:
   · the index toggle on a phone (slate.js): html.is-swapping — main out over a beat, the swap, main back
   · the soft-navigation router (nav.js, branch dev, 2026-08-19): html.is-navigating — main, the
     footer and the lab's landing nav out while the next page is fetched, then in again as the new
     content, on the same paper, so no frame between two pages is ever grey
   out is quick (0.14s) and in is slower (0.22s): a leaving page should be gone before it is missed,
   an arriving one should settle. the index swap keeps its 0.22s both ways — the class comes off
   the frame after the swap, so it is timed as the slower, arriving beat. */
/* the index swap on a phone fades only what is changing: the pictures out, the list in. It used to
   fade the whole of <main>, so the words that stay — the landing's intro, the store's own words, a
   project's title — went with them and came back, which reads as a glitch rather than as a swap
   (Louie, 2026-08-19). A project's frame does not fade either: the list comes up over it. */
.is-swapping .reel,
.is-swapping .feed,
.is-swapping .tiles,
.is-swapping .work__grid,
.is-swapping .work__stills,
.is-swapping .work__next,
.is-swapping .about__editorial,
.is-swapping .about__sign,
.is-swapping .about__clients,
.is-swapping .ledger { opacity: 0; }
.js .reel, .js .feed, .js .tiles, .js .work__grid, .js .work__stills, .js .work__next,
.js .about__editorial, .js .about__sign, .js .about__clients, .js .ledger { transition: opacity 0.22s var(--ease-io); }
.is-navigating main, .is-navigating .foot, .is-navigating .labnav { opacity: 0; }
.js main, .js .foot, .js .labnav { transition: opacity 0.14s var(--ease-io); }
.js:not(.is-navigating) main { transition-duration: 0.22s; }
/* the router moves focus to the new <main> so a screen reader starts at the top of the page it
   asked for; a focus ring around the whole page is not a thing to see (2026-08-19) */
main:focus, main:focus-visible { outline: none; }
.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 page-to-page transition here, on purpose (2026-08-19). The browser's cross-document
   view transition was tried on 2026-08-18: it cross-fades two whole-page snapshots, and a page
   with a dark hero into light paper passed through a grey frame; and it ran only when the next
   document was ready in time, so it came and went. A fade by hand before the browser goes was
   tried too: a fade, then the browser's own paint — a flash. Both are gone: pages cut cleanly,
   which is at least reliable, until the soft-navigation layer (slate/js/nav.js, on branch dev)
   owns page-to-page motion: it fades CONTENT on the constant paper, so no frame is ever grey.
   (In-page transitions — index, grid, paper — are kept: they are same-document.) The rules the
   router needs are just above: .is-navigating fades the content out, .js:not(.is-navigating)
   fades it back in. */

@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) {
  .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; }
/* (the reel page was made a pixel and then 420px taller than the screen, 2026-08-18/19, to see
   whether iPhone Safari would then let a page reached from it scroll behind the Dynamic Island.
   it does not: that decision is made when a page LOADS. hence nav.js navigates plainly on touch.) */
/* a picture that lands after the page it belongs to fades in, rather than popping (nav.js) */
.js main img.is-fading { opacity: 0; transition: opacity 0.35s var(--ease); }
.js main img.is-fading.is-loaded { opacity: 1; }
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;   /* the one exception: on a phone on its side the height is the scarce thing */
    --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 auto 1fr; }
  .product__back { grid-column: 1 / -1; grid-row: 1; }
  .product .bench { position: sticky; top: calc(var(--bar-h) + var(--gutter)); grid-column: 1; grid-row: 2; }
  .product__images { grid-column: 1; grid-row: 3; align-self: start; }
  .product__info { grid-column: 2; grid-row: 2 / 4; }
  .product:has(.product__images:not([hidden])) .bench { position: relative; top: auto; }
}
