/* ==========================================================================
   Website 2.0 — "A monograph of places, drawn like a building."
   Website 2.0 (v4) stylesheet, loaded only by pages the generator builds with the
   v4 design (admin/lib/v4.js). v3 pages keep site.css. Spec: docs/design/WEBSITE-2-CREATIVE-DIRECTION.md
   Three devices only: the Title Block, the Cut, Plates on a datum.
   ========================================================================== */

@font-face { font-family: "Inter"; font-weight: 400; font-display: swap; src: url("fonts/inter-latin-400-normal.woff2") format("woff2"); }
@font-face { font-family: "Inter"; font-weight: 600; font-display: swap; src: url("fonts/inter-latin-600-normal.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-weight: 400; font-display: swap; src: url("fonts/ibmplexmono-400-latin.woff2") format("woff2"); }
@font-face { font-family: "IBM Plex Mono"; font-weight: 500; font-display: swap; src: url("fonts/ibmplexmono-500-latin.woff2") format("woff2"); }

/* ---------- tokens (§23) ---------- */
:root {
  --ink: #000; --page: #fff; --rule: rgba(0, 0, 0, .15); --muted: #6b6b6b;
  --err: #b3261e; --room: #0a0a0a; --room-ink: #f2f2f0; --room-rule: rgba(255, 255, 255, .18); --room-muted: #a3a3a0;
  --cols: 12; --m: clamp(16px, 3.2vw, 48px); --g: clamp(12px, 1.4vw, 24px);
  --off-s: clamp(24px, 3vw, 48px); --off-m: clamp(48px, 6vw, 96px); --off-l: clamp(96px, 12vw, 192px);
  --sec: clamp(96px, 10vw, 144px);
  --f-sans: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
  --f-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
  --t-cut: clamp(72px, 15vw, 260px); --t-title: clamp(32px, 4.2vw, 64px); --t-section: clamp(20px, 1.6vw, 26px);
  --t-body: 17px; --t-meta: 11px;
  --tr-cut: -.055em; --tr-title: -.03em; --tr-meta: .08em;
  --ease: cubic-bezier(.2, .7, .2, 1); --t-plate: 520ms; --t-block: 280ms; --t-quick: 200ms;
  --cap-h: 88svh; /* no plate taller than this: verticals dominate without cropping */
  color-scheme: light;
}
:root[data-theme="dark"] {
  --err: #f2b8b5; --ink: #f2f2f0; --page: #0a0a0a; --rule: rgba(255, 255, 255, .16); --muted: #9a9a96;
  --room: #000; --room-rule: rgba(255, 255, 255, .14);
  color-scheme: dark;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --err: #f2b8b5; --ink: #f2f2f0; --page: #0a0a0a; --rule: rgba(255, 255, 255, .16); --muted: #9a9a96;
    --room: #000; --room-rule: rgba(255, 255, 255, .14); color-scheme: dark;
  }
}

/* ---------- base ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body { margin: 0; background: var(--page); color: var(--ink); font: 400 var(--t-body)/1.55 var(--f-sans); overflow-x: clip; font-feature-settings: "ss01", "cv11"; }
a { color: inherit; text-decoration: none; }
a:hover, a:focus-visible { text-decoration: underline; text-underline-offset: .2em; text-decoration-thickness: 1px; }
:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
img, video { display: block; width: 100%; height: auto; }
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
p, h1, h2, h3, dl, dd, ol, ul, figure { margin: 0; }
ol, ul { padding: 0; list-style: none; }
.skip-link { position: absolute; left: var(--m); top: -60px; z-index: 50; background: var(--ink); color: var(--page); padding: 10px 14px; font: 500 var(--t-meta)/1 var(--f-mono); text-transform: uppercase; letter-spacing: var(--tr-meta); }
.skip-link:focus { top: 8px; }
.visually-hidden { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }

/* ---------- six type roles (§7) ---------- */
.cut { font: 600 var(--t-cut)/.82 var(--f-sans); letter-spacing: var(--tr-cut); white-space: nowrap; }
.t-title { font: 600 var(--t-title)/1.02 var(--f-sans); letter-spacing: var(--tr-title); }
.t-section { font: 400 var(--t-section)/1.2 var(--f-sans); letter-spacing: -.012em; max-width: 34ch; }
.t-body { font: 400 var(--t-body)/1.55 var(--f-sans); max-width: 62ch; }
.meta, .tb, .idx { font: 500 var(--t-meta)/1.4 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; font-variant-numeric: tabular-nums; }
.cap { font: 400 var(--t-meta)/1.45 var(--f-mono); letter-spacing: .02em; color: var(--muted); font-variant-numeric: tabular-nums; }

/* ---------- grid (§6): rail = cols 1-2, field = cols 3-12 ---------- */
.g { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--g); padding-inline: var(--m); }
.rail { grid-column: 1 / span 2; min-width: 0; }
.field { grid-column: 3 / -1; min-width: 0; }
.sec { padding-block: var(--sec) 0; }
.sec-label { color: var(--ink); }
.sec-label::before { content: ""; display: block; border-top: 1px solid var(--ink); margin-bottom: 8px; }

/* ---------- masthead + menu (§12) ---------- */
.masthead { align-items: baseline; padding-block: 22px; }
.mh-name { grid-column: 1 / span 2; }
.mh-pos { grid-column: 3 / span 4; color: var(--muted); }
.nav { grid-column: 7 / -1; display: flex; justify-content: flex-end; gap: clamp(14px, 2vw, 32px); } /* to the last column: the theme toggle used to hold 12 */
.nav [aria-current="page"] { text-decoration: underline; text-underline-offset: .35em; }
.mh-theme { grid-column: 12; justify-self: end; font-size: 14px; line-height: 1; width: 28px; height: 28px; margin: -8px -6px -8px 0; }
.mh-menu { display: none; }
.menu-sheet { border: 0; padding: 0; margin: 0; width: 100%; max-width: none; height: 100%; max-height: none; background: var(--page); color: var(--ink); }
.menu-sheet::backdrop { background: var(--page); }
.menu-sheet > .g { padding-block: 22px; }
.menu-sheet > .g > :last-child { grid-column: -2; justify-self: end; }
.menu-list { padding: 8vh var(--m) 0; }
.menu-list a { display: flex; align-items: baseline; gap: 16px; font: 600 var(--t-title)/1.1 var(--f-sans); letter-spacing: var(--tr-title); padding-block: 10px; border-bottom: 1px solid var(--rule); }
.menu-foot { position: absolute; bottom: 24px; left: 0; right: 0; }
.menu-foot > :last-child { grid-column: -3 / -1; justify-self: end; min-height: 44px; }

/* ---------- the Title Block (§9, device 1) ---------- */
.tb { border-top: 1px solid var(--ink); display: grid; grid-template-columns: auto 1fr; column-gap: 12px; }
.tb > div { grid-column: 1 / -1; display: grid; grid-template-columns: subgrid; padding: 6px 0 7px; position: relative; }
.tb > div::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; border-bottom: 1px solid var(--rule); transform-origin: left; }
.tb dt { color: var(--muted); font-weight: 500; }
.tb dd { overflow-wrap: anywhere; }
.tb a { text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: .25em; }
.tb a:hover { text-decoration-color: currentColor; }
.more { display: inline-block; margin-top: 12px; }

/* ---------- plates (§8, device 3) ---------- */
.pl { position: relative; min-width: 0; max-width: calc(var(--cap-h) * var(--ar, .6667)); }
.pl-media { display: block; position: relative; background: var(--rule) center / cover no-repeat; }
.pl-media picture, .pl-media img, .pl-media video { width: 100%; height: 100%; object-fit: cover; }
.pl-link { display: block; width: 100%; text-align: left; cursor: zoom-in; }
a.pl-link { cursor: pointer; }
.pl .cap { margin-top: 8px; display: flex; gap: 10px; }
.cap-n { color: var(--ink); white-space: nowrap; }
.cap-t { max-width: 40ch; }
.has-crop > .cap { margin-top: 22px; }

/* crop marks: one plate per page only (the cover plate) */
.crop { position: absolute; inset: -12px; pointer-events: none; z-index: 1; --l: 12px; --c: var(--ink);
  background:
    linear-gradient(var(--c), var(--c)) left top / var(--l) 1px no-repeat,
    linear-gradient(var(--c), var(--c)) left top / 1px var(--l) no-repeat,
    linear-gradient(var(--c), var(--c)) right top / var(--l) 1px no-repeat,
    linear-gradient(var(--c), var(--c)) right top / 1px var(--l) no-repeat,
    linear-gradient(var(--c), var(--c)) left bottom / var(--l) 1px no-repeat,
    linear-gradient(var(--c), var(--c)) left bottom / 1px var(--l) no-repeat,
    linear-gradient(var(--c), var(--c)) right bottom / var(--l) 1px no-repeat,
    linear-gradient(var(--c), var(--c)) right bottom / 1px var(--l) no-repeat; }

/* modules: rows + pauses (sequence.js). A row's plates share one height that fills the
   field: --h = (field width - gutters) / sum of aspect ratios, capped so an upright is
   never taller than the screen. The field itself is capped so a row of two uprights
   fills it exactly at the cap: every row then shares both edges of the field. */
.mods { position: relative; padding-block: var(--off-m); --gut: clamp(10px, 1.1vw, 16px); }
/* The field cap keeps a row of photos near screen height — rows take their height from the
   width they are given, and .mod .pl drops its own max-width, so this is the primary control
   (the 112svh on .row is only a backstop for extremes). */
.p-seq .mods, .room .mods, .grp-plates { max-width: calc(96svh * 4 / 3 + var(--gut)); container-type: inline-size; }
/* The HOME groups get a looser cap (2026-10-03). At 96svh it bound on ordinary laptop windows
   and pulled the photo blocks in from the right margin, so they stopped lining up with the hero
   and the portrait, which are flush right: short by 60px at 1512x860, 80px at 1440x800. At
   110svh both align and the tallest photo only goes from 75% to ~80% of the screen, and it
   still binds when it matters — 85% at 1440x620, where removing the cap entirely gave 104-112%.

   DELIBERATELY NOT applied to .p-seq .mods: a project page's sequence uses taller modules
   (solo plates and pairs, not three-across rows), so the same 110svh puts its photos at 110%
   of the screen where 96svh keeps them at 96%. Measured. The two surfaces need different
   numbers; do not merge these rules back together. */
.grp-plates { max-width: calc(110svh * 4 / 3 + var(--gut)); }
.grp-plates { --gut: clamp(10px, 1.1vw, 16px); }
.datum::before { content: ""; position: absolute; top: 0; bottom: 0; left: calc(var(--g) / -2); border-left: 1px solid var(--rule); }
.mods > .mod + .mod { margin-top: var(--off-m); }
.mod .pl { max-width: none; }
.row { display: flex; gap: var(--gut); align-items: flex-start; }
.row > .pl { flex: var(--ar) 1 0%; }
.mod--rows > .row + .row { margin-top: var(--gut); }
.mods .cap:not(:has(.cap-t)) { display: none; } /* numbers only on the cover and in the viewer: a number under some plates and not others reads as missing plates */
/* A pause pair fills the field with one gutter, on a shared bottom edge. The small plate
   is 0.62 of the big one's height when the screen is tall enough. On wide, short screens
   the big plate may run a little past the screen (108svh) so the small one stays clearly
   smaller (at most 0.78 of the big one): the pause must read as big and small. */
.mod--pair { display: flex; gap: var(--gut); align-items: flex-end; }
.mod--pair.is-flip { flex-direction: row-reverse; }
.mod--pair.is-narrow { justify-content: center; }
.mod--solo { display: flex; justify-content: center; }
.mod--solo > .pl { width: min(60%, calc(88svh * var(--ar))); }
.mod--film { justify-content: center; } /* a lone film, or films capped by the screen, sit centred */
@supports (width: 1cqi) {
  .row { --h: min(calc((100cqi - (var(--n) - 1) * var(--gut)) / var(--sum)), 112svh); } /* the field cap already keeps rows near screen height; this only stops extremes */
  .mod--film.row { --h: min(calc((100cqi - (var(--n) - 1) * var(--gut)) / var(--sum)), 80svh); }
  .row > .pl { flex: none; width: calc(var(--h) * var(--ar)); }
  .mod--pair { --h: min(calc((100cqi - var(--gut)) / var(--sum)), 108svh); }
  .mod--pair > .pl { width: calc(var(--h) * var(--ar)); }
  .mod--pair > .pl--small { width: min(calc(100cqi - var(--gut) - var(--h) * var(--arb)), calc(var(--h) * .78 * var(--ar))); } /* the rest of the field, at most 0.78 of the big plate; PAIR_K in v4.js */
  .mod--pair.is-narrow { --h: min(calc((100cqi - var(--gut)) / var(--sum)), 96svh); } /* two phone-shaped stills stay within the screen */
  .mod--pair.is-narrow > .pl--small { width: calc(var(--h) * var(--ar)); }
}

/* ---------- the Cut (§7, device 2) ---------- */
.page-head { padding-top: clamp(24px, 4vw, 64px); }
.page-head .rail { align-self: end; padding-bottom: .6em; }
.cut--page { margin-left: -.06em; white-space: normal; text-wrap: balance; overflow-wrap: break-word; font-size: min(var(--t-cut), 14cqi); line-height: .86; }
.page-head .field { container-type: inline-size; } /* titles wrap and scale to the field: never cropped (Chris, 2026-09-24) */
.cut--bleed { overflow: visible; }
.grp > .field { container-type: inline-size; }
.cut--band { margin: var(--off-m) 0 0; color: var(--ink); font-size: min(var(--t-cut), 15.5cqi); } /* the whole word, sized to the field: never cropped (Chris, 2026-09-24) */
.cut--footer { height: .64em; overflow: hidden; padding-inline: var(--m); margin-top: var(--off-m); font-size: clamp(56px, 13.2vw, 280px); line-height: .82; }
.next-title { display: block; margin: 10px 0 20px; }
.lead { margin-top: 24px; }
.t-section.lead { margin-top: 32px; }

/* ---------- home: the cover (§13) ---------- */
/* Same top padding as .page-head, so the landing's title sits the same distance below the
   masthead as every other page's (Chris, 2026-10-07). Measured before: the ink of the Work
   title started 63px below the masthead at 1440 and the landing's 47px, because this was
   clamp(16px, 3vw, 48px) against the page head's clamp(24px, 4vw, 64px). */
.cover { padding-top: clamp(24px, 4vw, 64px); row-gap: 40px; align-items: end; }
.cover > .rail { align-self: start; }
.cover > .rail .tb { margin-top: 0; }
/* The rule above FEATURED lands on the headline's CAP-LINE, not on its box (Chris,
   2026-10-07). Both start at the same y, but a headline's line box carries half-leading above
   the capitals - measured at 1440 the box began at 117 and the letters at 120 - so the rule
   read 3px high against the word beside it. 0.3em of the 11px label is 3.3px, which tracks
   that half-leading across the range (it is 0.0235em of the headline, ~2-4px from 1280 to
   1920). The label's own text follows the rule down, as it should: the rule is the line the
   eye reads against. */
.cover > .rail .sec-label { margin-top: .3em; margin-bottom: 32px; }
/* 5 columns, not 6, and pinned to row 1 beside the plate. The headline is sized in container
   units (13.2cqi), so the narrower track also makes it a little smaller, which is the right
   direction - it was the biggest thing on the screen. */
/* padding-top lands the landing headline's CAPITALS where the Work title's capitals land,
   not where its box lands (Chris, 2026-10-07). Both h1 boxes already start at the same y, but
   a line box carries half-leading above the caps in proportion to the font size, and the Work
   title is 222px against the landing's 120px at 2000 - so the landing's letters began 4px
   higher, and the gap under the header read tighter. The letters are what the eye measures.
   Measured need: +2px at 1440, +3px at 1920, +4px at 2000.

   It has to be `top`, not padding: this block is align-self: end, so padding-top grows the box
   upward and the text does not move - measured, it changed nothing. */
/* SIX columns, not five (Chris, 2026-10-07). .cover-h1 is 15.4cqi of THIS box, so the box is
   what sets the headline's size - and at span 5 the box got NARROWER above 1100px than the
   span 6 it already had below, which made the headline shrink on the bigger screen. Measured
   at span 5: 75px at 1280, 85px at 1440, 100px at 1680, 115px at 1920, with 184-275px of the
   page left empty between the headline and the photograph. Span 6 is +21% at every one of
   those widths and still clears the cover plate's third cell by 83-121px. Span 7 overlaps the
   photograph by 21px at 1440, so six is the ceiling without moving the plate.
   Only the landing page has .cover-text (checked against all 26 sitemap URLs). */
.cover-text { grid-column: 3 / span 6; grid-row: 1; align-self: end; padding-bottom: 30px; container-type: inline-size; position: relative; top: clamp(2px, 0.2vw, 4px); } /* anchored to the foot of the cover plate */
/* The home headline speaks at the size of the page titles (Chris). Sized so its longest
   word fits its column (13.2cqi); line-height .9 keeps descenders clear of the next line. */
.cover-h1 { font: 600 min(clamp(46px, 8.4vw, 150px), 13.2cqi)/.9 var(--f-sans); letter-spacing: var(--tr-cut); margin-left: -.04em; }
.cover-h1 span { display: block; text-wrap: balance; }
.cover-h1 span + br { display: none; } /* the spans already stack; the <br> keeps the lines when the text is edited */
.cover-sub { margin-top: clamp(24px, 2vw, 32px); color: var(--muted); } /* 24 floor: the phone scale is 24/32/40, this used to drop to 18 */
/* The hero sits on the SAME column structure and the SAME shape as a grid tile (Chris,
   2026-10-06). It used to be four columns at the photograph's own 2:3, beside tiles at 4:5 and
   a portrait at 3:4 - three shapes on one page, which is why the hero read as the odd one out
   and as smaller than the work below it even though it was 73px WIDER than a tile at 1440.
   Five columns at 4:5 is 546x682 at 1440: bigger than the old 436x654, same shape as the grid,
   and still inside the 78svh cap that keeps the arrival screen working. */
/* The hero photograph is EXACTLY the third column of the work grid below it - same width,
   same edges - and never wider (Chris, 2026-10-06). It keeps its OWN proportions: forcing it
   to the tiles' 4:5 cropped a 2:3 photograph and Chris said it looked shrunk, so the width is
   matched and the height is whatever that photograph's shape gives.
   A tile is not a whole number of the 12-column grid: at 1440 it is 363px, between 3 columns
   (318) and 4 (432), because the grid row spans columns 3-12 and divides THAT into three. So
   the plate cannot be placed with a column span; it has to repeat the grid's own arithmetic.
   .cover-plate therefore spans the whole field and is itself a three-column grid with the same
   gutter, and the photograph sits in its third cell. Its first two cells are empty, which is
   why it can share row 1 with the headline without pushing it: the headline's track ends at
   822px and the third cell begins at 1032px. */
.cover-plate { grid-column: 3 / -1; grid-row: 1; --gut: clamp(10px, 1.1vw, 16px);
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--gut); align-items: end; }
.cover-plate > .pl { grid-column: 3; }
.cover-plate .pl, .p-cover-plate .pl { --cap-h: 78svh; width: 100%; }
/* The hero fills its cell, always. .pl's own `max-width: calc(--cap-h * --ar)` turns a HEIGHT
   cap into a WIDTH cap, so on a wide but short screen the photograph came out narrower than
   the grid column it is meant to match and left a gap beside it (measured at 2000x1100: 513
   against the tile's 517, and far worse as the screen gets shorter). The width now comes from
   the cell and the cap is applied to the height, where a too-tall photograph is cropped by
   object-fit instead of pulling the whole plate in. */
.cover-plate > .pl { max-width: none; }
.cover-plate > .pl .pl-media { width: 100%; max-height: var(--cap-h); overflow: hidden; }
/* `width: 100%` is load-bearing. With `aspect-ratio` and an auto width, capping the height
   makes the browser hold the ratio by shrinking the WIDTH instead - 770px tall x 0.667 came
   out 513 against the tile's 517. A definite width cannot be derived from the capped height,
   so the box keeps the cell's width and the photograph crops. */
.cover-lines { grid-column: 3 / -1; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--g); border-top: 1px solid var(--ink); padding-top: 10px; }
.cover-lines a { display: block; padding-bottom: 6px; }
.cl-n { color: var(--muted); }
.cl-t { color: var(--ink); }

/* ---------- home: the rotating hero (HERO_ROTATE in v4.js, 2026-10-09) ----------
   An EXTENSION of .cover, so the reasoning above stays intact: with the flag off none of
   this matches anything. The cover becomes a fixed size-box beside a narrow column of
   type. THREE columns of type, NINE of photograph (Chris, 2026-10-09: "I sell the image,
   so it needs to be the hero"). The headline is sized from its own container, so the one
   change both shrinks the copy and widens the photograph. */
.cover--wide { padding-top: clamp(24px, 4vw, 64px); row-gap: 40px; align-items: start; --fade: 1.2s; }
.cover--wide .cover-text {
  grid-column: 1 / span 3; grid-row: 1;
  align-self: start; padding-bottom: 0; top: auto;
  container-type: inline-size;
}
/* The same cqi figure the live headline uses, against a narrower container: the photograph
   has taken the width the headline used to spend. (A larger headline was tried on
   2026-10-08 and Chris asked for this size back.) Line-height 1, not .9: at .9 the
   homepage headline collided at every width (memory inter-line-height-2026-10-06). */
.cover--wide .cover-h1 {
  font: 600 min(clamp(34px, 4.4vw, 96px), 15.4cqi)/1 var(--f-sans);
  letter-spacing: var(--tr-cut); margin-left: -.04em;
}
.cover--wide .sec-label { margin-top: 0; margin-bottom: 28px; }
.cover--wide .cover-sub { margin-top: clamp(20px, 1.6vw, 28px); color: var(--muted); }
/* --gut is NOT inherited here: it is declared on .mods and .grp-plates and this grid is
   inside neither, so the triptych's gutter has to be declared again. It is the same figure,
   so the three verticals sit on the same gutter as every photo row on the site. */
.hero-media { grid-column: 4 / -1; grid-row: 1; --gut: clamp(10px, 1.1vw, 16px); --cap-h: 78svh; }

/* THE SIZE-BOX. One box, one shape, for every slide: the landscape frame fills it, and a
   project with no landscape puts three verticals inside the SAME box. The box never changes
   size between slides, so nothing below it moves as the hero rotates. */
.hero-box {
  position: relative; width: 100%;
  aspect-ratio: 1920 / 1280;   /* HERO_BOX_AR in v4.js: keep the two in step */
  max-height: var(--cap-h);
  overflow: hidden;
  /* No placeholder tone. On the triptych slide the photographs do not fill the box edge to
     edge while they load, and a grey band under them read as a broken image rather than as
     white space. .pl-media keeps its own tone, which is under the photograph it belongs to. */
  background: transparent;
}
/* A slide, and the fade. */
.hero-box .slide {
  position: absolute; inset: 0;
  opacity: 0; visibility: hidden;
  /* The visibility delay must track the fade, or a slide is taken out of the accessibility
     tree while it is still visible on screen. */
  transition: opacity var(--fade) ease, visibility 0s linear var(--fade);
}
.hero-box .slide.is-on { opacity: 1; visibility: visible; transition: opacity var(--fade) ease, visibility 0s; }
/* The plates fill the box: .pl's own max-width turns a HEIGHT cap into a WIDTH cap, which
   would pull a slide in from the box's edge (the same trap .cover-plate documents above). */
.hero-box .pl { max-width: none; width: 100%; height: 100%; }
/* `width: 100%` on .pl-media is load-bearing, the same way it is on .cover-plate above: the
   plate carries an aspect-ratio inline, so with a definite HEIGHT and an auto width the
   browser holds the ratio by sizing the width from it. Measured at 1680x1000: each triptych
   cell came out 520px wide (780 x 2/3) in a 1182px box, so the three overflowed it by 378px.
   With both axes definite the declared ratio no longer applies and the photograph crops. */
.hero-box .pl-link, .hero-box .pl-media { width: 100%; height: 100%; }
/* Case A: one horizontal photograph, filling the box.
   Case B: no landscape frame, so three verticals in the SAME box. The three fill the box
   edge to edge, so the hero is the same block of photograph whichever project is showing.
   Keeping their true proportions left a 191px band of empty page under them (699 of box,
   508 of picture), which read as the photographs having shrunk rather than as white space.
   Filling costs about 27% of each frame's width, so the frames are chosen to take a tall
   narrow crop. --cells follows the selection, so choosing two photographs gives a row of
   two rather than three cells with a hole in the last one. */
.slide--trio { display: grid; grid-template-columns: repeat(var(--cells, 3), minmax(0, 1fr)); column-gap: var(--gut); align-items: stretch; }
/* The cells are stretched to the box and the column gives them their width, so both axes
   are definite and the 2/3 the markup declares (HERO_TRIO_AR) no longer applies: the three
   share ONE height and the row has a straight bottom edge, which is the standing rule in
   v4.js ("every photo in a group at one shared height", "no big and small pairs"). Left to
   their true ratios three frames came out 507/545/508 and the row ended ragged. */

/* The facts must follow the photograph: the hero names a project, so they cannot stay still
   while the photograph changes. They change on the same clock, stacked in one box so the
   column does not jump when one project carries fewer rows than another.
   They do NOT crossfade. Two photographs dissolving read as one dissolve; two FACT TABLES
   dissolving read as a smear of overlapping words - "RESIDENTIAL...RET...LOFT" was caught on
   screen. So the outgoing set fades out and the incoming one waits for it, and the two are
   never both legible at once.
   The two halves are FRACTIONS OF --fade, the photograph's own dissolve, declared once on
   .cover--wide: out over the first 42%, in over the remaining 58%. The words and the picture
   therefore begin and end on the same clock, and one number moves both. The figures used to
   be typed by hand (out in .35s, in over .4s after a .45s delay), which landed the new facts
   at .85s while the photograph ran to 1.2s, and the text read as lagging behind the picture
   (Chris, 2026-10-09). */
.hero-factbox { position: relative; margin-top: clamp(28px, 2.6vw, 44px); min-height: 190px; }
.hero-factbox .fslide {
  position: absolute; inset: 0 0 auto 0;
  opacity: 0; visibility: hidden;
  transition: opacity calc(var(--fade) * .42) ease, visibility 0s linear calc(var(--fade) * .42);
}
.hero-factbox .fslide.is-on { opacity: 1; visibility: visible; transition: opacity calc(var(--fade) * .58) ease calc(var(--fade) * .42), visibility 0s; }
.hero-factbox .tb { margin-top: 0; }
.hero-more { margin-top: 18px; }
.hero-count { margin-top: 10px; color: var(--muted); text-align: right; }

/* A person who asked for less motion gets none: v4.js does not start the timer, and the
   fade itself is removed so nothing animates at all.
   `.is-on` MUST be named here. `.hero-box .slide` is 0-2-0 and loses to the
   `.hero-box .slide.is-on` rule above at 0-3-0, so the ACTIVE slide - the only one anybody
   sees fade - kept its transition and reduced motion did nothing. Found in the sketch. */
@media (prefers-reduced-motion: reduce) {
  .hero-box .slide, .hero-box .slide.is-on,
  .hero-factbox .fslide, .hero-factbox .fslide.is-on { transition: none; }
}

/* ---- phone: one photograph at a time ---------------------------------------
   Three uprights side by side on a 390px screen come out ~110px wide, which is a thumbnail,
   not a photograph. The row unrolls into the rotation instead: each chosen photograph takes
   the whole field in turn, blended by v4.js so a project's photographs never play back to
   back. The box is portrait here, which is the shape of both the screen and most of the work.
   data-mode is set by v4.js and is only ever "solo" under this query. */
@media (max-width: 760px) {
  .hero-box[data-mode="solo"] { aspect-ratio: 3 / 4; }
  /* A landscape frame is shown WHOLE rather than cropped to a third of itself. There are two
     of them in the whole selection, so this is the minority case, and letterboxing it against
     the page is better than losing most of the picture. */
  .hero-box[data-mode="solo"] .slide--solo.is-contain img { object-fit: contain; }
}

.profile { display: grid; grid-template-columns: repeat(10, minmax(0, 1fr)); grid-template-rows: auto auto 1fr; column-gap: var(--g); row-gap: 28px; align-items: start; } /* the last row flexes, so the portrait spanning all three cannot stretch the text apart */
/* The text is placed on explicit rows so it can SHARE rows 1-3 with the portrait's frame.
   Without this the frame, which spans every column, took all three rows to itself and auto
   placement pushed the text below the photograph. The frame only paints in its third cell. */
.profile-lead { grid-row: 1; grid-column: 1 / span 6; max-width: none; font-size: clamp(24px, 2.4vw, 36px); line-height: 1.15; letter-spacing: -.02em; }
.profile-body { grid-row: 2; grid-column: 1 / span 6; }
/* 6 of 10, matching the lead and the body above it. At 7 the row ran under the portrait: the
   portrait's cell now begins at 67.7% of the block and 7 columns reach 70%, so the third
   title block's rule crossed the photograph - measured 21px of overlap at 1440 and 50px at
   2560. The old padding-right was holding it a gutter short of a portrait that used to start
   further right; at 6 columns there is real clearance and it is no longer needed. */
.profile-cols { grid-row: 3; grid-column: 1 / span 6; display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--g); margin-top: 8px; }
/* The portrait is the same third-column width as the hero and the work grid, by the same
   means: the figure spans the whole block as a three-column grid and its contents sit in the
   third cell. The text beside it ends at 59% of the block and that cell begins at 68%, so they
   do not meet. (Was 3 of 10 columns at the portrait's own 3:4.) */
.profile > .pl { grid-column: 1 / -1; grid-row: 1 / span 3; --gut: clamp(10px, 1.1vw, 16px);
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); column-gap: var(--gut);
  max-width: none; } /* the figure is now the measuring frame, so .pl's own cap must not shrink it */
.profile > .pl > * { grid-column: 3; }

/* ---------- the arrival screen (Chris, 2026-10-06) ----------
   Every page opens the same way: the opening block, then the FIRST CONTENT BELOW IT reaching
   into the same screen. Never a blank band, and never a closed screen that gives no reason to
   scroll. The site's rule has always been "peek, not a scroll icon"; nothing enforced where
   the fold landed, so each page did something different. Measured on arrival at 1440x900:
   home ended on 120px of empty padding with the Work label 24px BELOW the fold; /work showed a
   31px sliver of a tile (6%); /profile cut a photograph at 93%, just before it finished;
   /architecture and /contact crossed nothing at all, so the screen looked complete.

   --arrive is the gap between an opening block and the content under it. It is deliberately
   much smaller than --sec, because --sec is the rhythm BETWEEN sections in the body of a page,
   where both neighbours are already visible. Here the gap is competing with the fold.

   Why not simply shrink the hero until a photograph peeks: it would cost about 130px of the
   landing's cover plate, and that photograph is already smaller than the headline beside it.
   The first screen should end on the section rule, the label and the top of the work, which
   says what is below without spending the hero to say it. */
:root { --arrive: clamp(28px, 4vh, 52px); }
/* The Work section sits the same distance below the hero as every other section sits below
   the one above it (Chris, 2026-10-07). It used to take the tighter --arrive so that the work
   reached into the first screen; the hero is now one grid column wide instead of four, so it
   ends high enough that --sec still leaves the label and the top of the photographs above the
   fold. Consistency wins where it costs nothing. */
.sec--work { padding-top: var(--sec); }
/* With the rotating hero on there is no grid under this line, so the section is a rail label
   and a link in a band of its own. At var(--sec) - 144px at 1680 - that band opened with 144px
   above a 24px row, and the Profile section below opens with another 144px of its own: 312px of
   page carrying one thin line, which read as an empty room rather than as a divider. Measured
   at 1680 the line sat 169px below the hero and 144px above Profile.
   48px is the top of --off-m's range, the rhythm step this site already uses between a block
   and the thing it belongs to, and it puts the line 73px under the hero: close enough to read
   as the hero's own footer line, far enough not to crowd the count above it.
   A MODIFIER, not .sec--work itself: with the flag off the markup is byte-identical to today's
   and the rendering has to be too, or the rollback is not a rollback. */
.sec--work--bare { padding-top: clamp(32px, 3vw, 48px); }
.grp { padding-top: clamp(72px, 8vw, 128px); overflow-x: clip; } /* the Cut band bleeds to the screen edge, never past it */
.grp > .rail { position: sticky; top: 24px; align-self: start; }
.grp-title { margin-bottom: var(--off-s); font-size: clamp(28px, 3vw, 48px); }
.grp-title a:hover { text-decoration: none; opacity: .7; }

/* ---------- rooms (§13.4, route C) ---------- */
.room { background: var(--room); color: var(--room-ink); margin-top: var(--sec); --ink: var(--room-ink); --rule: var(--room-rule); --muted: var(--room-muted); }
.room-in { padding-block: var(--sec); align-items: center; }
.room-text { grid-column: 3 / span 4; }
.room-text .cap { margin-top: 12px; }
.room-plate { grid-column: 8 / span 3; }
.room-plate .pl { --cap-h: 80svh; }
.room .mods { padding-block: var(--off-m); }
.room-label { padding-top: var(--off-m); }
.project .room { margin-top: 0; }

/* ---------- approach / credits ---------- */
.steps { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--g); }
.steps .cap { margin-top: 10px; color: var(--ink); }
.credits { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--g); }

/* ---------- index (§14) ---------- */
/* The index is the first content under a page head, so it takes --arrive like the landing's
   Work section rather than the larger --off-m that used to leave the first row of tiles
   showing a 31px sliver at 1440x900. */
.index { padding-block: var(--arrive) var(--sec); align-items: start; }
.filters li a { display: block; padding: 4px 0; color: var(--muted); }
.filters [aria-current="page"] { color: var(--ink); text-decoration: underline; text-underline-offset: .35em; }
.index-table { grid-column: 3 / span 6; }
.index-preview { grid-column: 9 / -1; position: sticky; top: 24px; display: grid; }
.pv { grid-area: 1 / 1; opacity: 0; transition: opacity var(--t-quick) var(--ease); pointer-events: none; }
.pv.is-on { opacity: 1; }
.index-preview:not(.is-warm) .pv:not(.is-on) { display: none; } /* the rest download after load (perf audit RC4) */

/* The landing page's Work preview: the selected projects as their covers, three across.
   Replaced five full project groups that cost 4.32 screens between them for one title
   each. Same row unit as everywhere else: plates at one shared height. */
/* The gutter. --gut is NOT inherited here: it is declared on .mods and .grp-plates, and the
   cover grids are inside neither, so `gap: var(--gut)` and `margin-top: var(--gut)` below both
   computed to nothing and the tiles touched on every side at desktop while the phone, which
   set its own gap, had 10px (measured 2026-10-06 at 1440: horizontal gaps 0 and 0, row gap 0).
   Same value as .grp-plates, so the grids gutter like the rest of the site. */
.hw-grid, .index-grid { --gut: clamp(10px, 1.1vw, 16px); }
.hw-grid > .row { grid-column: 3 / -1; }
.hw-grid > .row + .row { margin-top: var(--gut); }
.hw-grid .pl .cap { color: var(--muted); }
/* Every tile reserves the same caption height, so tiles in a row are the same height.
   This was removed earlier on 2026-10-06 on the reasoning that a flex row takes its height
   from its tallest tile anyway, which is true on a laptop and WRONG below 761px, where the
   row becomes `display: contents` and the tiles are grid items: a shorter caption then leaves
   its tile 17px short of its neighbours (measured 268, 268, 251 at 375 and 296, 280, 280 at
   768). The cost is dead space under a tile whose title does not wrap. Equal tiles are worth
   more than those few pixels. */
.hw-grid .pl .cap, .index-grid .pl .cap { min-height: 2.9em; align-items: flex-start; }
/* Three equal columns, sized by the row and never by what is inside them.
   This has to be said explicitly because the plate sizing a row normally uses does not work
   here: `.row > .pl` is `flex: none; width: calc(var(--h) * var(--ar))`, and --h is only set
   inside a container-query context (.p-seq .mods, .room .mods, .grp-plates). .hw-grid is not
   one, so --h never resolves, that width declaration is invalid, and every tile fell back to
   `width: auto` -- its own content's intrinsic width. A <video> carries real intrinsic
   dimensions where an <img> in an aspect-ratio box does not, so the two film tiles came out
   634px against the stills' 389px at 1440. min-width: 0 keeps a flex item from refusing to
   shrink below that same intrinsic width. */
.hw-grid > .row > .pl, .index-grid > .row > .pl { flex: 1 1 0; width: auto; min-width: 0; }

/* Work: the grid view (2026-10-06). The DEFAULT view, with the list behind the switch.
   One tile per project, the same tiles as the landing page's preview and from the same
   function, so the two views of this work cannot drift apart. It ends: no endless scroll. */
.index-grid { grid-column: 3 / -1; }
.index-grid > .row + .row { margin-top: var(--gut); }
/* Offscreen rows cost nothing to lay out. The intrinsic size is a guess at a row's height so
   the scrollbar does not jump while they are still unrendered. */
.index-grid > .row { content-visibility: auto; contain-intrinsic-size: auto 38svh; }
.index-grid .pl .cap { color: var(--muted); }

/* The switch is two radios, so the view changes with CSS alone: it works with JavaScript
   off and there is no flash of the wrong view. v4.js only remembers the choice. */
.wv { display: flex; gap: 18px; margin-top: 18px; }
.wv label { display: block; padding: 4px 0; color: var(--muted); cursor: pointer; }
#wv-grid:checked ~ .rail .wv label[for="wv-grid"],
#wv-list:checked ~ .rail .wv label[for="wv-list"] { color: var(--ink); text-decoration: underline; text-underline-offset: .35em; }
#wv-grid:focus-visible ~ .rail .wv label[for="wv-grid"],
#wv-list:focus-visible ~ .rail .wv label[for="wv-list"] { outline: 2px solid var(--ink); outline-offset: 3px; }
#wv-grid:checked ~ .index-table, #wv-grid:checked ~ .index-preview { display: none; }
#wv-list:checked ~ .index-grid { display: none; }
.pv .pl { --cap-h: 62svh; }
.pv .tb { margin-top: 16px; }
.idx { width: 100%; border-collapse: collapse; }
.idx thead th { text-align: left; font-weight: 500; color: var(--muted); padding: 0 12px 8px 0; border-bottom: 1px solid var(--ink); }
.idx td, .idx tbody th { text-align: left; vertical-align: baseline; padding: 11px 12px 11px 0; border-bottom: 1px solid var(--rule); transition: border-color var(--t-quick) var(--ease); }
.idx tbody th { font: 600 clamp(17px, 1.5vw, 22px)/1.15 var(--f-sans); letter-spacing: -.02em; text-transform: none; }
.idx tbody th a { display: block; }
.idx tbody tr:hover td, .idx tbody tr:hover th, .idx tbody tr:focus-within td, .idx tbody tr:focus-within th, .idx tbody tr.is-on td, .idx tbody tr.is-on th { border-bottom-color: var(--ink); }
.idx .i-no { color: var(--muted); width: 7ch; white-space: nowrap; }
.idx .i-n { text-align: right; padding-right: 0; white-space: nowrap; }
.idx .idx-sep th { font: 500 var(--t-meta)/1.4 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; padding-top: 36px; color: var(--ink); border-bottom-color: var(--ink); }
.idx tbody:first-of-type .idx-sep th { padding-top: 14px; }
.idx thead + tbody .idx-sep th { padding-top: 14px; }
.idx .idx-prod th[scope="row"] { font-size: clamp(15px, 1.15vw, 17px); font-weight: 400; } /* §14: productions sit a size down (the old selector lost to .idx tbody th) */
.i-thumb { display: none; }
.counter { margin-top: 16px; }
.idx--guides tbody th { font-size: clamp(16px, 1.3vw, 19px); }

/* ---------- project (§15) ---------- */
.project .page-head { overflow: visible; }
.project .cut--page { font-size: min(clamp(56px, 9vw, 168px), 13cqi); }
.p-cover { padding-top: var(--off-m); align-items: start; row-gap: 32px; }
.p-cover > .rail { position: sticky; top: 24px; }
.p-cover-plate { grid-column: 3 / span 6; }
.p-context { grid-column: 9 / -1; align-self: end; display: grid; gap: 1em; font-size: 16px; }
.colophon { padding-block: var(--sec) var(--off-m); border-top: 1px solid var(--ink); margin-top: var(--off-m); align-items: start; }
.colophon-tb { grid-column: 3 / span 4; }
.next { grid-column: 8 / -1; display: block; overflow: hidden; }
.next:hover { text-decoration: none; }
.next .pl { width: 34%; }
.next:hover .next-title { opacity: .7; }

/* hospitality tempo: larger plates, rooms for film */
/* commercial tempo: denser */

/* ---------- profile / contact / resources ---------- */
/* Profile and Contact open the same way as the rest: --arrive, not --off-m, so their first
   block reaches into the arrival screen instead of starting below it. */
.dossier, .contact { padding-block: var(--arrive) 0; align-items: start; row-gap: 40px; }
.dossier > .rail, .contact > .rail { position: sticky; top: 24px; }
.dossier-plate { grid-column: 3 / span 4; }
.dossier-text { grid-column: 8 / -1; display: grid; gap: 1em; }
.dossier-text .t-section { max-width: 28ch; font-size: clamp(22px, 2vw, 30px); margin-bottom: .4em; }
.contact-main { grid-column: 3 / span 6; display: grid; gap: 24px; }
.contact-main .t-section { font-size: clamp(26px, 2.6vw, 40px); }
.btn { display: inline-flex; align-items: center; min-height: 44px; padding: 0 18px; border: 1px solid var(--ink); font: 500 var(--t-meta)/1 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; }
.btn:hover { background: var(--ink); color: var(--page); text-decoration: none; }
.c-form { display: grid; gap: 18px; margin-top: 24px; max-width: 560px; border-top: 1px solid var(--ink); padding-top: 12px; }
.c-form label { display: grid; gap: 6px; }
.c-form input, .c-form textarea { font: 400 16px/1.4 var(--f-sans); color: var(--ink); background: transparent; border: 0; border-bottom: 1px solid var(--rule); padding: 8px 0; border-radius: 0; }
.c-form input:focus, .c-form textarea:focus { outline: none; border-bottom-color: var(--ink); }
.c-form .btn { justify-self: start; }

/* ---------- footer: the final sheet (§20) ---------- */
.sheet { margin-top: var(--sec); border-top: 1px solid var(--ink); padding-top: 24px; overflow: hidden; overflow: clip; } /* clip, not a scroll box, so the wordmark can follow the page scroll */
.p-project .sheet { margin-top: 0; } /* the colophon already closes the page with its own space */
.sheet-row + .sheet-row { margin-top: 40px; }
.sheet-cta a { text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: .2em; }
.sheet-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--g); }
.sheet-grid .tb > div { grid-template-columns: 1fr; gap: 2px; }
/* Footer index: the Menu & footer tool writes bare links, so the separators are CSS. */
.ft-links { display: flex; flex-wrap: wrap; gap: 2px .5em; }
.ft-links a:not(:last-child)::after { content: "·"; display: inline-block; margin-left: .5em; color: var(--muted); } /* a line ends on a dot, never starts with one */
/* Narrow rails (profile, contact): label above value so long values like emails never break mid-word. */
.tb[data-stack] { grid-template-columns: 1fr; }
.tb[data-stack] > div { gap: 2px; }
.tb a[href^="mailto:"] { text-transform: none; letter-spacing: .02em; } /* an address reads in its own case, and fits the rail */

/* ---------- lightbox ---------- */
.lb { border: 0; padding: 0; margin: 0; width: 100%; max-width: none; height: 100%; max-height: none; background: var(--page); color: var(--ink); }
.lb::backdrop { background: var(--page); }
.lb[open] { display: grid; grid-template-columns: 64px 1fr 64px; align-items: center; }
.lb figure { grid-column: 2; display: grid; justify-items: center; gap: 10px; }
.lb img { width: auto; max-width: 100%; max-height: calc(100svh - 96px); }
.lb-x { position: absolute; top: 18px; right: var(--m); min-height: 44px; }
.lb-p, .lb-n { font-size: 16px; height: 44px; }

/* ---------- motion (§11): rules draw from the rail ----------
   Photos are simply there: no reveal on entry (Chris, 2026-09-27: the sliding reveal looked cheap). */
@media (prefers-reduced-motion: no-preference) {
  html.js .tb > div::after { transform: scaleX(0); transition: transform var(--t-block) var(--ease) 120ms; }
  html.js .tb.is-in > div::after { transform: scaleX(1); }
  html.js .cut--band { transform: translateX(12px); opacity: .001; transition: transform 600ms var(--ease), opacity 600ms var(--ease); }
  html.js .cut--band.is-in { transform: none; opacity: 1; } /* page titles are content: they are there at once (GSD 05) */
}

/* ---------- tablet: 8 columns (§6) ---------- */
@media (max-width: 1100px) {
  .mh-pos { grid-column: 3 / span 3; }
  .cover-text { grid-column: 3 / span 6; } /* same as the base now; kept so the span survives a change to it */
  .cover-h1 span { display: block; }
  .index-table { grid-column: 3 / -1; }
  .index-preview { display: none; }
  .p-cover-plate { grid-column: 3 / span 7; }
  .p-context { grid-column: 3 / span 8; }
  .credits, .sheet-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 24px; }
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); row-gap: 24px; }
}

/* ---------- mobile: 4 columns (§21) ---------- */
@media (max-width: 760px) {
  :root { --m: 16px; --g: 12px; --t-body: 16px; --t-cut: 28vw; --sec: 88px; --off-l: 72px; --off-m: 40px; --off-s: 20px; }
  .g { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .rail, .field, .g > * { grid-column: 1 / -1; }
  .masthead { grid-template-columns: 1fr auto; }
  .mh-name { grid-column: 1; display: inline-flex; align-items: center; min-height: 44px; margin: -14px 0; justify-self: start; }
  .mh-pos, .nav, .mh-theme { display: none; }
  .mh-menu { display: block; grid-column: 2; min-height: 44px; min-width: 44px; text-align: right; margin: -14px 0; } /* 44px tap target both ways */
  .menu-sheet > .g { grid-template-columns: 1fr auto; }
  .menu-foot { grid-template-columns: 1fr auto; }
  .menu-foot > :last-child { grid-column: 2; }

  .rail { position: static !important; }
  .grp > .rail, .p-cover > .rail, .dossier > .rail { order: 2; } /* contact keeps its details before the form */
  .grp > .rail { margin-top: 24px; }
  .page-head .rail { order: 0; padding-bottom: 0; }
  .cut--page { margin-left: -2vw; }
  .cut--band { margin: var(--off-s) 0 0; }
  .cut--footer { font-size: 13.2vw; }

  .cover { row-gap: 24px; }
  .cover-text { order: 1; }
  /* No three-column frame on a phone: there is no three-up grid here to match, so the hero
     and the portrait keep the widths they always had. */
  /* The headline and the photograph STACK here, they do not share a row. Both carry an
     explicit grid-row: 1 for the desktop layout, and an explicitly placed grid item ignores
     `order` - so on a phone they landed in the same cell and the headline, which is
     align-self: end, was painted over the bottom of the photograph and became unreadable.
     Returning them to auto rows lets `order` stack them again. */
  /* The title lands where the Work page's title lands. Both blocks take the same 24px here,
     but .page-head keeps an EMPTY rail that still occupies a grid row and its 12px row gap,
     while .cover hides its rail outright - so the landing's title started 10px higher
     (measured: Work's capitals 36px below the masthead at 375, the landing's 26px). */
  .cover { padding-top: 34px; }
  .cover-text, .cover-plate { grid-row: auto; }
  .cover-plate { order: 2; padding: 12px; display: block; }
  .profile > .pl { display: block; }
  .cover-plate > .pl, .profile > .pl > * { grid-column: auto; }
  .p-cover-plate { padding: 12px; }
  .cover > .rail { order: 3; }
  .cover-lines { order: 4; grid-template-columns: 1fr; }
  .cover-plate .pl, .p-cover-plate .pl { --cap-h: 200svh; }
  /* The rotating hero on a phone: the PHOTOGRAPH comes before the words. v4.css orders the
     phone hero text(1) then plate(2), and with a headline, a sub and a four-row fact table
     above it the picture started 617px down an 844px screen - an arrival screen of all type
     and no photograph, which is backwards for a page whose product is the image.
     The text column stops being a box (display: contents) so its parts can be placed AROUND
     the picture: label, headline, PHOTOGRAPH, sub, facts. That also settles the old trap
     noted above for .cover-text - an explicitly placed grid item ignores `order` - because
     every one of these is now placed and ordered on its own. */
  .cover--wide { row-gap: 0; }
  .cover--wide .cover-text { display: contents; }
  .cover--wide .sec-label { grid-column: 1 / -1; order: 1; margin: 0 0 2px; }
  .cover--wide .cover-h1 { grid-column: 1 / -1; order: 2; }
  /* `.t-title.cover-h1` 0-2-0 at 16cqi, further down this file, is what actually sizes the
     phone headline - and it is BELOW this block, so a rule of equal weight here loses to it.
     Hence the third class. Measured: without it the headline came out 62.4px (16cqi with no
     query container left, which falls back to 16svw) over four lines, and the photograph
     started 391px down an 844px screen. At this size the headline is 28px and the picture
     starts at 241. Four lines of display type ate the arrival screen. */
  .cover--wide .t-title.cover-h1 { font-size: clamp(26px, 7.2vw, 36px); }
  .hero-media { grid-column: 1 / -1; grid-row: auto; order: 4; margin-top: 20px; }
  .cover--wide .cover-sub { grid-column: 1 / -1; order: 5; margin-top: 14px; }
  .hero-factbox { grid-column: 1 / -1; order: 6; margin-top: 16px; min-height: 150px; }

  .profile { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  /* grid-row: auto on every child, so the block stacks in DOCUMENT order and the portrait
     lands where v4.js writes it (between the prose and the facts). The explicit rows are a
     desktop device: kept here, they pinned the portrait below the fact table. */
  .profile > * { grid-column: 1 / -1 !important; grid-row: auto !important; }
  /* The portrait takes the WHOLE field and is formatted exactly like the hero photograph
     (Chris, 2026-10-07). It used to be the last two of this block's four columns, the width of
     a Work tile - 173px at 390, which read as a thumbnail beside the hero's 334px.
     The 12px inset is the hero's: .cover-plate carries `padding: 12px` inside a 1/-1 field, so
     its photograph runs 28px to 362px at 390. margin-inline here puts this one on exactly the
     same two edges, because .profile > .pl is the plate itself and has no wrapper to pad. */
  .profile > .pl { grid-column: 1 / -1 !important; width: auto; margin-inline: 12px; }
  .profile-cols, .credits, .sheet-grid, .steps { grid-template-columns: 1fr; }
  .profile-cols { padding-right: 0; } /* nothing sits beside it on the phone */

  /* rows of three open with one plate full width, then a pair; pauses become pairs */
  .row { flex-wrap: wrap; align-items: flex-start; }
  .row > .pl { flex: var(--ar) 1 0%; width: auto; }
  .row--3 > .pl:first-child { flex-basis: 100%; }
  .row--4 > .pl { flex: 0 0 calc(50% - var(--gut) / 2); } /* four phone-shaped stills: two by two */
  /* a pause stays a pause: big plate full width, the small one smaller on the far side */
  .mod--pair, .mod--pair.is-flip { display: block; }
  .mod--pair > .pl { width: 100%; }
  .mod--pair > .pl--small { width: 62%; margin-top: var(--gut); margin-left: auto; }
  .mod--pair.is-flip > .pl--small { margin-left: 0; }
  /* two phone-shaped stills sit side by side at one height */
  .mod--pair.is-narrow { display: flex; gap: var(--gut); }
  .mod--pair.is-narrow > .pl, .mod--pair.is-narrow > .pl--small { flex: var(--ar) 1 0%; width: auto; margin: 0; }
  .mod--film.row--1 > .pl { flex: none; width: min(100%, calc(78svh * var(--ar))); }
  .mod--solo > .pl { width: 100%; }
  .datum { padding-left: 10px; }
  .datum::before { left: 0; }

  .room-in { row-gap: 24px; }
  .room-plate .pl { width: 75%; }

  .index { padding-top: var(--arrive); } /* was a hard 24px, which beat --arrive and opened /work and /architecture 8px tighter than the landing */
  .filters { display: flex; flex-wrap: wrap; gap: 4px 18px; margin-bottom: 16px; }
  .filters li a { min-height: 44px; display: flex; align-items: center; }
  /* The landing preview stays a grid on a phone, two across: its job is to show the range
     in little scroll, and one per line would cost six screens again.

     The six tiles flow as ONE two-column grid, not as rows of three that wrap. Wrapping a
     row of three at 50% put two tiles on one line and left the third alone beside a hole,
     twice over (measured live 2026-10-06 at 375). `display: contents` drops the row boxes so
     every tile is a direct child of the grid and the columns run 2-2-2. */
  .hw-grid, .index-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--gut); }
  /* No caption reserve here. Below 761px the rows are `display: contents` and the tiles are
     grid items, which already stretch to the tallest in their row - measured 276, 276, 259,
     259, 259, 259 at 360px, equal within every visual row. A reserve would only add dead
     space under the short captions. Above this breakpoint the row is flex and does need one. */

  .hw-grid > .row, .index-grid > .row { display: contents; }
  .hw-grid > .row + .row, .index-grid > .row + .row { margin-top: 0; }
  .wv { gap: 18px; margin-top: 12px; }
  .wv label { min-height: 44px; min-width: 44px; display: flex; align-items: center; }
  .idx thead, .idx .i-place, .idx .i-who { display: none; }
  .idx tbody th a { display: flex; align-items: center; gap: 12px; min-height: 44px; }
  .i-thumb { display: block; width: clamp(96px, 28vw, 132px); flex: none; } /* covers big enough to read on a phone (Chris) */
  .idx td, .idx tbody th { vertical-align: middle; }
  .i-thumb img { aspect-ratio: 4 / 5; object-fit: cover; }

  .p-context { font-size: 16px; }
  .colophon { row-gap: 32px; }
  .next .pl { width: 50%; }
}

/* ---------- production additions (generator port) ---------- */
/* Contact form: the live form, restyled. site.js drives its states. */
.contact-form .field-group { display: grid; gap: 18px; }
.contact-form .field { display: grid; gap: 6px; grid-column: auto; } /* the form reuses the live markup, whose .field is not the layout column */
.contact-form .field > label { display: block; font: 500 var(--t-meta)/1.4 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; color: var(--muted); }
.contact-form .field .opt { text-transform: none; letter-spacing: .02em; }
.contact-form .field input, .contact-form .field textarea, .contact-form .field-select { font: 400 16px/1.4 var(--f-sans); color: var(--ink); background: transparent; border: 0; border-bottom: 1px solid var(--rule); padding: 8px 0; border-radius: 0; width: 100%; }
.contact-form .field-select { appearance: none; -webkit-appearance: none; cursor: pointer; padding-right: 28px; }
.contact-form .field:has(.field-select) { position: relative; }
.contact-form .field:has(.field-select)::after { content: ""; position: absolute; right: 6px; bottom: 17px; width: 7px; height: 7px; border-right: 1px solid var(--ink); border-bottom: 1px solid var(--ink); transform: rotate(45deg); pointer-events: none; } /* the dropdown cue */
.field-select option { color: #000; }
.contact-form .field input:focus, .contact-form .field textarea:focus, .contact-form .field-select:focus { outline: none; border-bottom-color: var(--ink); }
.contact-form .field.is-invalid input, .contact-form .field.is-invalid textarea, .contact-form .field.is-invalid .field-select { border-bottom-color: var(--err); }
.contact-form .field.is-invalid > label { color: var(--err); }
.field-hint { margin: 4px 0 0; font: 400 var(--t-meta)/1.4 var(--f-mono); letter-spacing: .04em; color: var(--muted); }
.field-hint.over { color: var(--err); }
.hp-field { position: absolute; left: -9999px; top: auto; width: 1px; height: 1px; overflow: hidden; }
.form-status { color: var(--muted); margin: 0; }
.form-status.ok { color: var(--ink); }
.form-status.err { color: var(--err); }
.proj-guide { margin-top: 20px; }
.proj-guide a { text-decoration: underline; text-decoration-color: var(--rule); text-underline-offset: .25em; }

/* Cookie choices (site.js inserts .cc-banner): a quiet ruled sheet, bottom left. */
.cc-banner { position: fixed; left: var(--m); right: var(--m); bottom: 16px; z-index: 60; max-width: 560px; display: flex; gap: 16px; align-items: center; justify-content: space-between; padding: 12px 14px; background: var(--page); color: var(--ink); border: 1px solid var(--ink); opacity: 0; transform: translateY(8px); transition: opacity .3s var(--ease), transform .3s var(--ease); }
.cc-banner.is-in { opacity: 1; transform: none; }
.cc-text { font: 400 var(--t-meta)/1.5 var(--f-mono); letter-spacing: .02em; margin: 0; }
.cc-text a { text-decoration: underline; text-underline-offset: 2px; white-space: nowrap; }
.cc-actions { display: flex; gap: 8px; flex: 0 0 auto; }
.cc-btn { min-height: 44px; padding: 0 14px; border: 1px solid var(--ink); font: 500 var(--t-meta)/1 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; }
.cc-accept { background: var(--ink); color: var(--page); }
.cc-decline { background: transparent; color: var(--ink); }
@media (max-width: 560px) { .cc-banner { flex-direction: column; align-items: stretch; } .cc-actions > * { flex: 1; } }

/* Without JS the mobile menu button cannot open the sheet: show the nav itself. */
@media (max-width: 760px) {
  html:not(.js) .mh-menu { display: none; }
  html:not(.js) .nav { display: flex; grid-column: 1 / -1; justify-content: flex-start; flex-wrap: wrap; padding-top: 8px; }
}
@media print { .masthead, .menu-sheet, .lb, .cut--footer { display: none; } }

/* Film pauses and plays on click (or Enter/Space when focused); no floating button. */
.pl--film video:focus-visible { outline: 1px solid var(--ink); outline-offset: 4px; }
/* When a film isn't playing (autoplay blocked by the phone, or motion off) the poster
   says so; a tap plays it. Delayed so a film that starts at once never flashes the label. */
.pl--film .pl-media::after { content: "Play film"; position: absolute; left: 12px; bottom: 12px; padding: 7px 10px; font: 500 var(--t-meta)/1 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; color: var(--ink); background: var(--page); opacity: 0; transition: opacity var(--t-quick) var(--ease); pointer-events: none; }
.pl--film.is-paused .pl-media::after { opacity: 1; transition-delay: 700ms; }
.room .pl--film .pl-media::after { color: var(--room); background: var(--room-ink); } /* the film room swaps ink, not page: keep the label readable */

/* ---------- resources index (§18) ---------- */
.res-list { border-top: 1px solid var(--ink); }
.res-list a { display: grid; grid-template-columns: 5ch 1fr auto; gap: var(--g); align-items: baseline; padding: 20px 0 22px; border-bottom: 1px solid var(--rule); transition: border-color var(--t-quick) var(--ease); }
.res-list a:hover, .res-list a:focus-visible { text-decoration: none; border-bottom-color: var(--ink); }
.res-list a:hover .rl-t { text-decoration: underline; text-underline-offset: .15em; text-decoration-thickness: 1px; }
.rl-n { color: var(--muted); }
.rl-body { display: grid; gap: 6px; }
.rl-t { font: 600 clamp(20px, 2vw, 30px)/1.1 var(--f-sans); letter-spacing: -.02em; }
.rl-sub { font: 400 15px/1.5 var(--f-sans); color: var(--muted); max-width: 60ch; }
.rl-m { white-space: nowrap; }

/* ---------- guide (§18): contents in the rail, text on a 62ch measure ---------- */
.guide-head .field { max-width: 60rem; }
.guide-kicker { color: var(--muted); margin-bottom: 18px; }
.guide-h1 { font-size: clamp(34px, 4.4vw, 68px); max-width: 18ch; }
.guide-sub { margin-top: 20px; max-width: 40ch; }
.guide-by { margin-top: 20px; color: var(--muted); }
.guide-main { padding-block: var(--off-m) 0; align-items: start; }
.guide-toc { position: sticky; top: 24px; }
.toc { display: grid; gap: 2px; margin-top: 4px; }
.toc a { display: block; padding: 5px 0; color: var(--muted); text-transform: none; letter-spacing: .02em; line-height: 1.35; }
.toc a:hover, .toc a:focus-visible { color: var(--ink); }
.toc-n { color: var(--ink); margin-right: 4px; }
.guide-text { grid-column: 4 / span 6; min-width: 0; }
.guide-section { padding-bottom: var(--off-m); scroll-margin-top: 24px; }
.guide-section + .guide-section { border-top: 1px solid var(--rule); padding-top: 28px; }
.g-h { font: 600 clamp(22px, 2vw, 30px)/1.15 var(--f-sans); letter-spacing: -.02em; margin-bottom: 18px; }
.g-n { display: block; margin-bottom: 10px; color: var(--muted); }
.g-intro { font: 400 19px/1.55 var(--f-sans); max-width: 60ch; }
.g-quote { margin: 28px 0; padding-left: 20px; border-left: 1px solid var(--ink); }
.g-quote p { font: 400 var(--t-section)/1.3 var(--f-sans); letter-spacing: -.012em; max-width: 36ch; }
.g-steps { list-style: none; counter-reset: st; margin-top: 24px; display: grid; gap: 18px; }
.g-steps li { counter-increment: st; display: grid; grid-template-columns: 4ch 1fr; column-gap: 12px; }
.g-steps li::before { content: counter(st, decimal-leading-zero); font: 500 var(--t-meta)/1.9 var(--f-mono); color: var(--muted); grid-row: 1 / span 2; }
.g-step-t { font: 600 17px/1.35 var(--f-sans); margin: 0 0 4px; }
.g-steps p { font: 400 16px/1.6 var(--f-sans); max-width: 60ch; color: var(--ink); }
.g-take { margin-top: 28px; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--rule); padding: 10px 0 14px; display: grid; grid-template-columns: minmax(12ch, auto) 1fr; gap: 12px; }
.g-take p:last-child { font: 400 16px/1.55 var(--f-sans); }
.guide-close .colophon-tb { display: grid; gap: 20px; }
.i-thumb--on { display: inline-block !important; width: 36px; vertical-align: middle; margin-right: 12px; }
.i-thumb--on img { aspect-ratio: 4 / 5; object-fit: cover; }

/* ---------- prose pages: privacy, 404, pages made in the editor ---------- */
/* Their sections are kept verbatim (inline widths included), so the grid placement wins here. */
.p-prose main { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: var(--g); padding: clamp(24px, 4vw, 64px) var(--m) 0; }
.p-prose main > * { grid-column: 3 / span 7; max-width: none !important; margin: 0 0 var(--off-m) !important; min-width: 0; }
.p-prose main h1 { font: 600 var(--t-title)/1.02 var(--f-sans); letter-spacing: var(--tr-title); margin: 0 0 32px !important; }
.p-prose main p, .p-prose main li { font: 400 var(--t-body)/1.6 var(--f-sans); color: var(--ink); max-width: 62ch; }
.p-prose main p + p { margin-top: 1em; }
.p-prose main strong { font-weight: 600; }
.p-prose main a { text-decoration: underline; text-decoration-color: var(--muted); text-underline-offset: .2em; }
.p-prose main a:hover { text-decoration-color: currentColor; }
.p-prose .two-col { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--g); align-items: start; grid-column: 3 / -1; }
.p-prose .portrait img { width: 100%; height: auto; }
/* 404: the code is the page's one Cut */
.p-404 .notfound .code { font: 600 var(--t-cut)/.82 var(--f-sans); letter-spacing: var(--tr-cut); margin-bottom: 28px; }
.p-404 .nf-links { display: flex; flex-wrap: wrap; gap: 8px 24px; margin-top: 28px; font: 500 var(--t-meta)/1.4 var(--f-mono); letter-spacing: var(--tr-meta); text-transform: uppercase; }
.p-404 .nf-links a { min-height: 44px; display: inline-flex; align-items: center; }

@media (max-width: 1100px) {
  .guide-text { grid-column: 3 / -1; }
  .p-prose main > * { grid-column: 3 / -1; }
}
@media (max-width: 760px) {
  .guide-toc { position: static; margin-bottom: 40px; }
  .guide-text { grid-column: 1 / -1; }
  .g-take { grid-template-columns: 1fr; }
  .res-list a { grid-template-columns: 4ch 1fr; }
  .rl-m { grid-column: 2; }
  .p-prose main { grid-template-columns: repeat(4, minmax(0, 1fr)); }
  .p-prose main > *, .p-prose .two-col { grid-column: 1 / -1; }
  .p-prose .two-col { grid-template-columns: 1fr; }
}

/* ---------- tablet, 761-1100px (§6): the rail becomes the top of each section ----------
   At these widths two rail columns are ~130px, too narrow for a title block. So every
   rail spans the page above its section, and a title block becomes a ruled strip of
   cells (label above value), like the title strip along the foot of a drawing. */
@media (min-width: 761px) and (max-width: 1100px) {
  .mh-name { grid-column: 1 / span 3; }
  .mh-pos { grid-column: 4 / span 3; }
  .nav { grid-column: 7 / -1; gap: 18px; }

  .g > .rail { grid-column: 1 / -1; position: static !important; }
  .g > .field { grid-column: 1 / -1; }
  /* Three lines, not two: these rows are still flex, so nothing equalises the tiles for us,
     and at 768 a tile came out 296 against its neighbours' 280 with only two lines reserved. */
  .hw-grid .pl .cap, .index-grid .pl .cap { min-height: 4.35em; }
  /* Everything that measures itself against the work grid has to agree with that decision, or
     the Profile block and the Work grid sit on different columns and nothing lines up. At 768
     the field was 719px wide while the grid row was 597, so the portrait's frame divided a
     different width and came out 233 against a 192px tile. */
  .hw-grid > .row, .index-grid, .cover-plate { grid-column: 1 / -1; }
  /* The headline gives up its indent too, and stops short of the frame's third cell. */
  .cover-text { grid-column: 1 / span 8; }
  .rail .tb { grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); column-gap: var(--g); }
  .rail .tb > div { grid-column: auto; grid-template-columns: 1fr; gap: 2px; }
  .cover > .rail, .p-cover > .rail, .grp > .rail, .dossier > .rail, .contact > .rail, .guide-main > .rail { margin-bottom: 8px; }
  .page-head .rail { padding-bottom: 8px; }
  .filters { display: flex; flex-wrap: wrap; gap: 4px 20px; margin-bottom: 12px; }

  .cover { align-items: end; }
  .cover-text { grid-column: 1 / span 7; }
  /* The plate keeps the WHOLE field here too, because it is the frame that measures the
     column, not the photograph. Narrowing it to 8/-1 made the three-column frame divide five
     columns instead of ten and the hero came out 91px against a 192px tile at 768. The
     headline gives up a column so it still clears the frame's third cell. */
  .cover-text { grid-column: 3 / span 4; }
  .cover-lines { grid-column: 1 / -1; grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .index-table { grid-column: 1 / -1; }
  .p-cover-plate { grid-column: 1 / span 8; }
  .p-context { grid-column: 1 / span 9; }
  .dossier-plate { grid-column: 1 / span 5; }
  .dossier-text { grid-column: 6 / -1; }
  .contact-main { grid-column: 1 / span 9; }
  .colophon-tb { grid-column: 1 / span 6; }
  .next { grid-column: 7 / -1; }
  .room-text { grid-column: 1 / span 6; }
  .room-plate { grid-column: 8 / -1; }
  .guide-text { grid-column: 1 / span 10; }
  .p-prose main > * { grid-column: 1 / span 10; }
  .profile-cols { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* ---------- phone + tablet pass (GSD mobile/tablet audit, 2026-09-24) ---------- */
/* Touch screens, any width: every control at least 44px tall. */
@media (pointer: coarse) {
  .nav a, .filters li a, [data-menu-close], .ft-links a, .tb dd a, .contact-links a, .sheet-cta a, .more { display: inline-flex; align-items: center; min-height: 44px; }
  .mh-theme { width: 44px; height: 44px; margin: -14px -12px -14px 0; }
  .idx tbody th a { display: flex; align-items: center; min-height: 44px; }
}
/* Tablet: the headline and cover open the page, the project's details follow; the Work
   index shows its covers; the project cover and the next project read at tablet size. */
@media (min-width: 761px) and (max-width: 1100px) {
  .mh-pos { white-space: nowrap; } /* "Architecture · 2 of 3" stays on one line at 12px */
  .cover-text { order: 1; }
  .cover-plate { order: 2; }
  .cover > .rail { order: 3; margin-top: 8px; }
  .cover-lines { order: 4; }
  .idx tbody th a { display: flex; align-items: center; gap: 16px; }
  .i-thumb { display: block; width: 88px; flex: none; }
  .i-thumb img { aspect-ratio: 4 / 5; object-fit: cover; }
  .idx td, .idx tbody th { vertical-align: middle; }
  .p-cover-plate { grid-column: 1 / -1; }
  .p-cover-plate .pl { margin-inline: auto; }
  .next .pl { width: 60%; }
}
/* Phones: labels a size up, the photo viewer uses the whole width, cookie choices sit in
   a slim bar at the very bottom instead of over the cover. */
@media (max-width: 760px) {
  :root { --t-meta: 12px; }
  .lb[open] { grid-template-columns: 1fr 1fr; grid-template-rows: 1fr auto; }
  .lb figure { grid-column: 1 / -1; grid-row: 1; padding-inline: 8px; }
  .lb img { max-height: calc(100svh - 150px); }
  .lb-p { grid-column: 1; grid-row: 2; justify-self: start; padding: 0 var(--m) 16px; }
  .lb-n { grid-column: 2; grid-row: 2; justify-self: end; padding: 0 var(--m) 16px; }
  .cc-banner { left: 0; right: 0; bottom: 0; max-width: none; border-width: 1px 0 0; padding: 10px var(--m) calc(10px + env(safe-area-inset-bottom)); gap: 10px; }
  .cc-actions > * { min-height: 44px; }
}

/* ---------- blocks added in the site editor, in the v4 voice ----------
   The class names are the editor's own hooks (it finds its blocks by them), so they stay;
   v4 gives them its type, rules and spacing. Stronger than the page's own
   <style data-ek-style> fallbacks, which were written for the old design. */
html[data-design="v4"] h2.nosto-h3, html[data-design="v4"] h3.nosto-h3 { font: 600 var(--t-section)/1.2 var(--f-sans); letter-spacing: -.012em; margin: 1.2em 0 .2em; max-width: 34ch; }
html[data-design="v4"] .ek-quote { margin: .6em 0; padding-left: 16px; border-left: 1px solid var(--ink); font: 400 var(--t-section)/1.3 var(--f-sans); letter-spacing: -.012em; max-width: 34ch; }
html[data-design="v4"] .ek-rule { border: 0; border-top: 1px solid var(--rule); opacity: 1; margin: 24px 0; width: 100%; }
html[data-design="v4"] .ek-figure { margin: 24px 0; }
html[data-design="v4"] .ek-figure img { display: block; width: 100%; height: auto; }
html[data-design="v4"] .ek-figure figcaption { margin-top: 8px; font: 400 var(--t-meta)/1.45 var(--f-mono); letter-spacing: .02em; color: var(--muted); }
html[data-design="v4"] .ek-btn-row { margin: 16px 0 0; }
/* the home Profile is a grid: an added block takes the text column, under the text */
.profile > [data-ek^="n:"] { grid-column: 1 / span 6; }
.profile > .ek-figure[data-ek^="n:"] { grid-column: 1 / span 3; } /* a picture block reads as a plate, not a banner */
@media (max-width: 760px) { .profile > [data-ek^="n:"] { grid-column: 1 / -1; } }

/* ---------- phone + tablet pass 2 (GSD mobile/tablet re-audit, 2026-09-24) ---------- */
.skip-link { min-height: 44px; display: inline-flex; align-items: center; }
/* Touch screens, any width: the remaining links reach 44px, and labels are a size up. */
@media (pointer: coarse) {
  :root { --t-meta: 12px; }
  .mh-name, .more a, .field.meta a, .grp-title a, .profile-cta a { display: inline-flex; align-items: center; min-height: 44px; }
  .proj-guide a { display: flex; align-items: center; min-height: 44px; }
  .proj-guide .pg-dot { display: none; }
  .cover-lines a { display: flex; align-items: center; min-height: 44px; padding-bottom: 0; } /* one inner line of text, so the credit wraps as words */
  .toc a { display: flex; align-items: center; min-height: 44px; padding-block: 0; }
  .filters li a { min-width: 44px; }
  .nav a { min-width: 44px; justify-content: center; }
  [data-menu-close], .lb-x { min-width: 44px; display: inline-flex; align-items: center; justify-content: flex-end; }
  .lb-p, .lb-n { min-width: 44px; }
  .tb:not([data-stack]) > div:has(dd a) { align-items: center; } /* a label stays level with a 44px link beside it; plain lists stay top-aligned */
  .contact-form .field input, .contact-form .field-select { min-height: 44px; }
  /* the menu sheet: Close and Theme sit on their rows */
  .menu-sheet > .g, .menu-foot { align-items: center; }
  .menu-sheet > .g > [data-menu-close] { margin-block: -14px; }
}
/* Phones: the label and its button share one row (the phone grid would give the label the whole row). */
@media (max-width: 760px) {
  .menu-sheet > .g > :first-child, .menu-foot > :first-child { grid-column: 1; }
}
/* A touch screen wider than 1100 (iPad landscape): no hover, so the Work index shows each
   cover in its row, and the next project reads at tablet size. */
@media (pointer: coarse) and (min-width: 1101px) {
  .index-table { grid-column: 3 / -1; }
  .index-preview { display: none; }
  .idx tbody th a { display: flex; align-items: center; gap: 16px; }
  .i-thumb { display: block; width: 88px; flex: none; }
  .i-thumb img { aspect-ratio: 4 / 5; object-fit: cover; }
  .idx td, .idx tbody th { vertical-align: middle; }
  .next .pl { width: 60%; }
}
/* Cookie choices never hide the end of the page, and stay one slim row. */
body.cc-open { padding-bottom: 96px; }
@media (max-width: 760px) {
  body.cc-open { padding-bottom: 84px; }
  .next .pl { width: 75%; }
}
@media (max-width: 560px) {
  .cc-banner { flex-direction: row; align-items: center; }
  .cc-text { flex: 1 1 auto; font-size: 12px; line-height: 1.35; }
  .cc-actions > * { flex: none; }
  .cc-btn { padding: 0 12px; }
}
/* Short screens (a phone on its side): the choices sit in a bar across the top and push the
   page down, so on first view they cover nothing; the headline is sized to fit the height. */
@media (max-height: 480px) and (orientation: landscape) {
  .cc-banner { top: 0; bottom: auto; left: 0; right: 0; max-width: none; border-width: 0 0 1px; padding: 6px max(var(--m), env(safe-area-inset-right)) 6px max(var(--m), env(safe-area-inset-left)); transform: translateY(-8px); }
  .cc-banner.is-in { transform: none; }
  body.cc-open { padding-top: 58px; padding-bottom: 0; }
  .cover { align-items: start; }
  .cover-text { align-self: start; padding-top: 8px; } /* the headline opens the screen instead of waiting at the foot of the plate */
  .cover-h1 { font-size: min(clamp(46px, 8.4vw, 150px), 13.2cqi, 12svh); }
}

/* ---------- phone typography + section rhythm (GSD 01-UI-REVIEW-mobile-typography-spacing) ----------
   One type scale on phones: display / title 32 / heading 24 / lead 21 / item 18 / body 16 / meta 12.
   One spacing scale: 64 between sections, 24 title to content, 16 heading to text, 12 label to content. */
@media (max-width: 760px) {
  :root { --sec: 64px; --off-m: 40px; --off-s: 24px; }
  .t-title, .guide-h1, .p-prose main h1,
  html[data-design="v4"] h2.nosto-h3, html[data-design="v4"] h3.nosto-h3 { font-size: 32px; line-height: 1.06; letter-spacing: -.03em; font-weight: 600; }
  /* .92, not .9: Inter needs .8885em of line box to keep a p or y clear of the cap below it,
     and .9 left only 0.6px at this size. Same reason as the desktop rule. */
/* Display line-height is 1, not the .92 it was until 2026-10-06. Inter's ink envelope
   for a headline charset is .9783em (tallest glyph is the i tittle, deepest is g), so .92
   overlapped: the y of "Hospitality" struck the i of "Commercial" at every width, 107
   overlapping pixels at 1920 and 27 at 375. .96 still overlaps; .99 still overlaps by 1px
   on the phone. Measured, not chosen. */
  .t-title.cover-h1 { font-size: 16cqi; line-height: 1; letter-spacing: var(--tr-cut); } /* the home headline stays the biggest words on the phone */
  .grp-title, .g-h { font-size: 24px; line-height: 1.15; letter-spacing: -.02em; }
  .t-section, .profile-lead, .dossier-text .t-section, .contact-main .t-section,
  .g-quote p, html[data-design="v4"] .ek-quote { font-size: 21px; line-height: 1.3; letter-spacing: -.012em; }
  .rl-t, .idx tbody th, .idx--guides tbody th, .g-step-t { font-size: 18px; line-height: 1.2; letter-spacing: -.015em; }
  .idx .idx-prod th[scope="row"] { font-size: 16px; }
  .g-intro, .g-steps p, .rl-sub, .p-prose main p, .p-prose main li { font-size: 16px; line-height: 1.55; }
  .steps .cap { font: 400 16px/1.5 var(--f-sans); letter-spacing: 0; text-transform: none; }
  .field-hint { letter-spacing: .02em; }
  .t-title, .t-section, .grp-title, .g-h, .rl-t, .idx tbody th, .g-step-t, .cl-t { text-wrap: balance; }
  .t-body p, .t-body, .g-intro, .g-steps p, .rl-sub, .p-context p, .steps .cap { text-wrap: pretty; }
  .index { padding-bottom: 0; }
  .room { margin-top: var(--sec); }
  .project .room { margin-top: 0; }
  .room-in { padding-block: 48px; row-gap: 12px; }
  .room-text .cap { margin-top: 8px; }
  /* a project sits closer to its neighbour than a section does, as on desktop (121 of 144).
     0.84 of --sec is 53.8px at the current 64px, i.e. the 54px asked for, and it keeps the
     relationship if --sec ever moves. Without any override .grp's own clamp gives 72px here,
     which is MORE than the gap between whole sections — the inversion this rule exists to fix. */
  .grp { padding-top: calc(var(--sec) * 0.84); }
  .colophon { margin-top: 0; padding-top: 24px; row-gap: 12px; }
  .colophon .next { margin-top: 28px; }
  .notfound { margin-bottom: 0 !important; }
  .guide-section { padding-bottom: 32px; }
  .guide-section + .guide-section { padding-top: 32px; }
  .guide-section:last-child { padding-bottom: 0; }
  .guide-main + .sec { padding-top: var(--sec); }
  .sec.g, .dossier, .contact, .guide-main { row-gap: 12px; }
  .dossier-plate + .dossier-text { margin-top: 20px; } /* 32px: the portrait was 13px above the lead, tighter than the 16px between two paragraphs */
  .cover > .rail .sec-label { margin-bottom: 12px; }
  .guide-toc { margin-bottom: 24px; }
  .sec .field > .t-section + .t-body { margin-top: 16px; }
  .profile { row-gap: 16px; }
  .profile-cols { margin-top: 8px; }
  .profile-body { margin-top: 8px; } /* 24px under the lead statement, as on the profile page: at 16px it read as one more paragraph */
  .profile > .pl { margin-top: 16px; } /* 32px above: a photo opens a block, so it cannot be tighter than the 23px between two text blocks. It is full-field now, so there is nothing left to align it against. */
  /* The first block after a page head takes --arrive, like the landing's Work section. A hard
     24px here beat --arrive on every inner page, because this sibling selector comes later in
     source order than .index and .dossier and carries the same specificity. */
  .page-head + * { padding-top: var(--arrive); }
  .p-cover { padding-top: 24px; }
  .cover-text { padding-bottom: 0; }
  .credits { row-gap: 0; }
  .credits > .tb + .tb { border-top: 0; }
  .credits .tb { grid-template-columns: 11ch 1fr; }
  .colophon-tb .tb + .tb { border-top: 0; }
  /* The footer's four blocks keep the ink rule that opens each of them and sit 24px apart, as
     they already do at every width above 760px. The phone was the only one where they had
     neither: row-gap: 0 with the rule removed ran Contact, Based in, Pages and Small print
     into one undifferentiated table, with more space inside a block (44px touch rows) than
     between two of them. Separation has to beat the space it separates. */
  .sheet-grid { row-gap: 24px; }
  /* A footer label needs the same 12px under it that a section label gets; .sheet-row is a
     bare .g, so it inherited no row-gap and "Get in touch" sat welded to the line below it.
     The second row's rail is an empty spacer div that only exists to hold a desktop column:
     left in the flow it would take a grid row of its own and spend that 12px on nothing. */
  .sheet-row { row-gap: 12px; }
  .sheet-row > .rail:empty { display: none; }
  .sheet-row + .sheet-row { margin-top: 32px; }
  .page-head.g { row-gap: 10px; }
  .contact.g { row-gap: 32px; }
  .profile-cols { row-gap: 0; }
  .profile-cols > .tb + .tb { border-top: 0; }
  .profile-cols .tb, .colophon-tb .tb { grid-template-columns: 11ch 1fr; } /* one value column down the block */
  /* pass 2: section breaks, one ink rule per labelled block, readable guide titles */
  .guide-close { margin-top: 40px; }
  .page-head + .sec { padding-top: 40px; }
  .sec.g > .field > .credits > .tb:first-child,
  .sec.g > .field > .steps > li:first-child > .tb,
  .res-list, .cover > .rail .tb, .colophon-tb > .tb:first-child { border-top-color: var(--rule); } /* the label above already draws the ink rule */
  .idx--guides tbody tr { display: grid; grid-template-columns: 4ch 1fr; column-gap: 12px; border-bottom: 1px solid var(--rule); }
  .idx--guides td, .idx--guides tbody th { border-bottom: 0; }
  .idx--guides tbody th { padding-bottom: 0; }
  .idx--guides td.i-n { grid-column: 2; text-align: left; padding-top: 0; }
  .dossier > .rail { margin-top: 12px; }
  .guide-work .idx tbody tr { display: grid; grid-template-columns: 4ch 1fr; column-gap: 12px; border-bottom: 1px solid var(--rule); }
  .guide-work .idx td, .guide-work .idx tbody th { border-bottom: 0; }
  .guide-work .idx td.i-n { grid-column: 2; text-align: left; padding: 0 0 12px; }
  .idx--guides tbody tr { align-items: baseline; }
  .guide-work .idx tbody tr { align-items: center; }
  .guide-work .idx td.i-n { padding-left: 60px; } /* the category lines up with the title, not the thumbnail */ /* the number sits level with the thumbnail and title */
  .project .cut--page { font-size: 14cqi; }
  .cut--band { font-size: min(var(--t-cut), 13.5cqi); }
  .res-list li:first-child a { padding-top: 12px; }
  /* list links: even padding instead of a centred 44px box, so one- and two-line entries keep one rhythm (still 44px to tap) */
  .cover-lines a { display: block; min-height: 0; padding-block: 14px; }
  .idx--guides tbody th { padding-top: 4px; }
  .idx--guides tbody th a { display: block; min-height: 0; padding-block: 11.5px; }
  .idx--guides td.i-n { padding-bottom: 12px; }
}

/* ---------- tablet + desktop typography (GSD 01-UI-REVIEW-tablet-desktop-typography) ----------
   One scale above the phone: display / headline / title / heading / statement / subhead /
   lead / item / body 17 / small 15 / meta. Phone rules (max-width: 760px) are untouched. */
@media (min-width: 761px) {
  /* S-1 wrap: no widows, balanced headings (the phone rules, carried up) */
  .t-title:where(:not(.cover-h1)), .guide-h1, .grp-title, .g-h, .rl-t, .t-section, .profile-lead, .idx tbody th,
  .g-step-t, .cl-t, .next-title, .g-quote p, html[data-design="v4"] .ek-quote,
  html[data-design="v4"] h2.nosto-h3, html[data-design="v4"] h3.nosto-h3 { text-wrap: balance; }
  .t-body, .t-body p, .g-intro, .g-steps p, .rl-sub, .p-context p, .cap, .p-prose main p,
  .p-prose main li, .nosto-body, .g-take p { text-wrap: pretty; }
  .contact-main .t-body, .steps .cap { text-wrap: balance; } /* two-line paragraphs: even lines */

  /* D-1 the home headline stays the biggest words on the page (owner rule) */
  .cover-h1 { line-height: 1; }
  .cut--band { font-size: min(var(--t-cut), 15.5cqi, 5.8vw); } /* headline = 13.2cqi of its field = 6.1-6.2vw on desktop */

  /* Title: one size for every page title (guide-h1 was 5% bigger than t-title) */
  .t-title:where(:not(.cover-h1)), .guide-h1, .p-prose main h1,
  html[data-design="v4"] h2.nosto-h3, html[data-design="v4"] h3.nosto-h3 { font-size: clamp(36px, 4.2vw, 64px); line-height: 1.04; letter-spacing: var(--tr-title); }
  /* Heading: project names and the next-project link (next-title no longer rivals the H1) */
  .grp-title, .next .next-title { font-size: clamp(30px, 3vw, 48px); line-height: 1.08; letter-spacing: -.025em; }
  /* Statement: one size for the three big leads (were 3 sizes) */
  .t-section.profile-lead, .contact-main .t-section, .dossier-text .t-section { font-size: clamp(26px, 2.4vw, 40px); line-height: 1.15; letter-spacing: -.02em; }
  /* Subhead */
  .g-h, .rl-t { font-size: clamp(24px, 1.9vw, 30px); line-height: 1.15; letter-spacing: -.02em; }
  /* Lead */
  .t-section, .g-quote p, html[data-design="v4"] .ek-quote { font-size: clamp(21px, 1.55vw, 26px); line-height: 1.3; }
  .g-intro { font-size: clamp(21px, 1.55vw, 26px); line-height: 1.45; }
  /* Item: list titles sit a clear step above body */
  .idx tbody th, .idx--guides tbody th, .g-step-t { font-size: clamp(18px, 1.4vw, 22px); line-height: 1.2; letter-spacing: -.015em; }
  /* Body: one size, one leading */
  .p-context, .p-context p, .g-steps p, .p-prose main p, .p-prose main li, .nosto-body, .g-take p:last-child { font-size: 17px; line-height: 1.55; }
  /* Small */
  .rl-sub, .idx .idx-prod th[scope="row"] { font-size: 15px; line-height: 1.5; }
  /* Meta: one tracking for lower-case mono */
  .field-hint { letter-spacing: .02em; }
}

/* Tablet: fixed steps (the clamps above bottom out here) + label directly above its value */
@media (min-width: 761px) and (max-width: 1100px) {
  .cut--band { font-size: min(var(--t-cut), 15.5cqi, 6.8vw); } /* headline = 7.1-7.2vw on tablet */
  .grp-title, .next .next-title { font-size: 30px; }
  .t-section.profile-lead, .contact-main .t-section, .dossier-text .t-section { font-size: 26px; }
  .g-h, .rl-t { font-size: 24px; }
  .t-section, .g-quote p, html[data-design="v4"] .ek-quote { font-size: 21px; }
  .g-intro { font-size: 21px; }
  .idx tbody th, .idx--guides tbody th, .g-step-t { font-size: 18px; }
  /* the 44px link box keeps its height, but its text sits 2px under the label like a plain value */
  .rail .tb dd a, .rail .tb dd { align-items: flex-start; }
  .rail .tb:not([data-stack]) > div { align-items: start; }
  .rail .tb > div, .sheet-grid .tb > div { align-content: start; } /* a cell stretched to its row keeps the value right under its label */
  .dossier-text { padding-left: 12px; } /* clear of the photo's corner marks */
  .rail .tb > div:has(a[href^="mailto:"]) { grid-column: span 2; } /* the address never breaks mid-word */
  .profile-cols .tb { grid-template-columns: 1fr; } /* three narrow columns: label above value, so words never break mid-word */
  .profile-cols .tb > div { grid-template-columns: 1fr; gap: 2px; align-content: start; }
}
/* tablet: credit values share one column; footer links sit under their labels like plain values */
@media (min-width: 761px) {
  .credits .tb, .colophon-tb .tb { grid-template-columns: 12ch minmax(0, 1fr); } /* one value column at every width above the phone */
}
@media (min-width: 761px) and (max-width: 1100px) {
  .credits > .tb + .tb { border-top-color: var(--rule); }
  .colophon-tb > .tb + .tb { border-top: 0; } /* one hairline between the credits and the counts */
}
@media (pointer: coarse) and (min-width: 761px) {
  .sheet-grid .tb dd a, .sheet-grid .ft-links a, .sheet-grid .contact-links a { align-items: flex-start; }
}
@media (min-width: 761px) {
  .sheet-grid .tb > div { align-content: start; align-items: start; } /* footer values sit right under their labels, touch or mouse */
}
/* ---------- Work index (GSD 01-UI-REVIEW-work-index) ---------- */
/* A category's intro sits at the top of the table column, so the filter menu stays at the same spot on /work and every category page. */
.index-lead { margin: 0 0 24px; max-width: 60ch; }
/* Desktop with a mouse: the table gets the room it needs, so every row is one line and one height. */
@media (min-width: 1101px) and (pointer: fine) {
  .index-table { grid-column: 3 / span 7; }
  .index-preview { grid-column: 10 / -1; }
  .idx .i-place, .idx .i-who { white-space: nowrap; }
}
/* the Photos column takes only what it needs, so Place and Architect keep one line (tablet and desktop) */
@media (min-width: 761px) { .idx .i-n { width: 1%; } }
/* portrait tablets: the Place column goes (as on the phone; the project page has it), so titles and architects keep one line */
@media (min-width: 761px) and (max-width: 900px) { .idx .i-place { display: none; } }

/* ---------- motion, first set (GSD phase 05: .planning/phases/05-motion/05-RESEARCH.md) ----------
   Motion draws the sheet; the frame moves, the photo stays; one easing, three durations.
   Everything here switches off with reduced motion, and browsers without a feature keep the still page. */
@media (max-width: 760px) { :root { --t-plate: 360ms; } } /* shorter on the phone (§21) */
@media (prefers-reduced-motion: no-preference) {
  /* one tempo: the Cut and the cookie bar use the tokens, hover states share --t-quick */
  html.js .cut--band { transition-duration: var(--t-plate); }
  .cc-banner { transition-duration: var(--t-block); }
  .grp-title a, .next .next-title { transition: opacity var(--t-quick) var(--ease); }
  .btn { transition: background-color var(--t-quick) var(--ease), color var(--t-quick) var(--ease); }
  .toc a { transition: color var(--t-quick) var(--ease); }
  .tb a, .p-prose main a { transition: text-decoration-color var(--t-quick) var(--ease); }
}
/* scroll measures: the datum draws down as you read; the footer name rises into its cut */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .datum::before { transform-origin: 50% 0; animation: datum-draw linear both; animation-timeline: view(); animation-range: cover 30vh cover calc(100% - 45vh); }
    .cut--footer { animation: word-rise linear both; animation-timeline: view(); animation-range: entry 0% entry 100%; }
  }
}
@keyframes datum-draw { from { transform: scaleY(0); } }
@keyframes word-rise { from { transform: translateY(35%); } }
/* page to page: only between projects, the photo you tapped carries into the next cover (v4.js decides) */
@view-transition { navigation: auto; }
.p-cover-plate .pl-media { view-transition-name: cover-plate; }
html.lb-vt .p-cover-plate .pl-media { view-transition-name: none; } /* opening or closing the viewer: only the tapped photo is carried, no copy of the cover on top */
/* the landing portrait carries into the profile page the same way a project cover does.
   Its own name, so it can never collide with cover-plate; v4.js picks which one to use. */
.dossier-plate .pl-media { view-transition-name: portrait; }
::view-transition-group(cover-plate), ::view-transition-group(portrait) { animation-duration: var(--t-plate); animation-timing-function: var(--ease); }
::view-transition-group(lb-plate) { animation-duration: var(--t-block); animation-timing-function: var(--ease); }
/* only the frame moves: no crossfade of two crops of the photo */
::view-transition-old(cover-plate), ::view-transition-old(lb-plate), ::view-transition-old(portrait) { animation: none; opacity: 0; }
::view-transition-new(cover-plate), ::view-transition-new(lb-plate), ::view-transition-new(portrait) { animation: none; height: 100%; object-fit: cover; }
/* the new page (or the viewer) is simply there; only the photo moves. The theme switch keeps its fade. */
:root:not(.theme-vt)::view-transition-old(root) { animation: none; opacity: 0; }
:root:not(.theme-vt)::view-transition-new(root) { animation: none; }
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; }
}

/* ---------- motion, second set (GSD phase 05) ---------- */
/* Film time as a rule: a hairline under the film measures its running time; the caption shows it. */
.pl--film .ft { display: block; position: relative; height: 1px; margin-bottom: 8px; background: var(--rule); }
.pl--film .ft::after { content: ""; position: absolute; inset: 0; background: var(--ink); transform: scaleX(var(--p, 0)); transform-origin: 0 50%; }
.pl .cap:has(.ft) { display: block; } /* rule on top, then number and time on one line */
.mods .pl--film .cap:has(.ft) { display: block; } /* project films show their time (no number, like the other plates) */
.mods .pl--film .cap-n { display: none; }
.cap-time { color: var(--muted); font-variant-numeric: tabular-nums; }
.cap-n + .cap-time { margin-left: .75em; }
@media (prefers-reduced-motion: no-preference) {
  .pl--film .ft::after { transition: transform 250ms linear; }
  .pl--film.ft-reset .ft::after { transition: none; }
}
/* Work index (desktop): the row's rule draws across, cell by cell; a leader carries it to the preview,
   which opens from its datum edge instead of fading. */
.index { position: relative; }
.idx-leader { position: absolute; height: 1px; background: var(--ink); transform: scaleX(0); transform-origin: 0 50%; pointer-events: none; }
@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .idx tbody tr:not(.idx-sep) > * { position: relative; }
  .idx tbody tr:not(.idx-sep) > *::after { content: ""; position: absolute; left: 0; right: 0; bottom: -1px; height: 1px; background: var(--ink); transform: scaleX(0); transform-origin: 0 50%; transition: transform 120ms var(--ease); }
  .idx tbody tr:not(.idx-sep):is(:hover, :focus-within, .is-on) > *::after { transform: scaleX(1); }
  .idx tbody tr:not(.idx-sep) > :nth-child(2)::after { transition-delay: 40ms; }
  .idx tbody tr:not(.idx-sep) > :nth-child(3)::after { transition-delay: 80ms; }
  .idx tbody tr:not(.idx-sep) > :nth-child(4)::after { transition-delay: 120ms; }
  .idx tbody tr:not(.idx-sep) > :nth-child(5)::after { transition-delay: 160ms; }
  .idx tbody tr:hover td, .idx tbody tr:hover th, .idx tbody tr:focus-within td, .idx tbody tr:focus-within th, .idx tbody tr.is-on td, .idx tbody tr.is-on th { border-bottom-color: var(--rule); } /* the ink is drawn, not switched */
  .idx-leader.is-on { transform: scaleX(1); transition: transform var(--t-quick) var(--ease) 160ms; }
  .pv { transition: none; }
}
/* Phones: the menu sheet is lowered from the masthead line and lifted away. */
@media (prefers-reduced-motion: no-preference) {
  .menu-sheet { clip-path: inset(0 0 100% 0); transition: clip-path var(--t-block) var(--ease), overlay var(--t-block) allow-discrete, display var(--t-block) allow-discrete; }
  .menu-sheet[open] { clip-path: inset(0); }
  @starting-style { .menu-sheet[open] { clip-path: inset(0 0 100% 0); } }
  .menu-sheet::backdrop { background: transparent; }
}

/* ---------- whole-site audit 06 (GSD) ---------- */
/* Small desktops and iPads on their side (1101-1320): the rail is narrow, so each label sits above its value. */
@media (min-width: 1101px) and (max-width: 1320px) {
  .p-cover > .rail .tb, .cover > .rail .tb { grid-template-columns: 1fr; }
  .p-cover > .rail .tb > div, .cover > .rail .tb > div { grid-template-columns: 1fr; gap: 2px; }
}
/* Very wide screens: the home cover plate is a little shorter, so the headline is not left far below the rail. */
@media (min-width: 1700px) {
  .cover-plate .pl { --cap-h: 70svh; }
}

/* accessibility (06-A11Y-REVIEW) */
.contact-form .field input:focus-visible, .contact-form .field textarea:focus-visible, .contact-form .field-select:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }
.contact-form .field input:focus, .contact-form .field textarea:focus, .contact-form .field-select:focus { box-shadow: 0 1px 0 0 var(--ink); }
.form-status:empty { display: none; }
.cover-h1 { overflow-wrap: anywhere; } /* only matters when a reader widens letter spacing: the headline wraps instead of widening the page */
@media (max-width: 400px) {
  .idx td, .idx tbody th { padding-right: 8px; }
  .idx .i-no { width: auto; }
  .idx tbody th a { overflow-wrap: anywhere; }
}

/* Page and project titles: flush with the text column (a small optical correction for the first
   letter, no hang) and lines that never touch; on phones the photo below keeps clear of the descenders. */
.cut--page { margin-left: -.04em; line-height: 1; }
@media (max-width: 760px) {
  .cut--page { margin-left: -.04em; }
  .p-cover { padding-top: 32px; } /* the title's descenders (the y in "by") clear the photo's crop marks */
}

/* ---------- motion, third set (GSD phase 05 ideas 1, 2, 5, 7; the hover frame, 14, was removed at Chris's request) ---------- */
/* 1. (removed 2026-09-27: the crop marks are simply there, no draw) */
/* 2. Home cover lines point at the plate (desktop): the other covers wait on top, unseen. */
.cv-alt { position: absolute; inset: 0; z-index: 0; opacity: 0; pointer-events: none; }
.cv-alt.is-on { opacity: 1; }
@media (prefers-reduced-motion: no-preference) { .cv-alt { transition: opacity var(--t-quick) var(--ease); } }
/* 5. Running sheet number (desktop): the photo you are reading, held in the rail beside the photos. */
.seq-count { display: none; }
@media (min-width: 1101px) {
  .seq-count { display: block; position: sticky; top: 50svh; color: var(--muted); font-variant-numeric: tabular-nums; }
}

/* Home groups: every photo at one height (Chris, 2026-09-27: no big and small). On phones a group shows
   two photos side by side at the same size instead of one big plate over a pair; the project has them all. */
@media (max-width: 760px) {
  .grp-plates .row--3 > .pl:first-child { flex-basis: 0%; }
  .grp-plates .row--3 > .pl:nth-child(3) { display: none; }
}

/* ---------- home hierarchy: the work comes before the biography ----------
   (phones: GSD 11, 2026-09-27. Every width: 2026-10-06.)
   The landing page tells the story in one order at every width: the cover, then the work,
   the film, then more about Chris, then how he works.

   Measured 2026-10-06, live, at 1440x900: the profile block used to sit second on a laptop,
   so the first photograph of actual work started 1.76 screens down and the second screen a
   visitor saw was prose at 11.5% photograph. Phones already ran work-then-biography; now
   both do, and the phone order is unchanged by this.

   The order is stated for EVERY child rather than left to the default, because it used to
   hold only by accident: `.sec` does not match the work section (`sec--work`) or the film
   room (`room`), so those two kept `order: 0` and happened to sort first. Renaming either
   class would have silently reordered the landing page. Note `main` is a flex column here,
   so margins between these sections no longer collapse - the rhythm was measured at 1920,
   1440, 1280, 1000 and 375 before this shipped. */
.profile-cta { display: none; }
.p-home main { display: flex; flex-direction: column; }
.p-home main > .cover { order: 0; }
.p-home main > .sec--work { order: 1; }
.p-home main > .room { order: 2; }
.p-home main > .sec { order: 4; }
.p-home main > .sec[data-eks*="sec-profile"] { order: 3; }
@media (max-width: 760px) {
  .p-home .cover > .rail, .p-home .cover-lines { display: none; }
  .p-home .grp .tb > div:has(dd[data-count]) { display: none; }
  .p-home .profile-cta { display: block; grid-column: 1 / -1; margin: 24px 0 0; }
  .p-home .room-plate .pl { width: min(100%, calc(70svh * var(--ar))); margin-inline: auto; }
  .p-home .steps .tb dd { font: 600 18px/1.2 var(--f-sans); letter-spacing: -.015em; text-transform: none; }
  .p-home .sheet-cta { font-size: 32px; line-height: 1.1; letter-spacing: -.03em; }
}
/* dark theme: the film room keeps an edge, so it does not sink into the page */
:root[data-theme="dark"] .room { border-block: 1px solid var(--room-rule); }

/* ==========================================================================
   ACCENT — Klein blue #0018F9 (2026-10-03, Chris's pick)
   ONE self-contained block. Delete from this comment to the end of the file to
   go back to monochrome: every touchpoint below falls back to its ink/rule base.

   The rule Chris set when we last tried an accent (terracotta, 2026-09-17):
   95% neutral, and the accent is FUNCTIONAL only — it marks interaction,
   direction, what is active and what has focus. It is never a brand colour on
   a large area and it never touches a photograph. So at rest the page is still
   monochrome; the blue appears when you point at something, and on the one
   "you are here" marker.

   Contrast, measured, on the light page #ffffff (the only page there is now —
   see the dark-mode block at the end of this file):
     #0018F9 on #ffffff   8.40:1   (AA 4.5, AAA 7)
     #ffffff on #0018F9   8.40:1   (the button's hover fill)

   ONE colour. There is deliberately no pressed variant and no lighter sibling:
   Chris, 2026-10-03, "i dont want varietions of it ... i want to be all at the
   exact color i choose". That now includes the black film room, where the accent
   measures 2.36:1 — his explicit decision, recorded as a waiver in 13-contrast.js.
   ========================================================================== */
/* ONE colour, everywhere, exactly as picked (Chris, 2026-10-03: "i want to be all at the
   exact color i choose"). No pressed variant, no lighter sibling for dark surfaces, and
   nothing that fades it — see the opacity overrides below. */
:root { --acc: #0018F9; }

/* 1. focus — the accent's most useful job. Every surface, the film room included.
   The room is the one place the accent measures badly: #0018F9 on #0a0a0a is 2.36:1,
   under the 3:1 WCAG asks for a focus indicator. Chris decided the ring is blue there
   anyway (2026-10-03) — one colour everywhere beats one legible exception. The only
   focusable thing in that room is the film itself (site.js gives it tabindex="0"), so
   this affects keyboard users on a single element. 13-contrast.js records it as WAIVED
   with the real number, rather than pretending it passes. */
:focus-visible { outline-color: var(--acc); }
/* the film sets the whole `outline` shorthand at higher specificity (line ~436), so the
   generic rule above never reaches it — this is the one that actually turns it blue */
.pl--film video:focus-visible { outline-color: var(--acc); }
/* WCAG 1.4.11 (Non-text Contrast): a focus indicator needs 3:1 against the colours beside
   it. The accent is 8.40:1 on the page, but only 2.36:1 inside the black film room, which
   is the one place it failed. The ring stays the exact accent — a light halo either side
   supplies the contrast. That is the standard two-tone indicator, and it works on any
   backdrop. box-shadow paints under the outline, so the order out from the element is
   halo (0-4px), accent ring (4-5px), halo (5-6px).
   Measured: halo vs the room 17.66:1, accent vs the halo 8.40:1 — every adjacent pair passes. */
.room :focus-visible { box-shadow: 0 0 0 6px var(--ink); }
.contact-form .field input:focus-visible,
.contact-form .field textarea:focus-visible,
.contact-form .field-select:focus-visible { outline-color: var(--acc); }

/* 2. The link answers with the accent (Chris, 2026-10-03: the underline alone was "too
   little"). FOCUS is never gated — a keyboard user on a tablet still needs it. HOVER is,
   because iOS Safari applies :hover on a tap and keeps it until you tap somewhere else,
   which would leave a tapped title sitting blue. */
a:focus-visible { color: var(--acc); text-decoration-color: var(--acc); }
.toc a:focus-visible { color: var(--acc); }
/* these faded to .7 on hover, which rendered the blue as roughly rgb(77, 93, 251) — a
   second, lighter blue. The accent is one colour, so the fade goes. */
.grp-title a:hover, .next:hover .next-title { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
  a:hover { color: var(--acc); text-decoration-color: var(--acc); }
  .toc a:hover { color: var(--acc); }
  /* ...except the display titles sitting directly above their own photographs. Colouring
     them sends the eye to the heading instead of the work — the same reason .grp-title is
     ink at rest. They answer with an underline, matching how .rl-t already behaves. */
  .grp-title a:hover, .next:hover .next-title {
    color: var(--ink); text-decoration: underline; text-underline-offset: .15em; text-decoration-thickness: 1px;
  }
}

/* 2b. the small action links are blue AT REST, so you can find the way forward without
   hunting for it. This is also where the arrows get their colour: the glyph is plain text
   inside the link, so it simply inherits. No markup change, nothing to regenerate.
   Only mono/meta-sized links — never a headline, a project title or body copy. */
.more, .more a, .profile-cta a, .sheet-cta a,
.sec--work > .g .field.meta a, .proj-guide a,
/* 2026-10-03, using the headroom under the 60-30-10 convention (accent ~10% of a design).
   THE LINE: links in lists and navigation take the accent, up to the budget. Where colouring
   a link would make blue the dominant colour of its page, it stays ink and turns blue on
   hover instead. The budget is the arbiter, not taste — measured as the accent's share of
   rendered text area, per page:
     home 7.2%   work 4.5%   contact 4.1%   resources 1.6%
   Two versions were built and measured. Adding .rl-t (the guide titles on /resources, which
   are nearly the only text on that page) took it to 17.6% and the page read as blue-on-white,
   so it was backed out. .grp-title and .next-title stay ink for the same reason: they sit
   above their own photographs. Both still go blue on hover.

   The .idx project titles came out too (2026-10-03, Chris: "its too much"). That takes the
   accent off /work, the category listings and a guide's "see the work" table. Those rows keep
   their affordance without it: the rule under a hovered row already draws in the accent, and on
   a real pointer the title still colours on hover. .idx--guides was excluded earlier. It is the Resources block at the foot of the home
   page, and it was taking 70-82% of that page's blue — the least important block wearing the
   loudest colour while the work carried almost none. Taking it to ink moves the home page
   from 8.2% to 2.4% and puts the project names in front. Colouring .grp-title instead was
   measured at 8.0% with the work leading, and rejected on sight: a saturated heading directly
   above every photo group pulls the eye before the photographs do, which is backwards for a
   portfolio. Every other .idx IS the project navigation (/work, the listings, a guide's
   "see the work" table) and keeps the accent. */
.rl-m, .rl-n,
.filters li a,
.ft-links a,
.contact-links a,
.tb dd a { color: var(--acc); }
/* The home cover lines are "01 Project Name · Architect · Place" inside one link. Colouring
   the link (v1.8.7) put the accent on everything EXCEPT the project name, because .cl-t
   sets its own ink — so the metadata shouted and the work whispered. The accent belongs on
   the name; the number stays muted and the trailing metadata stays ink. */
.cover-lines .cl-t { color: var(--acc); }
/* the active filter is the accent too, and says so with the underline, like the nav does */
.filters [aria-current="page"] { color: var(--acc); }
/* WCAG 1.4.1: these two sit INSIDE running text ("Have a project? Start a project →",
   "Related guide · Title →"), so colour alone may not distinguish them. The accent is only
   2.50:1 against the black body text — below the 3:1 that a colour-only inline link needs —
   so the underline is what carries compliance. It was rgba(0,0,0,.15), technically present
   but barely perceptible. currentColor makes the cue as visible as the rule assumes. */
.sheet-cta a, .proj-guide a { text-decoration-color: currentColor; }

/* 3. "you are here" — the only accent visible at rest */
.nav [aria-current="page"],
.filters [aria-current="page"] { text-decoration-color: var(--acc); }

/* 4. the drawn rules that answer a pointer */
.res-list a:hover, .res-list a:focus-visible { border-bottom-color: var(--acc); }
.idx tbody tr:hover td, .idx tbody tr:hover th,
.idx tbody tr:focus-within td, .idx tbody tr:focus-within th,
.idx tbody tr.is-on td, .idx tbody tr.is-on th { border-bottom-color: var(--acc); }

/* 5. the one filled control: blue-bordered at rest, blue-filled under the pointer */
.btn { border-color: var(--acc); }
.btn:hover { background: var(--acc); }
.btn:active { background: var(--acc); }

/* 6. a field you are typing in */
.c-form input:focus, .c-form textarea:focus,
.contact-form .field input:focus, .contact-form .field textarea:focus,
.contact-form .field-select:focus { border-bottom-color: var(--acc); }

/* ==========================================================================
   DARK MODE OFF (2026-10-03, Chris: "i think the dark mode does not work.
   disable it for now, hide the toogle.")

   Temporary and CSS-only, on purpose. The pre-paint script that reads the
   stored preference and sets data-theme is covered by a CSP hash in nginx
   (deploy/chrislianopoulos.live.nginx) and is carried forward into every page
   by adoptHead, so changing it would mean a new hash AND a full regeneration.
   Instead the two dark blocks are simply answered later in this file with the
   light values, and the toggles are hidden. The script still runs and still
   sets data-theme="dark" — it just no longer changes anything here.

   To bring dark mode back: delete this block. Nothing else was touched.

   KNOWN GAP: that same script also sets <meta name="theme-color"> to #000000
   for a visitor whose OS prefers dark, so on a phone the browser's own chrome
   can still be black around a white page. CSS cannot reach a meta tag; fixing
   it needs the generator + CSP hash + regeneration.
   ========================================================================== */
:root[data-theme="dark"] {
  --err: #b3261e; --ink: #000; --page: #fff; --rule: rgba(0, 0, 0, .15); --muted: #6b6b6b;
  --room: #0a0a0a; --room-rule: rgba(255, 255, 255, .18);
  color-scheme: light;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --err: #b3261e; --ink: #000; --page: #fff; --rule: rgba(0, 0, 0, .15); --muted: #6b6b6b;
    --room: #0a0a0a; --room-rule: rgba(255, 255, 255, .18);
    color-scheme: light;
  }
}
:root[data-theme="dark"] .room { border-block: 0; } /* the light page draws no border here */
[data-theme-toggle] { display: none; } /* both of them: the masthead ◐ and the one in the menu */

/* ==========================================================================
   HEADER: hides going down, reveals going up (Chris, 2026-10-03)

   OPAQUE, never frosted. The v3 header was sticky + frosted and bled through the
   iOS status bar, which is why the standing rule says the mobile header must not be
   sticky. A solid bar does not have that failure mode, so this one is sticky on every
   width — but it is the thing to check first if anything looks wrong on an iPhone.

   To remove: delete this block and the matching section in site/v4.js.
   ========================================================================== */
.masthead {
  position: sticky; top: 0; z-index: 40;
  background: var(--page);
  transition: transform var(--t-quick) var(--ease);
}
:root.mh-hide .masthead { transform: translateY(-100%); }
@media (prefers-reduced-motion: reduce) { .masthead { transition: none; } }

/* the rails and the index preview already stick at 24px — they have to clear the bar,
   or they slide underneath it. --mh is set from the header's real height by v4.js. */
.grp > .rail, .index-preview, .p-cover > .rail,
.dossier > .rail, .contact > .rail, .guide-toc { top: calc(24px + var(--mh, 60px)); }

/* an in-page anchor must not land behind the bar */
html { scroll-padding-top: calc(var(--mh, 60px) + 8px); }

/* ==========================================================================
   CROP MARKS OFF (Chris, 2026-10-03: "dont like these symbols")

   The corner brackets on the landing hero, the profile portrait and every project
   page cover — the three plates built with `crop: true`. Hidden here rather than
   removed from the generator, so nothing has to be regenerated and it comes back
   by deleting this block.

   The caption spacing goes with them: .has-crop > .cap carried 22px to clear marks
   that sat 12px outside the plate. With no marks it takes the normal 8px.
   ========================================================================== */
.crop { display: none; }
.has-crop > .cap { margin-top: 8px; }

/* ---------- landing headline: start where every other page's title starts ----------
   Measured 2026-10-04 at 1440px. The cover headline was bottom-aligned against a
   ~700px plate, so it began 324px below the masthead while every other page's h1
   begins at 58px, and its cap was 13.2cqi of a six-column container, which put it at
   87.6px: the smallest h1 on the site, and 46% of the footer wordmark on its own page.
   That is why the landing read weaker than the pages it leads to.

   Top-aligning the block and lifting the cap to 15.4cqi gives 102.2px and a 43px start,
   without touching the photograph (436px either way), the composition, or the height of
   the cover (830px either way). Widening the words instead costs the hero photo a
   quarter of its width, and giving them the full field doubles the cover to 1502px and
   pushes the work off the first screen, so neither was taken.

   Scoped to 761px and up on purpose: the phone headline is already top-aligned and
   already 16cqi, and this cap would SHRINK it from 54.9px to 52.8px.
   Reversible: delete this block. */
@media (min-width: 761px) {
  .cover-text { align-self: start; padding-top: 0; padding-bottom: 0; }
  /* No line-height here on purpose. The .92 set above is what the font needs: Inter's p/y
     ink descends .1722em below the baseline and its caps rise .7163em, so a line box tighter
     than .8885em puts a descender into the cap of the line below. At .88 the clearance was
     -0.9px and "Hospitality &" collided with "Commercial Photography". */
  .cover-h1 { font-size: min(var(--t-cut), 15.4cqi); margin-left: -.06em; }
}
